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