Burt Murphy
Burt Murphy

Reputation: 25

Chaining Rspec Custom Matchers

For testing reasons I've recently moved some of my RSpec matchers to use the class form rather than the DSL. Is there a way to easily get chaining behaviour when they are in this form.

E.g.

class BeInZone
  def initialize(expected)
    @expected = expected
  end
  def matches?(target)
    @target = target
    @target.current_zone.eql?(Zone.new(@expected))
  end
  def failure_message
    "expected #{@target.inspect} to be in Zone #{@expected}"
  end
  def negative_failure_message
    "expected #{@target.inspect} not to be in Zone #{@expected}"
  end
  # chain methods here
end

Many thanks

Upvotes: 2

Views: 1496

Answers (1)

Aaron K
Aaron K

Reputation: 6961

Add a new method with the name of chain, which normally should return self. Typically you save the provided chained state. Which you then update the matches? method to use. This state can also be used in the various output message methods too.

So for your example:

class BeInZone
  # Your code

  def matches?(target)
    @target = target

    matches_zone? && matches_name?
  end

  def with_name(name)
    @target_name = name
    self
  end

  private
  def matches_zone?
    @target.current_zone.eql?(Zone.new(@expected))
  end

  def matches_name?
    true unless @target_name

    @target =~ @target_name
  end
end

Then to use it: expect(zoneA_1).to be_in_zone(zoneA).with_name('1')

The reason this works is that you are building the object that you are passing to either the should or expect(object).to methods. These methods then call matches? on the provided object.

So it's no different than other ruby code like puts "hi there".reverse.upcase.gsub('T', '7'). here the string "hi there" is your matcher and the chained methods are called on it, passing the final object returned from gsub to puts.

The built-in expect change matcher is a good example to review.

Upvotes: 4

Related Questions