Defining custom RSpec matchers

Updated . Posted . Visible to the public. Repeats.

There are three ways to define your own RSpec matchers, with increasing complexibility and options:

1) Use RSpec::Matchers.define

RSpec::Matchers.define :be_a_multiple_of do |expected|
  match do |actual|
    actual % expected == 0
  end
  
  # optional
  failure_message do |actual|
    "expected that #{actual} would be a multiple of #{expected}"
  end
  
  # optional
  failure_message_when_negated do |actual|
    "expected that #{actual} would not be a multiple of #{expected}"
  end
end

Chaining / fluent interfaces

Note that you can also make your matcher chainable, so a test can modifier its behavior. Using chaining you can write a matcher like this:

expect(x).to be_a_multiple_of(5).but_greater_than(2)

For more details see define matcher with a fluent interface Show archive.org snapshot from the RSpec docs.

block expectations

If you want to use a block expectation like expect { rand(100) }.to produce_different_results, you have to use this syntax and call supports_block_expectations:

RSpec::Matchers.define :produce_different_results do |sample_size: 10|
  supports_block_expectations
  
  match do |actual_proc|
    raise ArgumentError, 'Expected a proc' unless actual_proc.is_a?(Proc)

    results = Set.new
    sample_size.times do
      current_result = actual_proc.call
      results.add?(current_result)
    end
 
    results.length == sample_size
  end
end

2) Use matcher

To scope matchers to one or more example groups you can use the matcher method.

matcher :be_just_like do |expected|
  match {|actual| actual == expected}
end

3) Use simple_matcher (deprecated)

module RhymeWithMatcher
  def rhyme_with(expected)
    simple_matcher do |given, matcher|
      matcher.description = "rhyme with #{expected.inspect}"
      matcher.failure_message = "expected #{given.inspect} to rhyme with #{expected.inspect}"
      matcher.negative_failure_message = "expected #{given.inspect} not to rhyme with #{expected.inspect}"
      given.rhymes_with? expected
    end
  end
end

ActiveSupport::TestCase.send :include, RhymeWithMatcher

4) Write a matcher class (for complex matchers)

module Aegis
  module Matchers

    class CheckPermissions

      def initialize(expected_resource, expected_options = {})
        @expected_resource = expected_resource
        @expected_options = expected_options
      end

      def matches?(controller)
        @controller_class = controller.class
        @actual_resource = @controller_class.instance_variable_get('@aegis_permissions_resource')
        @actual_options = @controller_class.instance_variable_get('@aegis_permissions_options')
        @actual_resource == @expected_resource && @actual_options == @expected_options
      end

      def failure_message
        if @actual_resource != @expected_resource
          "expected #{@controller_class} to check permissions against resource #{@expected_resource.inspect}, but it checked against #{@actual_resource.inspect}"
        else
          "expected #{@controller_class} to check permissions with options #{@expected_options.inspect}, but options were #{@actual_options.inspect}"
        end
      end

      def negative_failure_message
        if @actual_resource == @expected_resource
          "expected #{@controller_class} to not check permissions against resource #{@expected_resource.inspect}"
        else
          "expected #{@controller_class} to not check permissions with options #{@expected_options.inspect}"
        end
      end

      def description
        description = "check permissions against resource #{@expected_resource.inspect}"
        description << " with options #{@expected_options.inspect}" if @expected_options.any?
        description
      end

    end

    def check_permissions(*args)
      CheckPermissions.new(*args)
    end

  end
end

Register the matcher class with RSpec like this:

RSpec.configure do |config|
 config.include Aegis::Matchers
end 

In very old versions of Rails and RSpec you need to do this instead:

ActiveSupport::TestCase.send :include, Aegis::Matchers
Profile picture of Henning Koch
Henning Koch
Last edit
Daniel Straßner
Keywords
writing, write
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2010-09-07 08:21)