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.