Custom ArgumentMatchers in rSpec
Ok, so you know the stuff you pass into #with when doing stubs and mocks in
rSpec? Those are called ArgumentMatchers. Most of the time you’re writing
code like obj.should_receive(:blah).with('a string'), but you can get fancier
and use rSpec’s built in ArgumentMatchers to do other nice things like
obj.should_receive(:blah).with(hash_including(:key => 'val')), which will
match a call like obj.blah(:foo => 'bar', :key => 'val').
This is neat and all, but rSpec’s ArgumentMatchers don’t always go as far as
you’d like. In particular, there’s a hash_including, but no array_including.
In the rest of this post, I’m going to show you how to write your own
expectations, using array_including as an example. It turns out to be
surprisingly simple.
So first, a word of warning, in rSpec 1.1.12 ArgumentMatchers are called
ArgumentConstraints. Replace all occurrences of ArgumentMatchers below with
ArgumentConstraints, unless you’re on rSpec edge.
With that disclaimer out of the way, open your spec_helper.rb, where we’re
going to add an ArrayIncludingMatcher to the Spec::Mocks::ArgumentMatchers
namespace. We will define an initialize method to take and store the expected
value, and == method to compare against the actual value, and a description
method to print out a handy description when the test fails.
module Spec
module Mocks
module ArgumentMatchers
class ArrayIncludingMatcher
# We'll allow an array of arguments to be passed in, so that you can do
# things like obj.should_receive(:blah).with(array_including('a', 'b'))
def initialize(*expected)
@expected = expected
end
# actual is the array (hopefully) passed to the method by the user.
# We'll check that it includes all the expected values, and return false
# if it doesn't or if we blow up because #include? is not defined.
def ==(actual)
@expected.each do |expected|
return false unless actual.include?(expected)
end
true
rescue NoMethodError => ex
return false
end
def description
"array_including(#{@expected.join(', ')})"
end
end
# array_including is a helpful wrapper that allows us to actually type
# #with(array_including(...)) instead of ArrayIncludingMatcher.new(...)
def array_including(*args)
ArrayIncludingMatcher.new(*args)
end
end
end
end
Note that we also defined an array_including method as the readable wrapper.
For symettry’s sake, we should also define ArrayNotIncludingMatcher, which
I’ve included in the following gist. Feel free to copy this matcher, but I’d
love to see you guys creating your own. Leave links to gists in the comments if
you come up with anything fun!
Update: I had to remove the embedded gist because it was hanging the page when github was down. Check out the link if you can: http://gist.github.com/62943.js.