Friday, July 06, 2007

Moving to RSpec

RSpec's been on my list of tools to adopt for a while now. My current project is unlikely to migrate from Test::Unit, and there's no plan for me to migrate off that project, so I decided to switch Nestegg over to use it. It's not the sort of hard-core dive-in that's going to force me to get fluent, but it's a nice first baby step. (I'm mean really baby: the whole gem is only one module.)

Here's what the migration looked like.

I started by running newgem -t rspec fakegem to get a template for my spec directory and Rakefile, then pulled the spec-related stuff that generated into nestegg.

The spec folder had

  • spec_helper.rb, which just does require 'spec',
  • spec.opts, which just hash --colour, and
  • fakegem_spec.rb, which is a do-nothing spec.

The Rakefile had this up top

  require 'spec/rake/spectask'
rescue LoadError
  puts 'To use rspec for testing you must install rspec gem:'
  puts '$ sudo gem install rspec'

and this down below

desc "Run the specs under spec" do |t|
  t.spec_opts = ['--options', "spec/spec.opts"]
  t.spec_files = FileList['spec/**/*_spec.rb']

desc "Default task is to run specs"
task :default => :spec

After a sudo gem install rspec, I was up and running. The default rake target ran my Test::Unit suite, then the do-nothing spec. Sweet.

I moved the do-nothing spec to nestegg/nesting_exception_spec.rb and copied each of my test names over (which was especially effortless since my Test::Unit suite was using RSpec-ish test declarations). I also remembered reading that you could pass a class to the describe method, so I landed with this

describe Nestegg::NestingException do
  it "includes cause in backtrace" do
    violated "Be sure to write your specs"
  it "includes cause's backtrace after cause in backtrace" do
    violated "Be sure to write your specs"
  it "removes duplicated backtrace elements from nesting exception" do
    violated "Be sure to write your specs"
  it "defaults cause to current raised exception ($!)" do
    violated "Be sure to write your specs"

Then I started at the top and copied the body of the first test into the spec. I'd anticipated some pain related to the helper methods that lived in my test, but those turned out to be completely portable. The only thing I had to modify in each test was the assertion. With special thanks to the very helpful Test::Unit Cheat Sheet, here are the before-and-afters.

  • assert_true e.backtrace.include?("cause: StandardError: #{cause.message}")
    e.backtrace.should include("cause: StandardError: #{cause.message}")
  • assert_equal ["cause: StandardError: #{cause.message}", "line_one", "line_two"], e.backtrace[-3..-1]
    e.backtrace[-3..-1].should == ["cause: StandardError: #{cause.message}", "line_one", "line_two"]
  • assert_match(/#{__FILE__}:\d+:in `test_.+'$/, e.backtrace[0])
    assert_equal "cause: StandardError: msg", e.backtrace[1]
    e.backtrace[0..1].should == ["#{__FILE__}:#{line}", "cause: StandardError: msg"]
    Note that on that one I'd done a Regexp match in the Test::Unit version, because I didn't want to put the test method name in the content of the test. Since RSpec apparently doesn't create methods out of examples, the backtrace line I'm interested in is free of any method name. To enable a simple equality check, I saved the line number on which the exception was raised in a temporary variable.
  • assert_equal expected_cause, raised_error.cause
    raised_error.cause.should == expected_cause

See here for the whole shebang. The original test is here. The two files are incredibly similar, no?

Just like that, Nestegg was converted. I was a little sad I hadn't used mocha, since that could have meant switching to RSpec's mocking library. Oh well. That'll give me something to look forward to when I migrate Handoff.


Anonymous said...

Nice series, and a great intro comparison of RSpec to Unit - makes me want to rush out and buy one! Thanks for the explanation and examples.

Aslak Helles√ły said...

FYI: As you point out, RSpec has its own mocking library built-in, but it also supports others, like Mocha, FlexMock and recently (on trunk) RR - pivotal labs' mock library.