Read more

Defining custom RSpec matchers

Henning Koch
September 07, 2010Software engineer at makandra GmbH

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

Illustration web development

Do you need DevOps-experts?

Your development team has a full backlog? No time for infrastructure architecture? Our DevOps team is ready to support you!

  • We build reliable cloud solutions with Infrastructure as code
  • We are experts in security, Linux and databases
  • We support your dev team to perform
Read more Show archive.org snapshot

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
Henning Koch
September 07, 2010Software engineer at makandra GmbH
Posted by Henning Koch to makandra dev (2010-09-07 10:21)