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
- This is automatically available in all your specs when the code is loaded. We have a note on where to put custom RSpec matchers.
- The shown approach can also be used to compose a custom matcher from existing matchers.
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
Posted by Henning Koch to makandra dev (2010-09-07 08:21)