Skip to content

Instantly share code, notes, and snippets.

@myronmarston
Created June 29, 2013 04:20
Show Gist options
  • Save myronmarston/5889730 to your computer and use it in GitHub Desktop.
Save myronmarston/5889730 to your computer and use it in GitHub Desktop.
Why I prefer RSpec's matchers to xUnit-style assertions

Why I prefer RSpec's matchers to xUnit Assertions

Matchers make the ordering of arguments intuitive

As others have pointed out, it's not intuitive what the order of arguments is for assert_equal(foo, bar). Which is expected? Which is actual? I had to look it up. Compare that to expect(bar).to eq(foo) -- which is expected and which is actual is immediately obvious. assert_equal is one of the simplest assertions. It gets worse when you start dealing with more complicated assertions like assert_in_delta -- now you've got 3 arguments, and the order is non-intuitive. Compare that to: expect(x).to be_within(d).of(y). The delta, actual and expected values are all obvious.

Matchers support automatic negation

With a matcher, you write one matches? method, and you get automatic negation (e.g. expect(x).not_to matcher) for free. With an xUnit style assert_xxx method, you have to write a separate method for it.

Matchers separate the match logic and failure message

xUnit style assertion methods mix the match logic and the failure message in a single method. The method is either a no-op or it raises an error. Matchers are first class objects that separate the match logic (in the form of a matches? method) from the failure message (in the form of a failure_message_for_should method). This separation is useful because it enables composability (see below) and makes it easy for RSpec to provide a default failure message for common cases. For example, with a custom matcher like this:

RSpec::Matchers.define :be_a_multiple_of do |factor|
  match { |value| value % factor == 0 }
end

...RSpec automatically generates a readable failure message based on the matcher name and argument. Here's the output RSpec produces for an expression like expect(5).to be_a_multiple_of(3): expected 5 to be a multiple of 3.

Matchers are composable

Some of RSpec's matchers are composable. (I'd like to improve things where they are all composable, but we're not there yet). You can pass a matcher as an argument to another matcher. For example:

expect {
  record.save
}.to change { record.created_at }.from(nil).to(be_within(1.second).of(Time.now))

Here we're passing the be_within matcher as an argument to the change matcher, to specify that we expected created_at to be changed to a value near Time.now.

Another example:

expect(array).to include(match(/foo/), match(/bar/))

Here we expect an array to include 2 strings, one of which matches the regex /foo/ and another that matches the regex /bar/.

assert_xyz methods cannot easily be composed in the same fashion.

Matchers more easily support variations

Due to the way matchers use a fluent interface, it is easy to support slight varitions without needing a complex options hash. For example, you can use the change matcher in multiple flexible ways:

expect { foo.x }.to change { foo.y }
expect { foo.x }.to change { foo.y }.by(3)
expect { foo.x }.to change { foo.y }.from(10).to(20)

In the first, we are specifing that foo.y should change but we don't care how it changes. In the second, we specify a relative change -- foo.y should increase by 3. In the last case, we are specifying the exact before/after values.

Another example:

expect(x).to be_within(0.1).of(y)
expect(x).to be_within(5).percent_of(y)

be_within(x).percent_of(y) is a relatively recent addition. The nice thing is it fits right in, reads untuitively, and did not require significant refactorings or any API breakages.

Matchers can be passed as arguments to anything

Since matchers are first class objects, they can be used in other contexts. For example, rspec-mocks supports passing a matcher to with when setting a message expectation:

# This is the new message expectation syntax shipping in 2.14.
# It works the same for `should_receive`.
expect(collaborator).to receive(:do_something).with(some_matcher(some_argument))

I've found other uses for matchers as well; for example, on my current project we're using Sequel and for some unit tests I added a test harness that uses Sequel's mock database adapter and a simple stub_query helper method to stub DB queries:

stub_query(include("WHERE x > 3")) do
  [
    { id: 3, other_column: 2 },
    { id: 5, other_column: 1 }
  ]
end

The matcher is used to match the query when it is performed (asserting that the SQL string includes "WHERE x > 3") and the return value of the block is used as the query result.

With xUnit-style assert methods, there's not a similar way (that I can think of) to use them in other contexts.

Conclusion

All that said, there are some downsides to using RSpec's matchers:

  • It creates more objects (expect() creates an object, and the matcher method creates the matcher object), which leads to more garbage for the GC to cleanup.
  • It's an extra layer of abstraction that assert_xyz lacks. assert methods are simpler. RSpec's approach adds cognative load to understanding what's going on.

Overall, I think it largely comes down to personal taste, and what you value the most. Both approaches have their merits. If you like the features rspec-core provides but prefer not to use rspec-expectations, you can configure it to use Minitest's assertions instead:

RSpec.configure do |rspec|
  rspec.expect_with :stdlib
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment