GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Writing matchers for shoulda

Shoulda provides assertions that allow developers to quickly test common Rails functionality, such as validations, associations, and controller responses. These assertions have traditionally been packaged as “macros” - class methods that generate test methods. For a while now, we’ve been moving towards matchers internally to provide that functionality. This allowed RSpec and Test::Unit users to access the same test assertions with only a thin wrapper. This past Spring, we cut out most of the wrapper code by eliminating the “macro” concept and instead having Test::Unit users access the matchers directly.

We believe it’s a waste for users to write the same test assertions twice (once for RSpec and once for Test::Unit), and now that shoulda supports them directly, matchers can be the common building blocks for test assertions in both frameworks.

One question we’ve frequently seen since switching to matchers is, “how do I write my own custom macros now?” The short answer is, “you don’t: you write a matcher instead.” The long answer is this blog post.

Old school: macros

If you were going to write a shouldbeinstance_of macro in the old style, you might do it like this:

def self.should_be_instance_of(class_name)
  should "be an instance of #{class_name}" do
    assert_equal class_name, subject.class.name
  end
end

This macro could be used like:

should_be_instance_of 'User'

This is pretty simple and legible, but it has a few flaws:

  • The assertion logic is stuck inside the should block, so it isn’t easy to reuse in RSpec or in other assertions. Creating macro methods like this divides the Test::Unit landscape by making some assertions instance-level assertions and others class-level “macros.”
  • The test name is also stuck in the macro, so you can’t write your own more meaningful test name. This leads to test names with a “generated” feeling that don’t adequately describe the intent of the test.
  • You’d have to write another macro that’s essentially the same if you want the negative case (shouldnotbeinstanceof)
  • Although this is a simple case, more complex assertions can be more than a hundred lines long. In these cases, you’d want to break them up into multiple methods, which is difficult to do in one-off test method generators.
  • Macros are difficult to test, because they generate test methods and can’t be invoked separately

What’s a matcher?

The common matcher API shared between RSpec and shoulda involves creating a Ruby object that responds to four methods:

  • matches?: returns true if the subject matches the expectations for this matcher
  • failure message: should uses this message if matches? returns false
  • negativefailuremessage: should_not uses this message if matches? returns true
  • description: used if a test name is generated from this matcher

The matcher API itself is not a dependency - it’s just a specification. All you need to write a matcher is Ruby.

To write the same macro as a matcher, you might use this code:

class InstanceOfMatcher
  def initialize(class_name)
    @expected_class_name = class_name
  end

  def matches?(subject)
    @actual_class_name = subject.class.name
    @actual_class_name == @expected_class_name
  end

  def failure_message
    "Expected an instance of #{@expected_class_name}, \
     but got an instance of #{@actual_class_name}"
  end

  def negative_failure_message
    "Didn't expect an instance of #{@expected_class_name}, \
     but got one anyway"
  end

  def description
    "should be an instance of #{@expected_class_name}"
  end
end

def be_instance_of(class_name)
  InstanceOfMatcher.new(class_name)
end

This matcher could be used like:

# Test::Unit with Shoulda
should be_instance_of('User')
should_not be_instance_of('Post')

# RSpec
it { should be_instance_of('User') }
it { should_not be_instance_of('Post') }

The matcher has the following benefits over the macro:

  • It’s usable from Test::Unit both at the class and instance level
  • It’s usable from RSpec
  • It’s a separate class, so breaking out into separate methods (or even more classes) is clean and cheap.
  • Although this is a simple case, it allows you to expand into private methods and other classes if necessary
  • You get the negative case without as much duplication
  • Because the matcher doesn’t generate a test, you can write your own test name if you’d like
  • Matchers are easier to test, because they’re just Ruby objects

Why the long face?

One thing you’ll immediately notice is that the matcher for this example is undeniably longer. Part of that is because an RSpec matcher has more infrastructure, so a simple matcher is always longer than a macro. However, I find that most cases are not so simple - otherwise, I wouldn’t need to write a reusable matcher for them.

If you’re willing to introduce a dependency on rspec-expectations, you can use the matcher DSL, which is considerably less verbose for simple cases: https://github.com/dchelimsky/rspec/wiki/Custom-Matchers

Using the matcher DSL, the above example could be written as:

RSpec::Matchers.define :be_instance_of do |expected_class_name|
  match do |subject|
    actual_class_name = subject.class.name
    actual_class_name.should == expected_class_name
  end
end

However, if you want to write matchers that are compatible with both RSpec and shoulda without adding any dependencies, you’ll have to write out the full class. In order to have as few dependencies as possible, all of shoulda’s built-in matchers are written as concrete classes and methods.

Another thing that should be said is that, although matchers tend to use more lines than an equivalent assertion or macro, they’re frequently less terse, so they can be easier to read and write despite their length.

Learn by example

You can find more examples by looking at shoulda itself: https://github.com/thoughtbot/shoulda

The shoulda matchers exercise the matcher concept in just about every way possible, and there are dozens of examples available. If you have specific questions about writing or using matchers, you can always ask on the shoulda mailing list: http://groups.google.com/group/shoulda

If you have existing macros that generate test methods using “should,” they’ll continue to work using the shoulda context framework. Nobody will stop you from writing more, but we highly recommend giving matchers a shot - they’re more flexible, more reusable, and will open your assertions to a wider audience.