Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Test your Rake tasks!

There are several reasons why you should test your Rake tasks:

  • Rake tasks are code and as such deserve testing.
  • When untested Rake tasks have a tendency to become overly long and convoluted. Tests will help keep them in bay.
  • As Rake tasks typically depend on your models, you (should) loose confidence in them if you don’t have tests and are attempting refactorings.

A problematic Rake task test

Here is a Rake file…

File: lib/tasks/bar_problematic.rake

namespace :foo do
  desc "bake some bars"
  task bake_a_problematic_bar: :environment do
    puts '*' * 60
    puts ' Step back: baking in action!'
    puts '*' * 60

    puts Bar.new.bake

    puts '*' * 60
    puts ' All done. Thank you for your patience.'
    puts '*' * 60
  end
end

…and its too simplistic spec:

File: spec/tasks/bar_rake_problematic_spec.rb
require 'spec_helper'
require 'rake'

describe 'foo namespace rake task' do
  describe 'foo:bake_a_problematic_bar' do

    before do
      load File.expand_path("../../../lib/tasks/bar_problematic.rake", __FILE__)
      Rake::Task.define_task(:environment)
    end

    it "should bake a bar" do
      Bar.any_instance.should_receive :bake
      Rake::Task["foo:bake_a_problematic_bar"].invoke
    end

    it "should bake a bar again" do
      Bar.any_instance.should_receive :bake
      Rake::Task["foo:bake_a_problematic_bar"].invoke
    end
  end
end

Some notable aspects of testing Rake tasks:

  • Rake has to be required.
  • The Rake file under test has to be manually loaded.
  • In this example, the Rake task depends on the environment task, which is not automatically available in a spec. Since we are in rspec, the environment is already loaded and we can just define environment as an empty Rake task to make the bake task run in the test.

When run, this spec fails on the second it block… and that is not the only problem with this spec and the Rake task:

  • The Rake task duplicates code to output information to the user.
  • The spec “should bake a bar” will output that information when run, which clobbers the spec runners output.
  • The spec “should bake a bar” again will fail, because Rake tasks are built to only execute once per process. See rake.rb. This makes sense for the normal use of Rake tasks where a task may be named as the prerequisite of another task multiple times through multiple dependencies it might have – the task only needs to run once. In our tests we have to reenable the task.

A better Rake task test

A new version of the above Rake file…

File: lib/tasks/bar.rake
class BarOutput
  def self.banner text
    puts '*' * 60
    puts " #{text}"
    puts '*' * 60
  end

  def self.puts string
    puts string
  end
end

namespace :foo do
  desc "bake some bars"
  task bake_a_bar: :environment do
    BarOutput.banner " Step back: baking in action!"
    BarOutput.puts Bar.new.bake
    BarOutput.banner " All done. Thank you for your patience."
  end
end

… and its spec:

File: spec/tasks/bar_rake_spec.rb
require 'spec_helper'
require 'rake'

describe 'foo namespace rake task' do
  before :all do
    Rake.application.rake_require "tasks/bar"
    Rake::Task.define_task(:environment)
  end

  describe 'foo:bar' do
    before do
      BarOutput.stub(:banner)
      BarOutput.stub(:puts)
    end

    let :run_rake_task do
      Rake::Task["foo:bake_a_bar"].reenable
      Rake.application.invoke_task "foo:bake_a_bar"
    end

    it "should bake a bar" do
      Bar.any_instance.should_receive :bake
      run_rake_task
    end

    it "should bake a bar again" do
      Bar.any_instance.should_receive :bake
      run_rake_task
    end

    it "should output two banners" do
      BarOutput.should_receive(:banner).twice
      run_rake_task
    end

  end
end

This spec passes just fine and does not clobber the spec output. Again, let’s look at noteworthy things:

  • The output of the Rake task now goes through the BarOutput class. This reduces code duplication and allows for easy stubbing. There are other ways to achieve a similar effect and not clobber test output: Stub puts and print, stub on $stdout.
  • Rake.application has a nicer way of requiring Rake files than a simple load, because rake_require knows where Rake files live.
  • Rake::Task["TASK"].reenable reenables the task with name “TASK” so that it will be run again and can be called multiple times in a spec.

Here is the gist: https://gist.github.com/1764423

Comments
  1. Thanks for the informative post.

    It’s worth noting that the friction associated with testing Rake tasks are related to one of the big advantages I see in Thor (https://github.com/wycats/thor) over Rake. With Thor, tasks are defined in Ruby classes, and it’s straightforward to test them.

    -Bryan

  2. bryanl says:

    Many people don’t test their rake tasks, because they shove a whole bunch of stuff in them, and that makes it too unwieldy.

    -A different Bryan

  3. Stephan Hagemann says:

    @bryanl Test-driving Rake tasks will prevent them from becoming overloaded and unwieldy!

  4. Steve C says:

    You could just keep your rake tasks really simple and not write tests.

    This seems like a case where testing allows you to take on more complexity in the wrong place where otherwise you might feel pressure to simplify.

  5. Aghyad says:

    That’s a good post on how to write some complicated tests plugged into your rake tasks. Nothing is wrong with that. But it’s more like getting a helicopter to travel two blocks in Manhattan.

    I’ll explain:

    1 – Rake tasks are not meant to be a complicated piece of your website. Instead, rake tasks are meant to be “thin” and only to invoke other modules in your system. Those modules do the heavy work. Those modules are what really matters. So, test the meaningful code and not the code that invokes it. Well, if you are still feeling unconfident about the invoking processes … then:

    2 – Trust rails and trust the whole framework. Don’t bother testing whether this rake task is going to print something minor. It will work. Otherwise, you have a bigger issue that missing tests in the framework.

    In summary: Focus your energy more on testing the functionality that this rake task is written to invoke, which is a class or a method some where else in your models or lib files or any where else. Otherwise, your website will start killing kittens and choking birds for no reason! – not that there’s a reason anyway to kill or choke kittens and birds :)

  6. Stephan Hagemann says:

    @Steve, @Aghyad:

    Thanks for your comments.

    Rake task testing is almost always integration-type testing.

    I absolutely agree that all rake tasks should be kept simple! However, I highly recommend testing them to ensure that what your are trying to achieve is actually achieved.

    As to, “So, test the meaningful code and not the code that invokes it.”: if you are interesting in ensuring that your invoking code works: test it. If you are not: delete it – that is the simplest implementation of something not working.

    Imagine a rake task that gets run on a server to clean up a logging table in the database. And imagine you rename that model. Without a test you will potentially notice that your rake task is broken when your production database runs out of space because the cleanup task never succeed in the last couple of months…

    Regarding testing the output of a rake task: if it is a hard product requirement that a certain rake task output something specific, I recommend testing that in a couple of ways. In the model/module/service test all the specifics of this output. In the rake task test do a happy path test to verify that something gets output.

  7. […] Test your Rake tasks! (Stephan Hagemann, Pivotal Labs) […]

  8. Calin says:

    `BarOutput.puts` will throw a ‘stack too deep’ error of course. Simply replace the body of the method with `Kernel::puts`

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *