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

Commit 0b2e5b2

Browse files
pdaSam Phippen
authored andcommitted
Creates syntax agnostic matchers for message expectations.
See pull: #311 and #334. Signed-off-by: Sam Phippen <[email protected]> Conflicts: Changelog.md lib/rspec/mocks/message_expectation.rb spec/rspec/mocks/mock_spec.rb
1 parent 22e50a2 commit 0b2e5b2

17 files changed

+233
-104
lines changed

Changelog.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
### Development
22

3+
Enhancements:
4+
5+
* Document test spies in the readme. (Adarsh Pandit)
6+
* Add an `array_including` matcher. (Sam Phippen)
7+
* Create syntax agnostic message matchers enable message allowances and
8+
expectations to be set independently of any particular syntax being
9+
enabled (Paul Anneesley, Myron Marston and Sam Phippen).
10+
311
Bug Fixes:
412

513
* Bypass RSpec::Mocks::Syntax when mass-assigning stubs via double(). (Paul Annesley)

lib/rspec/mocks.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,41 @@ def any_instance_recorder_for(klass)
3030
space.any_instance_recorder_for(klass)
3131
end
3232

33+
# Adds an allowance (stub) on `subject`
34+
#
35+
# @param subject the subject to which the message will be added
36+
# @param message a symbol, representing the message that will be
37+
# added.
38+
# @param opts a hash of options, :expected_from is used to set the
39+
# original call site
40+
# @param block an optional implementation for the allowance
41+
#
42+
# @example Defines the implementation of `foo` on `bar`, using the passed block
43+
# x = 0
44+
# RSpec::Mocks.allow_message(bar, :foo) { x += 1 }
45+
def allow_message(subject, message, opts={}, &block)
46+
orig_caller = opts.fetch(:expected_from) { caller(1)[0] }
47+
::RSpec::Mocks.proxy_for(subject).
48+
add_stub(orig_caller, message.to_sym, opts, &block)
49+
end
50+
51+
# Sets a message expectation on `subject`.
52+
# @param subject the subject on which the message will be expected
53+
# @param message a symbol, representing the message that will be
54+
# expected.
55+
# @param opts a hash of options, :expected_from is used to set the
56+
# original call site
57+
# @param block an optional implementation for the expectation
58+
#
59+
# @example Expect the message `foo` to receive `bar`, then call it
60+
# RSpec::Mocks.expect_message(bar, :foo)
61+
# bar.foo
62+
def expect_message(subject, message, opts={}, &block)
63+
orig_caller = opts.fetch(:expected_from) { caller(1)[0] }
64+
::RSpec::Mocks.proxy_for(subject).
65+
add_message_expectation(orig_caller, message.to_sym, opts, &block)
66+
end
67+
3368
# @api private
3469
KERNEL_METHOD_METHOD = ::Kernel.instance_method(:method)
3570

lib/rspec/mocks/any_instance/chain.rb

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,39 @@ def initialize(*args, &block)
77
@expectation_block = block
88
end
99

