8

I'm trying to write tests in rspec for two rake tasks which are defined in the same file (in a Rails 3.0.11 project). For some reason only one of them passes. I've written a small demo to abstract away the actual content of the tasks, and the same thing happens. Both tasks work when invoked with rake from the command line. What's going on? Here's my demo:

lib/tasks/demo_tasks.rake

namespace :demo do
  task :test => :environment do
    puts "test!"
  end

  task :test_two => :environment do
    puts "second test!"
  end
end

spec/lib/tasks/demo_spec.rb

require 'spec_helper'
require 'rake'

describe "test tasks" do
  let(:rake) do
    app = Rake::Application.new
    app.options.silent = true
    app
  end

  before :each do
    Rake.application = rake
    Rake.application.rake_require 'lib/tasks/demo_tasks',
                                  [Rails.root.to_s]
    Rake::Task.define_task :environment
  end

  describe "demo:test" do
    it "runs" do
      rake["demo:test"].invoke
    end
  end

  describe "demo:test_two" do
    it "also_runs" do
      rake["demo:test_two"].invoke
    end
  end
end

rspec spec/lib/tasks/demo_spec.rb

test tasks
  demo:test
test!
    runs
  demo:test_two
    also_runs (FAILED - 1)

Failures:

  1) test tasks demo:test_two also_runs
     Failure/Error: rake["demo:test_two"].invoke
     RuntimeError:
       Don't know how to build task 'demo:test_two'
     # ./spec/lib/tasks/demo_spec.rb:26:in `block (3 levels) in <top (required)>'
gregates
  • 6,607
  • 1
  • 31
  • 31

3 Answers3

9

Nutshell: Change your before to a before :all (instead of :each).

Or: Pass an empty array as a third parameter to rake_require.

Rake.application.rake_require 'lib/tasks/demo_tasks', 
                              [Rails.root.to_s], 
                              []

Details

def rake_require(file_name, paths=$LOAD_PATH, loaded=$")
  fn = file_name + ".rake"
  return false if loaded.include?(fn)
  ...

$" is a Ruby special variable that holds an array of modules loaded by require.

If you don't pass the optional parameter, rake_require will use that array of modules loaded by Ruby. This means the module won't be loaded again: Ruby knows the module was loaded, rake checks to see what Ruby knows, and it's a new rake instance for each test.

Switching to before :all worked because it meant the let block only ran once: one rake instance, one module load, everybody is happy.

All this said, why reload the rake environment twice anyway? Your goal is to test your tasks, which doesn't require a fresh rake context for every spec.

You could eliminate the local altogether at the cost of some minor verbosity in each spec:

describe "test tasks" do
  before :all do
    Rake.application = Rake::Application.new
    Rake.application.rake_require 'lib/tasks/demo_tasks', [Rails.root.to_s]
    Rake::Task.define_task :environment
  end

  describe "demo:test" do
    it "runs" do
      Rake::Task["demo:test"].invoke
    end
  end
end

You could define an instance variable in the before block to avoid the Rake::Task reference:

before :all do
  @rake = Rake::Application.new
  Rake.application = @rake
  Rake.application.rake_require 'lib/tasks/demo_tasks', [Rails.root.to_s]
  Rake::Task.define_task :environment
end

describe "demo:test" do
  it "runs" do
    @rake["demo:test"].invoke

IMO, less desirable for a number of reasons. Here's a summary I agree with.

Community
  • 1
  • 1
Dave Newton
  • 158,873
  • 26
  • 254
  • 302
  • This does make both tests pass. But can you explain why? – gregates Sep 09 '12 at 00:18
  • @gregates Not entirely without more digging. My guess is that the second `rake_require` isn't loading the file because it was loaded once already, so the new task definitions are gone for the second test. – Dave Newton Sep 09 '12 at 00:22
  • Cool. Well, thanks for the assistance. Still would like to understand this phenomenon better. – gregates Sep 09 '12 at 00:26
  • For reasons I don't understand, if you put the `require` etc.in `before :all`, and if you invoke the *same* task twice, the second invocation doesn't happen. Fortunately, the other suggestion works: leave the `require` in `before :each` and use `[]` as the third parameter. Then the second invocation works. – Mark Berry Mar 13 '15 at 20:14
  • Haven't tried with your example, but maybe using `execute` instead of `invoke` would make all your tasks to run. [invoke](http://rake.rubyforge.org/classes/Rake/Task.html#M000119) only runs the task if it consider it is necessary (don't ask me about the criteria used) – Waiting for Dev... Dec 02 '15 at 10:08
