14
votes

I'm new to Ruby, and I've been trying to learn Rake, RSpec, and Cucumber. I found some code that will help me test my Rake tasks, but I'm having trouble getting it to work. I was told here: http://blog.codahale.com/2007/12/20/rake-vs-rspec-fight/ to drop this:

def describe_rake_task(task_name, filename, &block)
  require "rake"

  describe "Rake task #{task_name}" do
    attr_reader :task

    before(:all) do
      @rake = Rake::Application.new
      Rake.application = @rake
      load filename
      @task = Rake::Task[task_name]
    end

    after(:all) do
      Rake.application = nil
    end

    def invoke!
      for action in task.instance_eval { @actions }
        instance_eval(&action)
      end
    end

    instance_eval(&block)
  end
end

into my spec_helper.rb file.

I've managed to take this code out and run it in my cucumber steps like this:

When /^I run the update_installers task$/ do
 @rake = Rake::Application.new
 Rake.application = @rake
 load "lib/tasks/rakefile.rb"
 @task = Rake::Task["update_installers"]

 for action in @task.instance_eval { @actions }
  instance_eval(&action)
 end

 instance_eval(&block)

 Rake.application = nil
end

but when I try to get things working in rspec, I get the following error.

ArgumentError in 'Rake task install_grapevine should install to the mygrapevine directory'

wrong number of arguments (1 for 2) /spec/spec_helper.rb: 21:in instance_eval' /spec/spec_helper.rb: 21:inblock in invoke!' /spec/spec_helper.rb: 20:in each' /spec/spec_helper.rb: 20:ininvoke!' /spec/tasks/rakefile_spec.rb:12:in `block (2 levels) in '

Unfortunately, I've got just under a week of ruby under by belt, so the metaprogramming stuff is over my head. Could anyone point me in the right direction?

4

4 Answers

19
votes

This works for me: (Rails3/ Ruby 1.9.2)

When /^the system does it's automated tasks$/ do    
  require "rake"
  @rake = Rake::Application.new
  Rake.application = @rake
  Rake.application.rake_require "tasks/cron"
  Rake::Task.define_task(:environment)
  @rake['cron'].invoke   
end

Substitute your rake task name here and also note that your require may be "lib/tasks/cron" if you don't have the lib folder in your load path.

I agree that you should only do minimal work in the Rake task and push the rest to models for ease of testing. That being said I think it's important to ensure that the code is ACTUALLY run in my cron tasks during my integration tests so I think very mild testing of the rake tasks is justified.

16
votes

Since testing rake is just too much for me, I tend to move this problem around. Whenever I find myself with a long rake task that I want to test, I create a module/class in lib/ and move all the code from the task there. This leaves the task to a single line of Ruby code, that delegates to something more testable (class, module, you name it). The only thing that remains untested is whether the rake task invokes the right line of code (and passes the right parameters), but I think that is OK.

It might be useful to tell us which is the 21nd line of your spec_helper.rb. But given that the approach you posted digs deep in rake (referring to its instance variables), I would entirely abandon it for what I suggested in the previous paragraph.

5
votes

I've just spent a little while getting cucumber to run a rake task so I thought I'd share my approach. Note: This is using Ruby 2.0.0 and Rake 10.0.4, but I don't think the behaviour has changed since previous versions.

There are two parts to this. The first is easy: with a properly set up instance of Rake::Application then we can access tasks on it by calling #[] (eg rake['data:import']). Once we have a task we can run it by calling #invoke and passing in the arguments (eg rake['data:import'].invoke('path/to/my/file.csv').

The second part is more awkward: properly setting up an instance of Rake::Application to work with. Once we've done require 'rake' we have access to the Rake module. It already has an application instance, available from Rake.application, but it's not yet set up — it doesn't know about any of our rake tasks. It does, however, know where to find our Rakefile, assuming we've used one of the standard file names: rakefile, Rakefile, rakefile.rb or Rakefile.rb.

To load the rakefile we just need to call #load_rakefile on the application, but before we can do that we need to call #handle_options. The call to #handle_options populates options.rakelib with a default value. If options.rakelib is not set then the #load_rakefile method will blow up, as it expects options.rakelib to be enumerable.

Here's the helper I've ended up with:

module RakeHelper
  def run_rake_task(task_name, *args)
    rake_application[task_name].invoke(*args)
  end

  def rake_application
    require 'rake'
    @rake_application ||= Rake.application.tap do |app|
      app.handle_options
      app.load_rakefile
    end
  end
end

World(RakeHelper)

Pop that code into a file in features/support/ and then just use run_rake_task in your steps, eg:

When /^I import data from a CSV$/ do
  run_rake_task 'data:import', 'path/to/my/file.csv'
end
3
votes

The behavior might have changed since the correct answer was posted. I was experiencing problems executing two scenarios that needed to run the same rake task (only one was being executed despite me using .execute instead of .invoke). I thought to share my approach to solve the issue (Rails 4.2.5 and Ruby 2.3.0).

I tagged all the scenarios that require rake with @rake and I defined a hook to setup rake only once.

# hooks.rb
Before('@rake') do |scenario|
  unless $rake
    require 'rake'
    Rake.application.rake_require "tasks/daily_digest"
    # and require other tasks
    Rake::Task.define_task(:environment)
    $rake = Rake::Task
  end
end

(Using a global variable is suggested here: https://github.com/cucumber/cucumber/wiki/Hooks#running-a-before-hook-only-once)

In the step definition I simply called $rake

# step definition
Then(/^the daily digest task is run$/) do
  $rake['collector:daily_digest'].execute
end

Any feedback is welcome.