10-
class << self
11-
private
12-
10+
module Customizations
1311
# @macro [attach] record
1412
# @method $1(*args, &block)
1513
# Records the `$1` message for playback against an instance that
1614
# invokes a method stubbed or mocked using `any_instance`.
1715
#
1816
# @see RSpec::Mocks::MessageExpectation#$1
1917
#
20-
def record(method_name)
18+
def self.record(method_name)
2119
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
2220
def #{method_name}(*args, &block)
2321
record(:#{method_name}, *args, &block)
2422
end
2523
EOM
2624
end
25+
26+
record :and_return
27+
record :and_raise
28+
record :and_throw
29+
record :and_yield
30+
record :and_call_original
31+
record :with
32+
record :once
33+
record :twice
34+
record :any_number_of_times
35+
record :exactly
36+
record :times
37+
record :never
38+
record :at_least
39+
record :at_most
2740
end
2841

29-
record :and_return
30-
record :and_raise
31-
record :and_throw
32-
record :and_yield
33-
record :and_call_original
34-
record :with
35-
record :once
36-
record :twice
37-
record :any_number_of_times
38-
record :exactly
39-
record :times
40-
record :never
41-
record :at_least
42-
record :at_most
42+
include Customizations
4343

4444
# @private
4545
def playback!(instance)
@@ -63,8 +63,17 @@ def expectation_fulfilled!
6363
@expectation_fulfilled = true
6464
end
6565

66+
def never
67+
ErrorGenerator.raise_double_negation_error("expect_any_instance_of(MyClass)") if negated?
68+
super
69+
end
70+
6671
private
6772

73+
def negated?
74+
messages.any? { |(message, *_), _| message.to_sym == :never }
75+
end
76+
6877
def messages
6978
@messages ||= []
7079
end

lib/rspec/mocks/any_instance/expectation_chain.rb

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,34 +36,7 @@ def invocation_order
3636
}
3737
end
3838
end
39-
40-
# @api private
41-
class NegativeExpectationChain < ExpectationChain
42-
# `should_not_receive` causes a failure at the point in time the
43-
# message is wrongly received, rather than during `rspec_verify`
44-
# at the end of an example. Thus, we should always consider a
45-
# negative expectation fulfilled for the purposes of end-of-example
46-
# verification (which is where this is used).
47-
def expectation_fulfilled?
48-
true
49-
end
50-
51-
private
52-
53-
def create_message_expectation_on(instance)
54-
proxy = ::RSpec::Mocks.proxy_for(instance)
55-
expected_from = IGNORED_BACKTRACE_LINE
56-
proxy.add_negative_message_expectation(expected_from, *@expectation_args, &@expectation_block)
57-
end
58-
59-
def invocation_order
60-
@invocation_order ||= {
61-
:with => [nil],
62-
:and_return => [:with, nil],
63-
:and_raise => [:with, nil]
64-
}
65-
end
66-
end
6739
end
6840
end
6941
end
42+

lib/rspec/mocks/any_instance/recorder.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ def should_receive(method_name, &block)
6060
end
6161

6262
def should_not_receive(method_name, &block)
63-
observe!(method_name)
64-
message_chains.add(method_name, NegativeExpectationChain.new(method_name, &block))
63+
should_receive(method_name, &block).never
6564
end
6665

6766
# Removes any previously recorded stubs, stub_chains or message

lib/rspec/mocks/error_generator.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ def raise_expectation_on_mocked_method(method)
116116
"method has been mocked instead of stubbed."
117117
end
118118

119+
def self.raise_double_negation_error(wrapped_expression)
120+
raise "Isn't life confusing enough? You've already set a " +
121+
"negative message expectation and now you are trying to " +
122+
"negate it again with `never`. What does an expression like " +
123+
"`#{wrapped_expression}.not_to receive(:msg).never` even mean?"
124+
end
125+
119126
private
120127

121128
def intro

lib/rspec/mocks/matchers/receive.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ def setup_expectation(subject, &block)
2222
alias matches? setup_expectation
2323

2424
def setup_negative_expectation(subject, &block)
25-
setup_mock_proxy_method_substitute(subject, :add_negative_message_expectation, block)
25+
# ensure `never` goes first for cases like `never.and_return(5)`,
26+
# where `and_return` is meant to raise an error
27+
@recorded_customizations.unshift Customization.new(:never, [], nil)
28+
29+
setup_expectation(subject, &block)
2630
end
2731
alias does_not_match? setup_negative_expectation
2832

lib/rspec/mocks/message_expectation.rb

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,20 @@ def expected_args
7171
# counter.stub(:count) { 1 }
7272
# counter.count # => 1
7373
def and_return(*values, &implementation)
74-
@expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 and @at_least)
75-
76-
if implementation
77-
# TODO: deprecate `and_return { value }`
78-
self.inner_implementation_action = implementation
74+
if negative?
75+
raise "`and_return` is not supported with negative message expectations"
7976
else
80-
self.terminal_implementation_action = AndReturnImplementation.new(values)
81-
end
77+
@expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 and @at_least)
8278

83-
nil
79+
if implementation
80+
# TODO: deprecate `and_return { value }`
81+
self.inner_implementation_action = implementation
82+
else
83+
self.terminal_implementation_action = AndReturnImplementation.new(values)
84+
end
85+
86+
nil
87+
end
8488
end
8589

8690
# Tells the object to delegate to the original unmodified method
@@ -167,7 +171,7 @@ def matches?(message, *args)
167171

168172
# @private
169173
def invoke(parent_stub, *args, &block)
170-
if (@expected_received_count == 0 && !@at_least) || ((@exactly || @at_most) && (@actual_received_count == @expected_received_count))
174+
if negative? || ((@exactly || @at_most) && (@actual_received_count == @expected_received_count))
171175
@actual_received_count += 1
172176
@failed_fast = true
173177
#args are the args we actually received, @argument_list_matcher is the
@@ -188,6 +192,11 @@ def invoke(parent_stub, *args, &block)
188192
end
189193
end
190194

195+
# @private
196+
def negative?
197+
@expected_received_count == 0 && !@at_least
198+
end
199+
191200
# @private
192201
def called_max_times?
193202
@expected_received_count != :any &&
@@ -365,6 +374,7 @@ def any_number_of_times(&block)
365374
#
366375
# car.should_receive(:stop).never
367376
def never
377+
ErrorGenerator.raise_double_negation_error("expect(obj)") if negative?
368378
@expected_received_count = 0
369379
self
370380
end
@@ -407,7 +417,7 @@ def ordered(&block)
407417

408418
# @private
409419
def negative_expectation_for?(message)
410-
return false
420+
@message == message && negative?
411421
end
412422

413423
# @private
@@ -450,25 +460,6 @@ def terminal_implementation_action=(action)
450460
end
451461
end
452462

453-
# @private
454-
class NegativeMessageExpectation < MessageExpectation
455-
# @private
456-
def initialize(error_generator, expectation_ordering, expected_from, method_double, &implementation)
457-
super(error_generator, expectation_ordering, expected_from, method_double, 0, {}, &implementation)
458-
end
459-
460-
# no-op
461-
# @deprecated and_return is not supported with negative message expectations.
462-
def and_return(*)
463-
RSpec.deprecate "and_return with should_not_receive"
464-
end
465-
466-
# @private
467-
def negative_expectation_for?(message)
468-
return @message == message
469-
end
470-
end
471-
472463
# Handles the implementation of an `and_yield` declaration.
473464
# @private
474465
class AndYieldImplementation

lib/rspec/mocks/method_double.rb

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,15 +226,6 @@ def add_expectation(error_generator, expectation_ordering, expected_from, opts,
226226
expectation
227227
end
228228

229-
# @private
230-
def add_negative_expectation(error_generator, expectation_ordering, expected_from, &implementation)
231-
configure_method
232-
expectation = NegativeMessageExpectation.new(error_generator, expectation_ordering,
233-
expected_from, self, &implementation)
234-
expectations.unshift expectation
235-
expectation
236-
end
237-
238229
# @private
239230
def build_expectation(error_generator, expectation_ordering)
240231
expected_from = IGNORED_BACKTRACE_LINE

lib/rspec/mocks/proxy.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ def add_message_expectation(location, method_name, opts={}, &block)
5151
meth_double.add_expectation @error_generator, @expectation_ordering, location, opts, &block
5252
end
5353

54-
# @private
55-
def add_negative_message_expectation(location, method_name, &implementation)
56-
method_double[method_name].add_negative_expectation @error_generator, @expectation_ordering, location, &implementation
57-
end
58-
5954
# @private
6055
def build_expectation(method_name)
6156
meth_double = method_double[method_name]

0 commit comments

Comments
 (0)