chrishough chrishough - 9 months ago 92
Ruby Question

rails rspec rake task with parameters

I have been working to get test coverage on the following rake task with the attached spec. However, nothing I appear to try sends the

env
parameter through correctly?

Test Failures

1) myapp:database tasks myapp:database :recreate works
Failure/Error: system("RAILS_ENV=#{args[:env]} rake db:create")

main received :system with unexpected arguments
expected: (/RAILS_ENV=testing rake db:drop/)
got: ("RAILS_ENV=testing rake db:create")
Diff:
@@ -1,2 +1,2 @@
-[/RAILS_ENV=testing rake db:drop/]
+["RAILS_ENV=testing rake db:create"]

# ./lib/tasks/database.rake:9:in `block (3 levels) in <top (required)>'
# ./spec/lib/tasks/database_rake_spec.rb:17:in `block (5 levels) in <top (required)>'
# ./spec/lib/tasks/database_rake_spec.rb:17:in `block (4 levels) in <top (required)>'
# -e:1:in `<main>'


Spec

describe 'myapp:database tasks' do
include_context 'rake'
let(:task_paths) { ['tasks/database', 'tasks/seed'] }

# rubocop:disable RSpec/MultipleExpectations
describe 'myapp:database' do
before do
invoke_task.reenable
end

# TODO!
context ':recreate', focus: true do
let(:task_name) { 'myapp:database:recreate' }

it 'works' do
expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:drop/).and_return(true)
expect { invoke_task.invoke('testing') }.to output(
"\nDropping the testing database\n"\
"\nCreating the testing database\n"\
"\nRunning the testing database migrations\n"
).to_stdout
end
end

# rubocop:disable RSpec/MessageSpies
context ':reset' do
let(:task_name) { 'myapp:database:reset' }

it 'works' do
expect(Rake::Task['myapp:database:recreate']).to receive(:invoke).twice
expect(Rake::Task['myapp:seed:all']).to receive(:invoke)
expect { invoke_task.invoke }.to output("\nResetting the development and testing databases\n").to_stdout
end
end
end
# rubocop:enable all
end


Task

namespace :myapp do
namespace :database do
if Rails.env.development? || Rails.env.test?
desc 'Drop and create a database, ["env"] = environment'
task :recreate, [:env] => [:environment] do |_t, args|
puts "\nDropping the #{args[:env]} database\n"
system("RAILS_ENV=#{args[:env]} rake db:drop")
puts "\nCreating the #{args[:env]} database\n"
system("RAILS_ENV=#{args[:env]} rake db:create")
puts "\nRunning the #{args[:env]} database migrations\n"
system("RAILS_ENV=#{args[:env]} rake db:migrate")
end

desc 'Reset the db data and setup development'
task reset: :environment do
puts "\nResetting the development and testing databases\n"
%w(development test).each do |db|
Rake::Task['myapp:database:recreate'].invoke(db)
end
Rake::Task['myapp:seed:all'].invoke
end
end
end
end


Shared Context

shared_context 'rake' do
let(:invoke_task) { Rake.application[task_name] }
let(:highline) { instance_double(HighLine) }

before do
task_paths.each do |task_path|
Rake.application.rake_require(task_path)
end
Rake::Task.define_task(:environment)
end

before do
allow(HighLine).to receive(:new).and_return(highline)
# rubocop:disable all
allow_any_instance_of(Object).to receive(:msg).and_return(true)
allow_any_instance_of(Object).to receive(:error_msg).and_return(true)
# rubocop:enable all
end
end


Update

context ':recreate' do
let(:task_name) { 'myapp:database:recreate' }

it 'works' do
expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:drop/).and_return(true)
expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:create/).and_return(true)
expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:migrate/).and_return(true)
expect { invoke_task.invoke('testing') }.to output(
"\nDropping the testing database\n"\
"\nCreating the testing database\n"\
"\nRunning the testing database migrations\n"
).to_stdout
end
end

Answer Source

As I mentioned in a comment, the task isn't being invoked from the test because of the way you're stubbing here:

    expect(Rake::Task['myapp:seed:all']).to receive(:invoke)

Although this checks whether invoke was called, it doesn't actually invoke invoke (actually, it makes the method return nil). To change that, you can either:

  1. tack on an and_return(<something>)
  2. tack on and_call_original.

Probably in this case you'd want to use and_call_original since you want to investigate what actually happens in the task. In order to stub individual method calls in the task, the approach you have been using (expect_any_instance_of(Object).to receive(:system)) will technically work, but could probably be refactored to be more decoupled from the code.

For example, you could separate each system call into its own method (available to the rake task), and then call those from the test. Then in order to stub it you only need to pass the method name. If you want, you can then go and unit test each of those methods individually, putting the system call expectation in there.

I don't recall where exactly but I've heard it advised to not do any acual programming in Rake tasks. Put your code somewhere in your regular codebase, and call those methods from the rake task. This can be seen as an example of a more general pattern which is to refactor large methods into smaller ones. Writing code this way (and also with a functional style, but I won't get into that) makes your life easier when testing.


onto your followup question:

as you can see in the test case's failure message, the only difference between the actual and expected is that one is a regex and the other is a string.

A simple fix for this is to change this line:

    expect_any_instance_of(Object).to receive(:system).with(/RAILS_ENV=testing rake db:drop/).and_return(true)

so that the with() argument is a string, not a regex