Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 70fbe12

Browse files
authored
Merge pull request #1411 from askreet/and_invoke
Add `and_invoke` for sequential mixed (return/raise) responses.
2 parents eadd0e6 + ffc4583 commit 70fbe12

File tree

8 files changed

+254
-4
lines changed

8 files changed

+254
-4
lines changed

DEV-README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ Or ...
2020
bundle install --binstubs
2121
bin/rspec
2222

23-
## Customize the dev enviroment
23+
## Customize the dev environment
2424

2525
The Gemfile includes the gems you'll need to be able to run specs. If you want
26-
to customize your dev enviroment with additional tools like guard or
26+
to customize your dev environment with additional tools like guard or
2727
ruby-debug, add any additional gem declarations to Gemfile-custom (see
2828
Gemfile-custom.sample for some examples).

features/configuring_responses/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ methods are provided to configure how the test double responds to the message.
33

44
* <a href="./configuring-responses/returning-a-value">`and_return`</a>
55
* <a href="./configuring-responses/raising-an-error">`and_raise`</a>
6+
* <a href="./configuring-responses/mixed-responses">`and_invoke`</a>
67
* <a href="./configuring-responses/throwing">`and_throw`</a>
78
* <a href="./configuring-responses/yielding">`and_yield`</a>
89
* <a href="./configuring-responses/calling-the-original-implementation">`and_call_original`</a>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Feature: Mixed responses
2+
3+
Use `and_invoke` to invoke a Proc when a message is received. Pass `and_invoke` multiple
4+
Procs to have different behavior for consecutive calls. The final Proc will continue to be
5+
called if the message is received additional times.
6+
7+
Scenario: Mixed responses
8+
Given a file named "raises_and_then_returns.rb" with:
9+
"""ruby
10+
RSpec.describe "when the method is called multiple times" do
11+
it "raises and then later returns a value" do
12+
dbl = double
13+
allow(dbl).to receive(:foo).and_invoke(lambda { raise "failure" }, lambda { true })
14+
15+
expect { dbl.foo }.to raise_error("failure")
16+
expect(dbl.foo).to eq(true)
17+
end
18+
end
19+
"""
20+
When I run `rspec raises_and_then_returns.rb`
21+
Then the examples should all pass

lib/rspec/mocks/matchers/receive_message_chain.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def initialize(chain, &block)
1313
@recorded_customizations = []
1414
end
1515

16-
[:with, :and_return, :and_throw, :and_raise, :and_yield, :and_call_original].each do |msg|
16+
[:with, :and_return, :and_invoke, :and_throw, :and_raise, :and_yield, :and_call_original].each do |msg|
1717
define_method(msg) do |*args, &block|
1818
@recorded_customizations << ExpectationCustomization.new(msg, args, block)
1919
self

lib/rspec/mocks/message_expectation.rb

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class MessageExpectation
5353
# etc.
5454
#
5555
# If the message is received more times than there are values, the last
56-
# value is received for every subsequent call.
56+
# value is returned for every subsequent call.
5757
#
5858
# @return [nil] No further chaining is supported after this.
5959
# @example
@@ -85,6 +85,48 @@ def and_return(first_value, *values)
8585
nil
8686
end
8787

