-
-
Notifications
You must be signed in to change notification settings - Fork 356
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
Feature suggestion: Custom expectation callbacks to default matchers like receive #1230
Comments
Why don't you just override the receive matcher? module ContractExpectation
def receive(*args, &block)
super(*args) do
# custom logic
block.call unless block.nil
end
end
end
RSpec.configuration.include ContractExpectation Or if you want to customise the receive matcher, vendor it into your code base and customise it. All matchers follow the "matcher protocol" and thus you wouldn't really be at risk from any underlying changes, you could probably simplify the matcher for your use case. You could then publish this as your own extension gem/ |
I was going to suggest something similar, but using a decorator based on require 'delegate'
class ReceiveWithContracts < SimpleDelegator
# Put your custom logic here; all method calls will be delegated to the underlying `receive` matcher.
# Just override the methods you need and use `super` to delegate as needed
end
module ReceiveWithContractsExtension
def receive(*args, &block)
ReceiveWithContracts.new(super)
end
end
RSpec.configure do |config|
config.include ReceiveWithContractsExtension
end You can customize it to your hearts content, with no changes needed in RSpec itself. In general decorating the matcher with |
(Whether @JonRowe's approach or mine is better largely depends on your needs, but I believe either would work). |
This is great! I will give these strategies a try. Thanks for all of your hard work on RSpec! |
Great; let us know if you have further questions. Closing. |
I got things in a state that is mostly working, but just wanted to point out some issues I ran into with the above approach when it came to making sure I could use the At the bottom is what I ended up doing. @JonRowe @myronmarston I went with a combination of both of your approaches. I couldn't simply override the existing matcher, because I needed to override other things such as There were some nuances with passing around the blocks correctly, since some of the functions take a block and others don't (e.g. I wasn't able to use the
Maybe there is a way to compose the delegation logic and ensure the proper Lastly, I needed to raise an error in my custom logic for Let me know what you think of this approach! Thank you, require 'delegate'
module ContractuallyReceiveExtension
def receive(*args, &block)
ContractuallyReceive.new(*args, block)
end
end
RSpec.configure do |config|
config.include ContractuallyReceiveExtension
end
require 'delegate'
class ContractuallyReceive < RSpec::Mocks::Matchers::Receive
attr_reader :object, :message, :not_supported_by_custom_matcher
def matches?(subject, &block)
@object = subject
if !not_supported_by_custom_matcher
# Custom logic -- I have to raise here rather than fail in the same way I can fail using a DSL created matcher
end
super(subject, &block)
end
def does_not_match?(subject, &block)
@object = subject
if !not_supported_by_custom_matcher
# Custom logic -- I have to raise here rather than fail in the same way I can fail using a DSL created matcher
end
super(subject, &block)
end
def setup_allowance(subject, &block)
if !not_supported_by_custom_matcher
# Custom logic -- I have to raise here rather than fail in the same way I can fail using a DSL created matcher
end
super(subject, &block)
end
def with(*args, &block)
@params = *args
super(*args, &block)
end
def and_return(*args, &block)
@stubbed_value = args.first
super(*args, &block)
end
def and_wrap_original(*args, &block)
@not_supported_by_custom_matcher = true
super(*args, &block)
end
end |
require 'delegate'
class ReceiveWithContracts < SimpleDelegator
include RSpec::Mocks::Matchers::Matcher
# Put your custom logic here; all method calls will be delegated to the underlying `receive` matcher.
# Just override the methods you need and use `super` to delegate as needed
end That said, the
It's almost identical in terms of the behavior you've able to achieve, but it's more brittle. Consider, for example, that your custom logic and the
So basically, |
Your points are valid -- wrapping and delegating to the existing matcher rather than pretending to be the existing matcher is definitely less brittle. However it appears the rspec matcher calls certain methods, such as Good education regarding matcher's unique behavior. Sounds like there was a bit of challenge getting receive to function as a matcher and still handle all these other requirements. Thanks again, this was all super helpful. |
Thanks everyone for awesome discussion–the only place to get info on how to add some custom logic to existing matchers. Hope that something on this topic will be added to the documentation. In my example I tried to add new expectation customization for additional check on when mocked object can be called. So I want to restrict calling some methods on some objects when database transaction is in progress (using gem isolator to detect it), like this: allow(ExternalApi::GetUser).to receive(:call).outside_of_transaction.and_return({ "Name" => "Vasya" }) I need it because in our tests we're actively mocking calls to our objects wrapping some external APIs instead of mocking raw http requests (e.g. with webmock), so isolator can't detect transaction violations out of the box. Thanks to examples posted here I was able to construct something like this (and it works): # spec/support/matchers/outside_of_transaction.rb
module RSpec
module Mocks
module Matchers
class Receive
def outside_of_transaction
self
end
end
module NotInTransaction
def matches?(subject, *args, &block)
if Isolator.within_transaction?
# We doesn't raise Isolator::UnsafeOperationError because it may be
# rescued somewhere in subject, while MockExpectationError may not.
@error_generator.send(:__raise, <<~MSG.squish)
#{@error_generator.intro} received #{subject.inspect}
while in database transaction, but it is unsafe to call it so.
MSG
end
super
end
end
end
class MessageExpectation
def outside_of_transaction
raise_already_invoked_error_if_necessary(__method__)
extend Matchers::NotInTransaction
self
end
end
end
end Can it be done better? Am I missing something? Thanks! |
The recommended way is to create a custom matcher yourself, not monkey patching ours. See: You can delegate as much or as little to our matchers within your own as you like, you can also inherit from our matchers if you must. In particular the additional "chain" style you are looking for is here: |
So useful output always given when a parsing test fails. Inspired by rspec/rspec-mocks#1230 (comment).
So useful output always given when a parsing test fails. Inspired by rspec/rspec-mocks#1230 (comment).
So useful output always given when a parsing test fails. Inspired by rspec/rspec-mocks#1230 (comment).
So useful output always given when a parsing test fails. Inspired by rspec/rspec-mocks#1230 (comment).
So useful output always given when a parsing test fails. Inspired by rspec/rspec-mocks#1230 (comment).
The links are not working anymore, can you help me to the updated documentation? |
Subject of the issue
I would like to customize the
receive
matcher to add custom behavior. We use the Contracts gem (runtime type checking) where I work, and we built out a custom matcher that verifies when you use this custom matcher, we effectively callsuper
(that is, use thereceive
matcher), and also verify that the input and output parameters match the specified contract of theExpectationTarget
's method. This works great, but:at_least
,and_yield
,exactly
and other chains thatreceive
supports.I want everything in our test suite to use this custom matcher by default. I tried something kind of hacky to make this happen:
The main problem is that I can't force people to use it until it supports more chains.
Suggested Changes
Allowing configuration of default matchers
One thought is that we can configure default matchers such as
RSpec::Mocks::Matchers::Receive
to allow for custom behavior. So when someone passes a receive matcher into theto
method of anExpectationTarget
using the normal syntaxexpect(target).to receive(:method)
, it will call our callbacks too.This could look like this, but could probably use a lot of work. The only thing I know about how
rspec-mocks
works under the hood is through debugging to try to make the hack above work, so I'm not sure what's considered public/private API and what would be a solid interface.I see that the matcher has a
recorded_customizations
instance variable, which looks like it could make this possible.Allowing inheriting from default matchers
It would be great to define custom matchers by inheriting from default matchers (if those matcher methods are or could be public interface).
So I could have a class that looks like this:
Would be happy to contribute after hearing your thoughts. Thanks for reading!
Your environment
ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-darwin16]
rspec-mocks (3.6.0)
Steps to reproduce
N/A
Expected behavior
N/A
Actual behavior
N/A
The text was updated successfully, but these errors were encountered: