Module: RR::WildcardMatchers
- Defined in:
- lib/rr/wildcard_matchers.rb,
lib/rr/wildcard_matchers/is_a.rb,
lib/rr/wildcard_matchers/boolean.rb,
lib/rr/wildcard_matchers/numeric.rb,
lib/rr/wildcard_matchers/satisfy.rb,
lib/rr/wildcard_matchers/anything.rb,
lib/rr/wildcard_matchers/duck_type.rb,
lib/rr/wildcard_matchers/hash_including.rb
Overview
Writing your own custom wildcard matchers.
Writing new wildcard matchers is not too difficult. If you’ve ever written a custom expectation in RSpec, the implementation is very similar.
As an example, let’s say that you want a matcher that will match any number divisible by a certain integer. In use, it might look like this:
# Will pass if BananaGrabber#bunch_bananas is called with an integer
# divisible by 5.
mock(BananaGrabber).bunch_bananas(divisible_by(5))
To implement this, we need a class RR::WildcardMatchers::DivisibleBy with these instance methods:
-
(other)
-
eql?(other) (usually aliased to #==)
-
inspect
-
wildcard_match?(other)
and optionally, a sensible initialize method. Let’s look at each of these.
.initialize
Most custom wildcard matchers will want to define initialize to store some information about just what should be matched. DivisibleBy#initialize might look like this:
class RR::WildcardMatchers::DivisibleBy
def initialize(divisor)
@expected_divisor = divisor
end
end
#==(other)
DivisibleBy#==(other) should return true if other is a wildcard matcher that matches the same things as self, so a natural way to write DivisibleBy#== is:
class RR::WildcardMatchers::DivisibleBy
def ==(other)
# Ensure that other is actually a DivisibleBy
return false unless other.is_a?(self.class)
# Does other expect to match the same divisor we do?
self.expected_divisor = other.expected_divisor
end
end
Note that this implementation of #== assumes that we’ve also declared
attr_reader :expected_divisor
#inspect
Technically we don’t have to declare DivisibleBy#inspect, since inspect is defined for every object already. But putting a helpful message in inspect will make test failures much clearer, and it only takes about two seconds to write it, so let’s be nice and do so:
class RR::WildcardMatchers::DivisibleBy
def inspect
"integer divisible by #{expected.divisor}"
end
end
Now if we run the example from above:
mock(BananaGrabber).bunch_bananas(divisible_by(5))
and it fails, we get a helpful message saying
bunch_bananas(integer divisible by 5)
Called 0 times.
Expected 1 times.
#wildcard_matches?(other)
wildcard_matches? is the method that actually checks the argument against the expectation. It should return true if other is considered to match, false otherwise. In the case of DivisibleBy, wildcard_matches? reads:
class RR::WildcardMatchers::DivisibleBy
def wildcard_matches?(other)
# If other isn't a number, how can it be divisible by anything?
return false unless other.is_a?(Numeric)
# If other is in fact divisible by expected_divisor, then
# other modulo expected_divisor should be 0.
other % expected_divisor == 0
end
end
A finishing touch: wrapping it neatly
We could stop here if we were willing to resign ourselves to using DivisibleBy this way:
mock(BananaGrabber).bunch_bananas(DivisibleBy.new(5))
But that’s less expressive than the original:
mock(BananaGrabber).bunch_bananas(divisible_by(5))
To be able to use the convenient divisible_by matcher rather than the uglier DivisibleBy.new version, re-open the module RR::DSL and define divisible_by there as a simple wrapper around DivisibleBy.new:
module RR::DSL
def divisible_by(expected_divisor)
RR::WildcardMatchers::DivisibleBy.new(expected_divisor)
end
end
Recap
Here’s all the code for DivisibleBy in one place for easy reference:
class RR::WildcardMatchers::DivisibleBy
def initialize(divisor)
@expected_divisor = divisor
end
def ==(other)
# Ensure that other is actually a DivisibleBy
return false unless other.is_a?(self.class)
# Does other expect to match the same divisor we do?
self.expected_divisor = other.expected_divisor
end
def inspect
"integer divisible by #{expected.divisor}"
end
def wildcard_matches?(other)
# If other isn't a number, how can it be divisible by anything?
return false unless other.is_a?(Numeric)
# If other is in fact divisible by expected_divisor, then
# other modulo expected_divisor should be 0.
other % expected_divisor == 0
end
end
module RR::DSL
def divisible_by(expected_divisor)
RR::WildcardMatchers::DivisibleBy.new(expected_divisor)
end
end
Defined Under Namespace
Classes: Anything, Boolean, DuckType, HashIncluding, IsA, Numeric, Satisfy