88+
# Tells the object to invoke a Proc when it receives the message. Given
89+
# more than one value, the result of the first Proc is returned the first
90+
# time the message is received, the result of the second Proc is returned
91+
# the next time, etc, etc.
92+
#
93+
# If the message is received more times than there are Procs, the result of
94+
# the last Proc is returned for every subsequent call.
95+
#
96+
# @return [nil] No further chaining is supported after this.
97+
# @example
98+
# allow(api).to receive(:get_foo).and_invoke(-> { raise ApiTimeout })
99+
# api.get_foo # => raises ApiTimeout
100+
# api.get_foo # => raises ApiTimeout
101+
#
102+
# allow(api).to receive(:get_foo).and_invoke(-> { raise ApiTimeout }, -> { raise ApiTimeout }, -> { :a_foo })
103+
# api.get_foo # => raises ApiTimeout
104+
# api.get_foo # => rasies ApiTimeout
105+
# api.get_foo # => :a_foo
106+
# api.get_foo # => :a_foo
107+
# api.get_foo # => :a_foo
108+
# # etc
109+
def and_invoke(first_proc, *procs)
110+
raise_already_invoked_error_if_necessary(__method__)
111+
if negative?
112+
raise "`and_invoke` is not supported with negative message expectations"
113+
end
114+
115+
if block_given?
116+
raise ArgumentError, "Implementation blocks aren't supported with `and_invoke`"
117+
end
118+
119+
procs.unshift(first_proc)
120+
if procs.any? { |p| !p.respond_to?(:call) }
121+
raise ArgumentError, "Arguments to `and_invoke` must be callable."
122+
end
123+
124+
@expected_received_count = [@expected_received_count, procs.size].max unless ignoring_args? || (@expected_received_count == 0 && @at_least)
125+
self.terminal_implementation_action = AndInvokeImplementation.new(procs)
126+
127+
nil
128+
end
129+
88130
# Tells the object to delegate to the original unmodified method
89131
# when it receives the message.
90132
#
@@ -683,6 +725,24 @@ def call(*_args_to_ignore, &_block)
683725
end
684726
end
685727

728+
# Handles the implementation of an `and_invoke` implementation.
729+
# @private
730+
class AndInvokeImplementation
731+
def initialize(procs_to_invoke)
732+
@procs_to_invoke = procs_to_invoke
733+
end
734+
735+
def call(*args, &block)
736+
proc = if @procs_to_invoke.size > 1
737+
@procs_to_invoke.shift
738+
else
739+
@procs_to_invoke.first
740+
end
741+
742+
proc.call(*args, &block)
743+
end
744+
end
745+
686746
# Represents a configured implementation. Takes into account
687747
# any number of sub-implementations.
688748
# @private

spec/rspec/mocks/and_invoke_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module RSpec
2+
module Mocks
3+
RSpec.describe 'and_invoke' do
4+
let(:obj) { double('obj') }
5+
6+
context 'when a block is passed' do
7+
it 'raises ArgumentError' do
8+
expect {
9+
allow(obj).to receive(:foo).and_invoke('bar') { 'baz' }
10+
}.to raise_error(ArgumentError, /implementation block/i)
11+
end
12+
end
13+
14+
context 'when no argument is passed' do
15+
it 'raises ArgumentError' do
16+
expect { allow(obj).to receive(:foo).and_invoke }.to raise_error(ArgumentError)
17+
end
18+
end
19+
20+
context 'when a non-callable are passed in any position' do
21+
let(:non_callable) { nil }
22+
let(:callable) { lambda { nil } }
23+
24+
it 'raises ArgumentError' do
25+
error = [ArgumentError, "Arguments to `and_invoke` must be callable."]
26+
27+
expect { allow(obj).to receive(:foo).and_invoke(non_callable) }.to raise_error(*error)
28+
expect { allow(obj).to receive(:foo).and_invoke(callable, non_callable) }.to raise_error(*error)
29+
end
30+
end
31+
32+
context 'when calling passed callables' do
33+
let(:dbl) { double }
34+
35+
it 'passes the arguments into the callable' do
36+
expect(dbl).to receive(:square_then_cube).and_invoke(lambda { |i| i ** 2 },
37+
lambda { |i| i ** 3 })
38+
39+
expect(dbl.square_then_cube(2)).to eq 4
40+
expect(dbl.square_then_cube(2)).to eq 8
41+
end
42+
end
43+
end
44+
end
45+
end

spec/rspec/mocks/matchers/receive_message_chain_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ module RSpec::Mocks::Matchers
5757
expect(object.to_a.length).to eq(3)
5858
end
5959

60+
it "works with and_invoke" do
61+
allow(object).to receive_message_chain(:to_a, :length).and_invoke(lambda { raise "error" })
62+
63+
expect { object.to_a.length }.to raise_error("error")
64+
end
65+
6066
it "can constrain the return value by the argument to the last call" do
6167
allow(object).to receive_message_chain(:one, :plus).with(1) { 2 }
6268
allow(object).to receive_message_chain(:one, :plus).with(2) { 3 }
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
module RSpec
2+
module Mocks
3+
RSpec.describe "a message expectation with multiple invoke handlers and no specified count" do
4+
let(:a_double) { double }
5+
6+
before(:each) do
7+
expect(a_double).to receive(:do_something).and_invoke(lambda { 1 }, lambda { raise "2" }, lambda { 3 })
8+
end
9+
10+
it "invokes procs in order" do
11+
expect(a_double.do_something).to eq 1
12+
expect { a_double.do_something }.to raise_error("2")
13+
expect(a_double.do_something).to eq 3
14+
verify a_double
15+
end
16+
17+
it "falls back to a previously stubbed value" do
18+
allow(a_double).to receive_messages :do_something => :stub_result
19+
expect(a_double.do_something).to eq 1
20+
expect { a_double.do_something }.to raise_error("2")
21+
expect(a_double.do_something).to eq 3
22+
expect(a_double.do_something).to eq :stub_result
23+
end
24+
25+
it "fails when there are too few calls (if there is no stub)" do
26+
a_double.do_something
27+
expect { a_double.do_something }.to raise_error("2")
28+
expect { verify a_double }.to fail
29+
end
30+
31+
it "fails when there are too many calls (if there is no stub)" do
32+
a_double.do_something
33+
expect { a_double.do_something }.to raise_error("2")
34+
a_double.do_something
35+
a_double.do_something
36+
expect { verify a_double }.to fail
37+
end
38+
end
39+
40+
RSpec.describe "a message expectation with multiple invoke handlers with a specified count equal to the number of values" do
41+
let(:a_double) { double }
42+
43+
before(:each) do
44+
expect(a_double).to receive(:do_something).exactly(3).times.and_invoke(lambda { 1 }, lambda { raise "2" }, lambda { 3 })
45+
end
46+
47+
it "returns values in order to consecutive calls" do
48+
expect(a_double.do_something).to eq 1
49+
expect { a_double.do_something }.to raise_error("2")
50+
expect(a_double.do_something).to eq 3
51+
verify a_double
52+
end
53+
end
54+
55+
RSpec.describe "a message expectation with multiple invoke handlers specifying at_least less than the number of values" do
56+
let(:a_double) { double }
57+
58+
before { expect(a_double).to receive(:do_something).at_least(:twice).with(no_args).and_invoke(lambda { 11 }, lambda { 22 }) }
59+
60+
it "uses the last return value for subsequent calls" do
61+
expect(a_double.do_something).to equal(11)
62+
expect(a_double.do_something).to equal(22)
63+
expect(a_double.do_something).to equal(22)
64+
verify a_double
65+
end
66+
67+
it "fails when called less than the specified number" do
68+
expect(a_double.do_something).to equal(11)
69+
expect { verify a_double }.to fail
70+
end
71+
72+
context "when method is stubbed too" do
73+
before { allow(a_double).to receive(:do_something).and_invoke lambda { :stub_result } }
74+
75+
it "uses the last value for subsequent calls" do
76+
expect(a_double.do_something).to equal(11)
77+
expect(a_double.do_something).to equal(22)
78+
expect(a_double.do_something).to equal(22)
79+
verify a_double
80+
end
81+
82+
it "fails when called less than the specified number" do
83+
expect(a_double.do_something).to equal(11)
84+
expect { verify a_double }.to fail
85+
end
86+
end
87+
end
88+
89+
RSpec.describe "a message expectation with multiple invoke handlers with a specified count larger than the number of values" do
90+
let(:a_double) { double }
91+
before { expect(a_double).to receive(:do_something).exactly(3).times.and_invoke(lambda { 11 }, lambda { 22 }) }
92+
93+
it "uses the last return value for subsequent calls" do
94+
expect(a_double.do_something).to equal(11)
95+
expect(a_double.do_something).to equal(22)
96+
expect(a_double.do_something).to equal(22)
97+
verify a_double
98+
end
99+
100+
it "fails when called less than the specified number" do
101+
a_double.do_something
102+
a_double.do_something
103+
expect { verify a_double }.to fail
104+
end
105+
106+
it "fails fast when called greater than the specified number" do
107+
a_double.do_something
108+
a_double.do_something
109+
a_double.do_something
110+
111+
expect_fast_failure_from(a_double) do
112+
a_double.do_something
113+
end
114+
end
115+
end
116+
end
117+
end

0 commit comments

Comments
 (0)