Saturday, February 14, 2009

Testing Rake Tasks with RSpec

Not sure if this is the correct way to go about testing rake tasks but it works pretty well. Hit up the comments if there is a better way.

So here is an example rake task I want to test:

[lib/tasks/app.rake]

namespace :app do
  namespace :options do
    desc "refreshes option values"
    task :refresh => :environment do
      options = YAML.load_file("config/options.yml")
      options.each do |attrs|
        option = Option.find_or_initailize_by_name(attrs["name"])
        option.update_attribute("value", attrs["value"])
      end
    end
  end
end

This is just loading in a yml file and refreshing some default values. Simple enough but I want it tested.

Here is what I came up with:

[spec/lib/tasks/app.rake_spec.rb]

require "/path/to/spec_helper"
require "rake"

describe "app rake tasks" do
  before do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require "lib/tasks/app"
    Rake::Task.define_task(:environment)
  end

  describe "rake app:options:refresh" do
    before do
      @task_name = "app:options:refresh"
      YAML.stub!(:load_file).and_return([])
    end
    it "should have 'environment' as a prereq" do
      @rake[@task_name].prerequisites.should include("environment")
    end
    it "should load 'config/options.yml'" do
      YAML.should_receive(:load_file).with("config/options.yml").and_return([])
      @rake[@task_name].invoke
    end
    it "should create or update all records in the config file" do
      YAML.should_receive(:load_file).with("config/options.yml").and_return([
        { "name" => "option one", "value" => 10 },
        { "name" => "option two", "value" => 20 }
      ])
      option_one = mock(Option, :null_object => true)
      option_two = mock(Option, :null_object => true)
      
      Option.should_receive(:find_or_initialize_by_name).with("option one").and_return(option_one)
      Option.should_receive(:find_or_initialize_by_name).with("option two").and_return(option_two)

      option_one.should_receive(:update_attribute).with("value", 10)
      option_two.should_receive(:update_attribute).with("value", 20)
      @rake[@task_name].invoke
    end
  end
end

Let's walk through whats going on here...

@rake = Rake::Application.new
Rake.application = @rake

This is initializing a new rake application object and setting the current Rake.application to it. This is done so that each test is run with a fresh application object.

Rake.application.rake_require "lib/tasks/app"

This is requiring the rake file with the task I will be testing.

Rake::Task.define_task(:environment)

This is essentially stubbing the environment task which loads the rails environment. The rails env is already loaded when you are running your specs so this is unnecesary.

@rake[@task_name].invoke

This is what actually runs the rake task.

The rest of the 'it' blocks are pretty self explanatory, testing that the task has the correct prereqs and then just spec'ing the functionality.

7 comments:

Mih said...

thanks! This is what I was looking for.

nate said...

I've been looking for this for a long time but too lazy to figure it out on my own- thanks a ton!

Anthony Burns said...

When I run my spec, any examples after the first one give me an error "Don't know how to build task"

hedge said...

An alternative is to move the logic/behaviour into some klass.method, spec that class/method in the usual way.... this simpliifes the spec file somewhat.
Then invoke the method in the rake task which becomes one line. HTH?

jim said...

Hi,

Firstly I have to say if you have any control over your post-comment page it could use some love. Your original post is near unreadable here which makes it hard to reference when commenting.

I was going to say, a more realistic but probably less efficient way of doing this is to setup the @rake object with:

@rake = Rake.application
@rake.init
@rake.load_rakefile

The only thing then is if a rake task has already been invoked once it won't run again unless you call:

@rake[@task_name].reenable

or invoke it with

@rake[@task_name].execute

Having done the above myself though, hedge's approach is actually probably the cleanest, though it would discourage you from using rake dependencies to compose complex tasks out of subtasks.

salbertson said...

Anthony, a little late but try this. It worked great for me.

http://stackoverflow.com/questions/1255176/test-rake-tasks/5306013#5306013

dib said...

I had problems runing multiple tests in one describe block until added:
after { @rake[@task_name].clear }