5

A popular search engine led me here, as in my case I was seeing tests fail when #invoke was used more than once for a given test. The solution below builds on @dave-newtown's answer.

The problem arises because at the time of writing (Rake v12), #invoke runs a task once only so for example:

RSpec.describe "demo:test" do
  it "runs" do
    expect(SomethingWeAreInvoking).to eql(ProofIfWasInvoked)
    Rake::Task["demo:test"].invoke
  end

  it "runs" do
    expect(SomethingWeAreInvoking).to eql(ProofIfWasInvoked)
    Rake::Task["demo:test"].invoke
  end
end

...might pass for whichever it runs first if the test is well written and the task invokes correctly, but will always then fail for the second it as within a given Rake.application, the use of #invoke only ever runs a task once. The has-been-run-before state is apparently remembered inside the Rake.application instance.

Yes, this does mean that at least under Rake v12 as tested, a lot of online articles showing how to test Rake tasks are (fo) incorrect, or get away with it because they only show a single test for any given task in their examples.

We could use Rake's #execute, but this doesn't run dependent tasks so causes its own set of problems and gets us even further away from testing the Rake stack as if it were invoked on the command line.

Instead, a hybrid of the accepted answer along with other bits and pieces from online yields this alternative:

require 'spec_helper'
require 'rake'

RSpec.describe 'demo:test' do
  before :each do
    Rake.application = Rake::Application.new
    Rake.application.rake_require 'lib/tasks/demo_tasks', [Rails.root.to_s], []
    Rake::Task.define_task(:environment)
  end

  it 'runs' do
    expect(SomethingWeAreInvoking).to eql(ProofIfWasInvoked)
    Rake.application.invoke_task('demo.test')
  end

  it 'runs with a parameter' do
    expect(SomethingWeAreInvoking).to eql(ProofIfWasInvoked)
    Rake.application.invoke_task('demo.test[42]')
  end
end
  • It prepares Rake on before :each.
  • Each time, a new Rake.application is created; this means we can use invoke in any number of tests, albeit only once in any single test.
  • If we just used the Rake.application out-of-the-box instance, we could just write Rake.application.rake_require 'tasks/demo_tasks' as all the paths etc. are set up, but since we definitely need a new Rake application instance for each test to avoid "dirtying" its state across tests, the "longhand" form from @dave-newtown is needed.
  • I use Rake.application.invoke_task instead of Rake::Task[...].invoke. This puts the syntax for arguments on parity with that used on the Rake command line, which I feel is a more 'accurate' and natural way to test tasks that take args.

Yes, this does mean that at least under Rake v12 as tested, a lot of online articles showing how to test Rake tasks are incorrect, or get away with it because they only show a single test for any given task in their examples. It's probable that earlier Rake versions didn't behave this way, so those articles were correct at the time they were written.

Hope someone finds this helpful.

Reference articles:

(search engine hints: testing rake rspec test invoke invoked run only once)

Andrew Hodgkinson
  • 4,379
  • 3
  • 33
  • 43
  • 1
    Omg, I was doubting myself thinking all those online articles where falsey. Even found an article with two test examples, one being `expect(...).not_to have_received(...)` which obviously makes sense if the rake tasks is not executed at all. – HJW Jan 10 '23 at 13:32
1

Rake keeps track of which tasks have been run and assumes you only want to run them once. A simple solution is to call #reenable on the task in a before(:each) hook before invoking the task. Say for example that the rake task is supposed to call two methods on SomeClass and you want to test these in separate examples:

RSpec.describe Rake::Task['my_task'] do
  before do
    allow(SomeClass).to receive(:method1)
    allow(SomeClass).to receive(:method2)

    described_class.reenable
    described_class.invoke
  end

  it 'does a thing' do
    expect(SomeClass).to have_received(:method1)
  end

  it 'does another thing' do
    expect(SomeClass).to have_received(:method2)
  end
end
Jimbali
  • 2,065
  • 1
  • 20
  • 24