Feature: Define a custom matcher rspec-expectations provides a DSL for defining custom matchers. These are often useful for expressing expectations in the domain of your application. Behind the scenes `RSpec::Matchers.define` evaluates the `define` block in the context of a singleton class. If you need to write a more complex matcher and would like to use the `Class`-approach yourself, please head over to our `API`-documentation and read [the docs](http://rspec.info/documentation/latest/rspec-expectations/RSpec/Matchers/MatcherProtocol.html) about the `MatcherProtocol`. Scenario: Define a matcher with default messages Given a file named "matcher_with_default_message_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 end end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } end RSpec.describe 9 do it { is_expected.not_to be_a_multiple_of(4) } end # fail intentionally to generate expected output RSpec.describe 9 do it { is_expected.to be_a_multiple_of(4) } end # fail intentionally to generate expected output RSpec.describe 9 do it { is_expected.not_to be_a_multiple_of(3) } end """ When I run `rspec ./matcher_with_default_message_spec.rb --format documentation` Then the exit status should not be 0 And the output should contain "is expected to be a multiple of 3" And the output should contain "is expected not to be a multiple of 4" And the output should contain "Failure/Error: it { is_expected.to be_a_multiple_of(4) }" And the output should contain "Failure/Error: it { is_expected.not_to be_a_multiple_of(3) }" And the output should contain "4 examples, 2 failures" And the output should contain "expected 9 to be a multiple of 4" And the output should contain "expected 9 not to be a multiple of 3" Scenario: Overriding the failure_message Given a file named "matcher_with_failure_message_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 end failure_message do |actual| "expected that #{actual} would be a multiple of #{expected}" end end # fail intentionally to generate expected output RSpec.describe 9 do it { is_expected.to be_a_multiple_of(4) } end """ When I run `rspec ./matcher_with_failure_message_spec.rb` Then the exit status should not be 0 And the stdout should contain "1 example, 1 failure" And the stdout should contain "expected that 9 would be a multiple of 4" Scenario: Overriding the failure_message_when_negated Given a file named "matcher_with_failure_for_message_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 end failure_message_when_negated do |actual| "expected that #{actual} would not be a multiple of #{expected}" end end # fail intentionally to generate expected output RSpec.describe 9 do it { is_expected.not_to be_a_multiple_of(3) } end """ When I run `rspec ./matcher_with_failure_for_message_spec.rb` Then the exit status should not be 0 And the stdout should contain "1 example, 1 failure" And the stdout should contain "expected that 9 would not be a multiple of 3" Scenario: Overriding the description Given a file named "matcher_overriding_description_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 end description do "be multiple of #{expected}" end end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } end RSpec.describe 9 do it { is_expected.not_to be_a_multiple_of(4) } end """ When I run `rspec ./matcher_overriding_description_spec.rb --format documentation` Then the exit status should be 0 And the stdout should contain "2 examples, 0 failures" And the stdout should contain "is expected to be multiple of 3" And the stdout should contain "is expected not to be multiple of 4" Scenario: With no args Given a file named "matcher_with_no_args_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :have_7_fingers do match do |thing| thing.fingers.length == 7 end end class Thing def fingers; (1..7).collect {"finger"}; end end RSpec.describe Thing do it { is_expected.to have_7_fingers } end """ When I run `rspec ./matcher_with_no_args_spec.rb --format documentation` Then the exit status should be 0 And the stdout should contain "1 example, 0 failures" And the stdout should contain "is expected to have 7 fingers" Scenario: With multiple args Given a file named "matcher_with_multiple_args_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_the_sum_of do |a,b,c,d| match do |sum| a + b + c + d == sum end end RSpec.describe 10 do it { is_expected.to be_the_sum_of(1,2,3,4) } end """ When I run `rspec ./matcher_with_multiple_args_spec.rb --format documentation` Then the exit status should be 0 And the stdout should contain "1 example, 0 failures" And the stdout should contain "is expected to be the sum of 1, 2, 3, and 4" Scenario: With a block arg Given a file named "matcher_with_block_arg_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :be_lazily_equal_to do match do |obj| obj == block_arg.call end description { "be lazily equal to #{block_arg.call}" } end RSpec.describe 10 do it { is_expected.to be_lazily_equal_to { 10 } } end """ When I run `rspec ./matcher_with_block_arg_spec.rb --format documentation` Then the exit status should be 0 And the stdout should contain "1 example, 0 failures" And the stdout should contain "is expected to be lazily equal to 10" Scenario: With helper methods Given a file named "matcher_with_internal_helper_spec.rb" with: """ruby require 'rspec/expectations' RSpec::Matchers.define :have_same_elements_as do |sample| match do |actual| similar?(sample, actual) end def similar?(a, b) a.sort == b.sort end end RSpec.describe "these two arrays" do specify "should be similar" do expect([1,2,3]).to have_same_elements_as([2,3,1]) end end """ When I run `rspec ./matcher_with_internal_helper_spec.rb` Then the exit status should be 0 And the stdout should contain "1 example, 0 failures" Scenario: Scoped in a module Given a file named "scoped_matcher_spec.rb" with: """ruby require 'rspec/expectations' module MyHelpers extend RSpec::Matchers::DSL matcher :be_just_like do |expected| match {|actual| actual == expected} end end RSpec.describe "group with MyHelpers" do include MyHelpers it "has access to the defined matcher" do expect(5).to be_just_like(5) end end RSpec.describe "group without MyHelpers" do it "does not have access to the defined matcher" do expect do expect(5).to be_just_like(5) end.to raise_exception end end """ When I run `rspec ./scoped_matcher_spec.rb` Then the stdout should contain "2 examples, 0 failures" Scenario: Scoped in an example group Given a file named "scoped_matcher_spec.rb" with: """ruby require 'rspec/expectations' RSpec.describe "group with matcher" do matcher :be_just_like do |expected| match {|actual| actual == expected} end it "has access to the defined matcher" do expect(5).to be_just_like(5) end describe "nested group" do it "has access to the defined matcher" do expect(5).to be_just_like(5) end end end RSpec.describe "group without matcher" do it "does not have access to the defined matcher" do expect do expect(5).to be_just_like(5) end.to raise_exception end end """ When I run `rspec scoped_matcher_spec.rb` Then the output should contain "3 examples, 0 failures" Scenario: Matcher with separate logic for expect().to and expect().not_to Given a file named "matcher_with_separate_should_not_logic_spec.rb" with: """ruby RSpec::Matchers.define :contain do |*expected| match do |actual| expected.all? { |e| actual.include?(e) } end match_when_negated do |actual| expected.none? { |e| actual.include?(e) } end end RSpec.describe [1, 2, 3] do it { is_expected.to contain(1, 2) } it { is_expected.not_to contain(4, 5, 6) } # deliberate failures it { is_expected.to contain(1, 4) } it { is_expected.not_to contain(1, 4) } end """ When I run `rspec matcher_with_separate_should_not_logic_spec.rb` Then the output should contain all of these: | 4 examples, 2 failures | | expected [1, 2, 3] to contain 1 and 4 | | expected [1, 2, 3] not to contain 1 and 4 | Scenario: Use define_method to create a helper method with access to matcher params Given a file named "define_method_spec.rb" with: """ruby RSpec::Matchers.define :be_a_multiple_of do |expected| define_method :is_multiple? do |actual| actual % expected == 0 end match { |actual| is_multiple?(actual) } end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } it { is_expected.not_to be_a_multiple_of(4) } # deliberate failures it { is_expected.to be_a_multiple_of(2) } it { is_expected.not_to be_a_multiple_of(3) } end """ When I run `rspec define_method_spec.rb` Then the output should contain all of these: | 4 examples, 2 failures | | expected 9 to be a multiple of 2 | | expected 9 not to be a multiple of 3 | Scenario: Include a module with helper methods in the matcher Given a file named "include_module_spec.rb" with: """ruby module MatcherHelpers def is_multiple?(actual, expected) actual % expected == 0 end end RSpec::Matchers.define :be_a_multiple_of do |expected| include MatcherHelpers match { |actual| is_multiple?(actual, expected) } end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } it { is_expected.not_to be_a_multiple_of(4) } # deliberate failures it { is_expected.to be_a_multiple_of(2) } it { is_expected.not_to be_a_multiple_of(3) } end """ When I run `rspec include_module_spec.rb` Then the output should contain all of these: | 4 examples, 2 failures | | expected 9 to be a multiple of 2 | | expected 9 not to be a multiple of 3 | Scenario: Using values_match? to compare values and/or compound matchers. Given a file named "compare_values_spec.rb" with: """ruby RSpec::Matchers.define :have_content do |expected| match do |actual| # The order of arguments is important for `values_match?`, e.g. # especially if your matcher should handle `Regexp`-objects # (`/regex/`): First comes the `expected` value, second the `actual` # one. values_match? expected, actual end end RSpec.describe 'a' do it { is_expected.to have_content 'a' } end RSpec.describe 'a' do it { is_expected.to have_content /a/ } end RSpec.describe 'a' do it { is_expected.to have_content a_string_starting_with('a') } end """ When I run `rspec ./compare_values_spec.rb --format documentation` Then the exit status should be 0 Scenario: Error handling Make sure your matcher returns either `true` or `false`. Take care to handle exceptions appropriately in your matcher, e.g. most cases you might want your matcher to return `false` if an exception - e.g. ArgumentError - occures, but there might be edge cases where you want to pass the exception to the user. You should handle each `StandardError` with care! Do not handle them all in one. ```ruby match do |actual| begin '[...] Some code' rescue ArgumentError false end end ``` Given a file named "error_handling_spec.rb" with: """ruby class CustomClass; end RSpec::Matchers.define :is_lower_than do |expected| match do |actual| begin actual < expected rescue ArgumentError false end end end RSpec.describe 1 do it { is_expected.to is_lower_than 2 } end RSpec.describe 1 do it { is_expected.not_to is_lower_than 'a' } end RSpec.describe CustomClass do it { expect { is_expected.not_to is_lower_than 2 }.to raise_error NoMethodError } end """ When I run `rspec ./error_handling_spec.rb --format documentation` Then the exit status should be 0 Scenario: Define aliases for your matcher If you want your matcher to be readable in different contexts, you can use the `.alias_matcher`-method to provide an alias for your matcher. Given a file named "alias_spec.rb" with: """ruby RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual % expected == 0 end end RSpec::Matchers.alias_matcher :be_n_of , :be_a_multiple_of RSpec.describe 9 do it { is_expected.to be_n_of(3) } end """ When I run `rspec ./alias_spec.rb --format documentation` Then the exit status should be 0 Scenario: With expectation errors that bubble up By default the match block will swallow expectation errors (e.g. caused by using an expectation such as `expect(1).to eq 2`), if you wish to allow these to bubble up, pass in the option `:notify_expectation_failures => true`. Given a file named "bubbling_expectation_errors_spec.rb" with: """ruby RSpec::Matchers.define :be_a_palindrome do match(:notify_expectation_failures => true) do |actual| expect(actual).to be_a(String) expect(actual.reverse).to eq(actual) end end RSpec.describe "a custom matcher that bubbles up expectation errors" do it "bubbles expectation errors" do expect("carriage").to be_a_palindrome end end """ When I run `rspec bubbling_expectation_errors_spec.rb` Then the output should contain all of these: | Failures: | | 1) a custom matcher that bubbles up expectation errors bubbles expectation errors | | Failure/Error: expect(actual.reverse).to eq(actual) | | expected: "carriage" | | got: "egairrac" | | (compared using ==) | | # ./bubbling_expectation_errors_spec.rb:4 | | # ./bubbling_expectation_errors_spec.rb:10 |