Logo Devoh

A Custom RSpec Example Group for Testing Rake Tasks

As I continue to strive to be better at test-driven development (BDD for you pursists), I'm continually surprised by how much friction there is within the testing frameworks for testing some of the dustier corners of the Rails stack. One of these overlooked parts is Rake tasks. While it could be argued that they aren't as important to test as customer-facing pages, Rake tasks can do a lot of the heavy lifting in some applications, and merit testing to ensure that the critical back-end functionality is behaving as you would expect.

Since there isn't anything included with RSpec for testing Rake tasks, I took the time earlier this year while working on Top Ruby Jobs to write a custom example group to facilitate such testing.

class RakeExampleGroup < Spec::ExampleGroup
  Spec::Example::ExampleGroupFactory.register(:tasks, self)

  require 'rake'

  set_description "Rake task"

  let(:rake) { Rake::Application.new }
  let(:task) { rake[description_args.first] }
  let(:namespace) { description_args.first.split(':').first }

  before(:each) do
    Rake.application = rake
    Rake.application.rake_require(File.join('lib', 'tasks', namespace))
    Rake::Task.define_task(:environment)
  end

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

Any specs under the /spec/tasks path will use this example group. Here's an example.

require 'spec_helper'

describe 'content:sync' do
  it "has 'environment' as a prerequisite" do
    task.prerequisites.should include('environment')
  end

  it "syncs the remote content" do
    Resque.should_receive(:enqueue).with(SyncContentJob)
    task.invoke
  end
end

This looks for the file /lib/tasks/content.rake, and calling task.invoke will run the task content:sync just like running rake content:sync on the command line would.

Unfortunately, this example group doesn't work in RSpec 2, which has undergone significant API changes. It took me a little while to figure out how example groups work in RSpec 2 in order to get my RakeExampleGroup working there, but here's what I came up with.

module RakeExampleGroup
  extend ActiveSupport::Concern

  included do
    require 'rake'

    metadata[:type] = :task

    let(:rake) { Rake::Application.new }
    let(:task) { rake[self.class.description] }
    let(:namespace) { self.class.description.split(':').first }

    before do
      Rake.application = rake
      Rake.application.rake_require(File.join('lib', 'tasks', namespace))
      Rake::Task.define_task(:environment)
    end

    after do
      Rake.application = nil
    end
  end

  RSpec.configure do |config|
    config.include self, :example_group => { :file_path => %r(spec/tasks) }
  end
end

I'm including both versions here, because I wasn't able to find any advice on how to migrate custom example groups to Rspec 2, so hopefully this proves helpful to someone else. It should be noted, though, that this particular example does depend on rspec-rails, which is fine in my case since I'm working within the context of a Rails application, but the include_self_when_dir_matches method comes from RSpec::Rails::ModuleInclusion, so you'd have handle that magic yourself if you're not using it within Rails.

Updated the RSpec 2 example to work with newer versions thanks to feedback from @jnimety.