Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

match matcher not invoked correctly from custom matchers #188

Closed
hgmnz opened this issue Nov 12, 2012 · 3 comments
Closed

match matcher not invoked correctly from custom matchers #188

hgmnz opened this issue Nov 12, 2012 · 3 comments

Comments

@hgmnz
Copy link

hgmnz commented Nov 12, 2012

When the match matcher is used within a custom matcher, it is invoking the wrong method.

A simple case to reproduce:

require 'rspec'                                                                                                                                               

Rspec::Matchers.define :match_string do                                                                                                                       
  match do |m|                                                                                                                                                
    expect(m).to match(/in it/)                                                                                                                               
  end                                                                                                                                                         
end                                                                                                                                                           

describe 'Example' do                                                                                                                                         
  let(:string) { "has a string in it" }                                                                                                                       
  it 'matches a string with custom matcher' do                                                                                                                
    expect(string).to match_string                                                                                                                            
  end                                                                                                                                                         

  it 'matches a string without custom matcher' do                                                                                                             
    expect(string).to match(/in it/)                                                                                                                          
  end                                                                                                                                                         
end 

which when run, gives:

rspec matcher_issue.rb
# snip
F.

Failures:

  1) Example matches a string with custom matcher
     Failure/Error: expect(m).to match(/in it/)
     ArgumentError:
       wrong number of arguments (1 for 0)
     # ./matcher_issue.rb:5:in `block (2 levels) in <top (required)>'
     # ./matcher_issue.rb:12:in `block (2 levels) in <top (required)>'

Finished in 0.00094 seconds
2 examples, 1 failure

Failed examples:

rspec ./matcher_issue.rb:11 # Example matches a string with custom matcher

This issue was originally reported at rspec/rspec-core#725

@stevenharman
Copy link
Contributor

Analysis

RSpec::Matchers.define :match_string do
  # block A
  match do |m|
    # block B
    expect(m).to match(/in it/)
  end
end

When defining a custom matcher via RSpec::Matchers.define block, you are actually defining new matchers to be added to the RSpec::Matchers module. The new matchers are added to that module (via define_method) using the name given (:match_string in your case).

When you call those matchers in your spec (i.e. expect(string).to match_string) they take the block passed to .define (block A) and execute it within a RSpec::Matchers::DSL::Matcher instance (via :instance_exec). Instances of that class define a protocol you need to implement via the #match, #failure_message_for_should, etc... methods.

So when executing the :match_string matcher, it executes the block given to its match method (block B) to determine success or failure.

In your example, block B itself calls a method #match. When Ruby tries to dispatch that method it starts looking up it's ancestor tree and immediately finds a method with the name match in RSpec::Matchers::DSL::Matchers class, and uses it. However, you were trying to get to the RSpec::Matchers#match method.

If we were to inspect the ancestor tree right before the expect(m).to match(/in it/), we'd see this:

$ method(:match).source_location
=> ["/Users/steven/code/rspec-dev/repos/rspec-expectations/lib/rspec/matchers/matcher.rb",

$ self.class.ancestors
=> [RSpec::Matchers::DSL::Matcher,
 RSpec::Matchers,
 RSpec::Matchers::Pretty,
 RSpec::Matchers::Extensions::InstanceEvalWithArgs,
 Object,
 PP::ObjectMixin,
 Kernel,
 BasicObject,
 RSpec::Mocks::Methods]

Possible solution

We could alias_method :match_regex :match within the RSpec::Matchers module. Then your custom matcher could use match_regex(/in it/) which would not exist in the RSpec::Matchers::DSL::Matcher class, so Ruby would eventually find it in the RSpec::Matchers module.

stevenharman added a commit to stevenharman/rspec-expectations that referenced this issue Dec 6, 2012
Addresses problem with dispatching the #match matcher from within a
custom matcher. (Wow... what a mouthful) See Issue rspec#188.
@myronmarston
Copy link
Member

@hgmnz -- give @stevenharman's fix (now in master) a try. It should solve your problem.

@hgmnz
Copy link
Author

hgmnz commented Dec 8, 2012

@myronmarston yup, this works for me. Like the simple solution, too. Thanks @stevenharman!

eloyesp pushed a commit to eloyesp/rspec-expectations that referenced this issue Nov 5, 2013
Rather than evaling the `define` block in the
context of the matcher instance, eval the `define`
block in the context of the matcher instance's
singleton class.

* Fixes rspec#272.
  `include` in `define` has a different meaning (module inclusion)
  than `include` in the `match` block (using the `include` matcher to
  match).
* Better solution than rspec#194
  for rspec#188. There's now
  a `match` class method and a `match` instance method.
* Completely avoids issues we had to use hacks to solve before:
  rspec#29,
  rspec#38,
  rspec@fc4b66d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants