0

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
halfer
  • 19,824
  • 17
  • 99
  • 186
Chris Hough
  • 3,389
  • 3
  • 41
  • 80
  • `env` is a reserved word, fwiw – max pleaner Mar 12 '17 at 03:26
  • @maxple ok, I altered that, and still the tests are not working? thoughts? – Chris Hough Mar 12 '17 at 03:32
  • @maxple the binding.pry never gets hit? – Chris Hough Mar 12 '17 at 03:34
  • Looking again I think the issue might be with your stubs. "expect/allow to receive" is a stub, and the method you stub won't be actually called after you've made the stub. try tacking on `.and_call_original` at the end of the `to_receive(:invoke)` lines. – max pleaner Mar 12 '17 at 03:39
  • @maxple I updated it. Now the spec runs, however, it attempts to fire the system commands to delete the db instead of stub/moching them. I was using that line you mentioned to prevent that. Thoughts on how to rewrite that? – Chris Hough Mar 12 '17 at 03:41
  • @maxple can you post an example answer we can collaborate on? – Chris Hough Mar 12 '17 at 03:42
  • lol? didn't mean to wipe your database, sorry. sure. – max pleaner Mar 12 '17 at 03:43
  • thank you. in my other specs I am using a combination of ```expect_any_instance_of(Object).to receive(:system).with(/rake db:seed/).and_return(true)``` for example – Chris Hough Mar 12 '17 at 03:43

1 Answers1

2

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

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • With your feedback, I posted an update. What are your thoughts? This appears to go green now? The ```context ':reset' do``` was working. – Chris Hough Mar 12 '17 at 03:59
  • I have to ask, do you have any thoughts on http://stackoverflow.com/questions/42754070/ruby-rails-rspec-rake-does-not-swallow-message-output or http://stackoverflow.com/questions/42753946/ruby-rails-rspec-rake-resque-task-spies ? Testing rake specs is tough. – Chris Hough Mar 13 '17 at 01:51
  • @chrishoughThere's kind of a lot to say there. As you can see, testing Stdout might be finnicky because of warnings and such. The first link provides one way to do it (capturing stdout in a temporary variable). I'm not sure I would personally go through the effort in this case. Not to say doing so would be bad, since you will add the skill to your reportoire. Regarding doubles and spies - I haven't personally used these a whole lot, but if you find a place for them, good on you. – max pleaner Mar 13 '17 at 06:59
  • @chrishough also, keep in mind that how 'difficult' testing is (beyond learning rspec itself) depends on what level you're aiming to test at. Realistically if you write a test case for every single line of code then you're going to have to throw a lot of that away if you refactor. That's why it's sometimes advised to try and decouple your specs from your code - write your tests in such a way that you're checking the result, not the implementation. – max pleaner Mar 13 '17 at 07:04
  • very true. I have been in ruby now for awhile but there are a few concepts like testing rake tasks and spies that I have found elusive over the years. I definitely agree with testing behavior over tightly coupling, but rake seems to take on a different animal here. I have never been a fan of testing tasks until recently after watching a few prezos. – Chris Hough Mar 13 '17 at 16:38
  • @chrishough I usually take out all the logic in the rake task and put it into a separate method. Then unit test that method, and just test that the rake task calls it – max pleaner Mar 13 '17 at 17:12
  • I have a number of rake tasks like that as well. In this case I felt the CLI calls belonged in rake. Are you saying you would go so far as to create a lib/class and extract all of them completely? – Chris Hough Mar 13 '17 at 17:13