expectation matchers

February 18th, 2007

Expectation Matchers

RSpec 0.8 introduces a new approach to setting and verifying expectations called Expectation Matchers.

The idea of a Matcher is not new. It’s just new to RSpec. Some other examples of matchers are argument matchers in dynamic mocking tools like jmock. In fact, RSpec’s own stubbing/mocking framework employs them as well.

Another example is Hamcrest, which is a java project that provides assertion matchers for use with junit. You can see a couple of examples in their tutorial.

How They Work

RSpec’s Expectation Matchers are designed to work in concert with #should and #should_not on Object. These methods do get added to every object, however this is a much lower level of pollution than that which uses method_missing on every object!

should and #should_not each accept either an Expectation Matcher or an expression using a specific subset of Ruby operators. See below for more on expressions using operators.

To be an Expectation Matcher, an object must respond_to #matches?(obj), #failure_message and #negative_failure_message.

When you pass an Expectation Matcher to #should, #should sends #matches?(self) to the Matcher. If #matches? returns true, the expectation passes. If false, #should then raises an ExpectationNotMetError with the result of #failure_message on the matcher.

should_not works similarly, but raises the ExpectationNotMetError when #matches? returns true. It also uses the result of matcher.negative_failure_expectation.

Here’s a concrete example:

class Equal
  def initialize(expected)
    @expected = expected
  end

def matches?(actual) @actual = actual @actual.equal?(@expected) end

def failure_message return "expected #{@expected.inspect}, got #{@actual.inspect} (using .equal?)", @expected, @actual end

def negative_failure_message return "expected #{@actual.inspect} not to equal #{@expected.inspect} (using .equal?)", @expected, @actual end end

included in specify blocks

module Matchers def equal(expected) Equal.new(expected) end end

in a spec

context "Ruby" do specify "should support simple addition" do (2+2).should equal(4) end end

As you can see, there’s really not much to each Matcher, and the framework is so simple it hardly deserves to be called a framework.

Once we got the initial framework (what else can I call it?) in place, adding each new matcher was quite simple. Of course we toyed w/ hierarchies with default behaviour and message-building objects, etc. In the end, we found that we lost the benefits of keeping the messages DRY because getting them worded correctly in every situation was starting to require some really convoluted centralized code that was tightly bound (conceptually) to the clients it served.

We also wanted to make it easy for users to create custom expectation matchers. Frameworks tend to follow the 80/20 rule – the common 80% of the problem set is made easy, but you’re on your own for the less common 20%. Since custom matchers will fall naturally into the less common 20%, it makes 0% sense to provide a framework for these.

Operator expressions

Here are some examples of expressions using operators:

result.should == 3
result.should be > 7
result.should =~ /some regexp/

My favorite of these is the “should be” collection. It turns out that Ruby converts that to this:

result.should be.>(7)

So it’s the result of #be that gets #> called on it. Because this is part of the language, Ruby doesn’t complain about the lack of parens. Sweet!

2 Responses to “expectation matchers”

  1. Adam Sroka Says:

    I like this. It is similar to the assertThat(Matcher) syntax that has become en vogue in both JMock and NUnit. It seems right to me that the whole Agile community is headed in the direction of literate programming for specifying behavior even though some of the frameworks still have a test-centric view.

  2. David Chelimsky Says:

    Hi Adam,

    <p>As it turns out, yes! Initially I was just calling these expectations. Then Dan North pointed me to <a href="http://jmock.org">jmock</a> and <a href="http://code.google.com/p/hamcrest/">hamcrest</a>. Turns out they share matchers across these projects. When I learned that, I added some glue to permit the same in RSpec, so now all of the expectation matchers can double as mock argument constraints.</p>
    
    
    <p>This does not work well with all of the matcher methods, but it does w/ many of them. I&#8217;m sure that we&#8217;ll improve that situation over time.</p>