This repository has been archived by the owner on Nov 30, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 358
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #467 from rspec/receive-chained-messages
Add allow(...).to receive_message_chain
- Loading branch information
Showing
17 changed files
with
559 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
features/message_expectations/message_chains_using_expect.feature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
Feature: Message chains in the expect syntax | ||
|
||
You can use `receive_message_chain` to stub nested calls | ||
on both partial and pure mock objects. | ||
|
||
Scenario: allow a chained message | ||
Given a file named "spec/chained_messages.rb" with: | ||
"""ruby | ||
describe "a chained message expectation" do | ||
it "passes if the expected number of calls happen" do | ||
d = double | ||
allow(d).to receive_message_chain(:to_a, :length) | ||
d.to_a.length | ||
end | ||
end | ||
""" | ||
When I run `rspec spec/chained_messages.rb` | ||
Then the output should contain "1 example, 0 failures" | ||
|
||
Scenario: allow a chained message with a return value | ||
Given a file named "spec/chained_messages.rb" with: | ||
"""ruby | ||
describe "a chained message expectation" do | ||
it "passes if the expected number of calls happen" do | ||
d = double | ||
allow(d).to receive_message_chain(:to_a, :length).and_return(3) | ||
expect(d.to_a.length).to eq(3) | ||
end | ||
end | ||
""" | ||
When I run `rspec spec/chained_messages.rb` | ||
Then the output should contain "1 example, 0 failures" | ||
|
||
Scenario: expect a chained message with a return value | ||
Given a file named "spec/chained_messages.rb" with: | ||
"""ruby | ||
describe "a chained message expectation" do | ||
it "passes if the expected number of calls happen" do | ||
d = double | ||
expect(d).to receive_message_chain(:to_a, :length).and_return(3) | ||
expect(d.to_a.length).to eq(3) | ||
end | ||
end | ||
""" | ||
When I run `rspec spec/chained_messages.rb` | ||
Then the output should contain "1 example, 0 failures" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module RSpec | ||
module Mocks | ||
module AnyInstance | ||
# @private | ||
class ExpectChainChain < StubChain | ||
def initialize(*args) | ||
super | ||
@expectation_fulfilled = false | ||
end | ||
|
||
def expectation_fulfilled? | ||
@expectation_fulfilled | ||
end | ||
|
||
def playback!(instance) | ||
super.tap { @expectation_fulfilled = true } | ||
end | ||
|
||
private | ||
|
||
def create_message_expectation_on(instance) | ||
::RSpec::Mocks::ExpectChain.expect_chain_on(instance, *@expectation_args, &@expectation_block) | ||
end | ||
|
||
def invocation_order | ||
@invocation_order ||= { | ||
:and_return => [nil], | ||
:and_raise => [nil], | ||
:and_yield => [nil] | ||
} | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
module RSpec | ||
module Mocks | ||
module Matchers | ||
#@api private | ||
class ReceiveMessageChain | ||
def initialize(chain, &block) | ||
@chain = chain | ||
@block = block | ||
@recorded_customizations = [] | ||
end | ||
|
||
[:and_return, :and_throw, :and_raise, :and_yield, :and_call_original].each do |msg| | ||
define_method(msg) do |*args, &block| | ||
@recorded_customizations << ExpectationCustomization.new(msg, args, block) | ||
self | ||
end | ||
end | ||
|
||
def name | ||
"receive_message_chain" | ||
end | ||
|
||
def setup_allowance(subject, &block) | ||
chain = StubChain.stub_chain_on(subject, *@chain, &(@block || block)) | ||
replay_customizations(chain) | ||
end | ||
|
||
def setup_any_instance_allowance(subject, &block) | ||
recorder = ::RSpec::Mocks.any_instance_recorder_for(subject) | ||
chain = recorder.stub_chain(*@chain, &(@block || block)) | ||
replay_customizations(chain) | ||
end | ||
|
||
def setup_any_instance_expectation(subject, &block) | ||
recorder = ::RSpec::Mocks.any_instance_recorder_for(subject) | ||
chain = recorder.expect_chain(*@chain, &(@block || block)) | ||
replay_customizations(chain) | ||
end | ||
|
||
def setup_expectation(subject, &block) | ||
chain = ExpectChain.expect_chain_on(subject, *@chain, &(@block || block)) | ||
replay_customizations(chain) | ||
end | ||
|
||
def setup_negative_expectation(*args) | ||
raise NegationUnsupportedError.new( | ||
"`expect(...).not_to receive_message_chain` is not supported " + | ||
"since it doesn't really make sense. What would it even mean?" | ||
) | ||
end | ||
|
||
alias matches? setup_expectation | ||
alias does_not_match? setup_negative_expectation | ||
|
||
private | ||
|
||
def replay_customizations(chain) | ||
@recorded_customizations.each do |customization| | ||
customization.playback_onto(chain) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
module RSpec | ||
module Mocks | ||
# @private | ||
class MessageChain | ||
attr_reader :object, :chain, :block | ||
|
||
def initialize(object, *chain, &blk) | ||
@object = object | ||
@chain, @block = format_chain(*chain, &blk) | ||
end | ||
|
||
# @api private | ||
def setup_chain | ||
if chain.length > 1 | ||
if matching_stub = find_matching_stub | ||
chain.shift | ||
chain_on(matching_stub.invoke(nil), *chain, &@block) | ||
elsif matching_expectation = find_matching_expectation | ||
chain.shift | ||
chain_on(matching_expectation.invoke_without_incrementing_received_count(nil), *chain, &@block) | ||
else | ||
next_in_chain = Double.new | ||
expectation(object, chain.shift, next_in_chain) | ||
chain_on(next_in_chain, *chain, &@block) | ||
end | ||
else | ||
::RSpec::Mocks.allow_message(object, chain.shift, {}, &block) | ||
end | ||
end | ||
|
||
private | ||
|
||
def expectation(object, message, returned_object) | ||
raise NotImplementedError.new | ||
end | ||
|
||
def chain_on(object, *chain, &block) | ||
initialize(object, *chain, &block) | ||
setup_chain | ||
end | ||
|
||
def format_chain(*chain, &blk) | ||
if Hash === chain.last | ||
hash = chain.pop | ||
hash.each do |k,v| | ||
chain << k | ||
blk = lambda { v } | ||
end | ||
end | ||
return chain.join('.').split('.'), blk | ||
end | ||
|
||
def find_matching_stub | ||
::RSpec::Mocks.proxy_for(object). | ||
__send__(:find_matching_method_stub, chain.first.to_sym) | ||
end | ||
|
||
def find_matching_expectation | ||
::RSpec::Mocks.proxy_for(object). | ||
__send__(:find_matching_expectation, chain.first.to_sym) | ||
end | ||
end | ||
|
||
# @private | ||
class ExpectChain < MessageChain | ||
# @api private | ||
def self.expect_chain_on(object, *chain, &blk) | ||
new(object, *chain, &blk).setup_chain | ||
end | ||
|
||
private | ||
|
||
def expectation(object, message, returned_object) | ||
::RSpec::Mocks.expect_message(object, message, {}) { returned_object } | ||
end | ||
end | ||
|
||
# @private | ||
class StubChain < MessageChain | ||
def self.stub_chain_on(object, *chain, &blk) | ||
new(object, *chain, &blk).setup_chain | ||
end | ||
|
||
private | ||
|
||
def expectation(object, message, returned_object) | ||
::RSpec::Mocks.allow_message(object, message, {}) { returned_object } | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.