rspec-mocks 3.0.4 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Changelog.md +26 -0
  5. data/README.md +29 -15
  6. data/lib/rspec/mocks/any_instance/chain.rb +2 -2
  7. data/lib/rspec/mocks/any_instance/expectation_chain.rb +2 -3
  8. data/lib/rspec/mocks/any_instance/message_chains.rb +8 -7
  9. data/lib/rspec/mocks/any_instance/proxy.rb +2 -2
  10. data/lib/rspec/mocks/any_instance/recorder.rb +27 -25
  11. data/lib/rspec/mocks/any_instance/stub_chain.rb +3 -5
  12. data/lib/rspec/mocks/argument_list_matcher.rb +4 -4
  13. data/lib/rspec/mocks/argument_matchers.rb +23 -5
  14. data/lib/rspec/mocks/configuration.rb +3 -10
  15. data/lib/rspec/mocks/error_generator.rb +33 -27
  16. data/lib/rspec/mocks/example_methods.rb +74 -6
  17. data/lib/rspec/mocks/instance_method_stasher.rb +5 -5
  18. data/lib/rspec/mocks/matchers/have_received.rb +7 -8
  19. data/lib/rspec/mocks/matchers/receive.rb +8 -8
  20. data/lib/rspec/mocks/matchers/receive_message_chain.rb +4 -5
  21. data/lib/rspec/mocks/matchers/receive_messages.rb +6 -7
  22. data/lib/rspec/mocks/message_chain.rb +5 -5
  23. data/lib/rspec/mocks/message_expectation.rb +56 -28
  24. data/lib/rspec/mocks/method_double.rb +15 -12
  25. data/lib/rspec/mocks/method_reference.rb +8 -7
  26. data/lib/rspec/mocks/mutate_const.rb +26 -100
  27. data/lib/rspec/mocks/object_reference.rb +12 -13
  28. data/lib/rspec/mocks/order_group.rb +4 -5
  29. data/lib/rspec/mocks/proxy.rb +31 -25
  30. data/lib/rspec/mocks/space.rb +24 -24
  31. data/lib/rspec/mocks/syntax.rb +8 -8
  32. data/lib/rspec/mocks/targets.rb +6 -6
  33. data/lib/rspec/mocks/test_double.rb +3 -3
  34. data/lib/rspec/mocks/verifying_double.rb +10 -12
  35. data/lib/rspec/mocks/verifying_message_expecation.rb +15 -13
  36. data/lib/rspec/mocks/verifying_proxy.rb +11 -15
  37. data/lib/rspec/mocks/version.rb +1 -1
  38. metadata +5 -5
  39. metadata.gz.sig +0 -0
@@ -40,29 +40,38 @@ module RSpec
40
40
  def raise_unexpected_message_args_error(expectation, *args)
41
41
  expected_args = format_args(*expectation.expected_args)
42
42
  actual_args = format_received_args(*args)
43
- __raise "#{intro} received #{expectation.message.inspect} with unexpected arguments\n expected: #{expected_args}\n got: #{actual_args}"
43
+ __raise "#{intro} received #{expectation.message.inspect} with " \
44
+ "unexpected arguments\n expected: #{expected_args}\n" \
45
+ " got: #{actual_args}"
44
46
  end
45
47
 
46
48
  # @private
47
49
  def raise_missing_default_stub_error(expectation, *args)
48
50
  expected_args = format_args(*expectation.expected_args)
49
51
  actual_args = format_received_args(*args)
50
- __raise "#{intro} received #{expectation.message.inspect} with unexpected arguments\n expected: #{expected_args}\n got: #{actual_args}\n Please stub a default value first if message might be received with other args as well. \n"
52
+ __raise "#{intro} received #{expectation.message.inspect} with " \
53
+ "unexpected arguments\n expected: #{expected_args}\n" \
54
+ " got: #{actual_args}\n Please stub a default value " \
55
+ "first if message might be received with other args as well. \n"
51
56
  end
52
57
 
53
58
  # @private
54
59
  def raise_similar_message_args_error(expectation, *args_for_multiple_calls)
55
60
  expected_args = format_args(*expectation.expected_args)
56
- actual_args = args_for_multiple_calls.collect {|a| format_received_args(*a)}.join(", ")
57
- __raise "#{intro} received #{expectation.message.inspect} with unexpected arguments\n expected: #{expected_args}\n got: #{actual_args}"
61
+ actual_args = args_for_multiple_calls.map { |a| format_received_args(*a) }.join(", ")
62
+ __raise "#{intro} received #{expectation.message.inspect} with " \
63
+ "unexpected arguments\n expected: #{expected_args}\n" \
64
+ " got: #{actual_args}"
58
65
  end
59
66
 
67
+ # rubocop:disable Style/ParameterLists
60
68
  # @private
61
69
  def raise_expectation_error(message, expected_received_count, argument_list_matcher, actual_received_count, expectation_count_type, *args)
62
70
  expected_part = expected_part_of_expectation_error(expected_received_count, expectation_count_type, argument_list_matcher)
63
71
  received_part = received_part_of_expectation_error(actual_received_count, *args)
64
72
  __raise "(#{intro}).#{message}#{format_args(*args)}\n #{expected_part}\n #{received_part}"
65
73
  end
74
+ # rubocop:enable Style/ParameterLists
66
75
 
67
76
  # @private
68
77
  def raise_unimplemented_error(doubled_module, method_name)
@@ -87,10 +96,10 @@ module RSpec
87
96
  # @private
88
97
  def raise_expired_test_double_error
89
98
  raise ExpiredTestDoubleError,
90
- "#{intro} was originally created in one example but has leaked into " +
91
- "another example and can no longer be used. rspec-mocks' doubles are " +
92
- "designed to only last for one example, and you need to create a new " +
93
- "one in each example you wish to use it for."
99
+ "#{intro} was originally created in one example but has leaked into " \
100
+ "another example and can no longer be used. rspec-mocks' doubles are " \
101
+ "designed to only last for one example, and you need to create a new " \
102
+ "one in each example you wish to use it for."
94
103
  end
95
104
 
96
105
  # @private
@@ -128,15 +137,13 @@ module RSpec
128
137
  # @private
129
138
  def method_call_args_description(args)
130
139
  case args.first
131
- when ArgumentMatchers::AnyArgsMatcher
132
- return " with any arguments"
133
- when ArgumentMatchers::NoArgsMatcher
134
- return " with no arguments"
140
+ when ArgumentMatchers::AnyArgsMatcher then " with any arguments"
141
+ when ArgumentMatchers::NoArgsMatcher then " with no arguments"
135
142
  end
136
143
  end
137
144
 
138
145
  # @private
139
- def describe_expectation(message, expected_received_count, actual_received_count, *args)
146
+ def describe_expectation(message, expected_received_count, _actual_received_count, *args)
140
147
  "have received #{message}#{format_args(*args)} #{count_message(expected_received_count)}"
141
148
  end
142
149
 
@@ -162,30 +169,30 @@ module RSpec
162
169
 
163
170
  # @private
164
171
  def raise_only_valid_on_a_partial_double(method)
165
- __raise "#{intro} is a pure test double. `#{method}` is only " +
172
+ __raise "#{intro} is a pure test double. `#{method}` is only " \
166
173
  "available on a partial double."
167
174
  end
168
175
 
169
176
  # @private
170
177
  def raise_expectation_on_unstubbed_method(method)
171
- __raise "#{intro} expected to have received #{method}, but that " +
172
- "method has not been stubbed."
178
+ __raise "#{intro} expected to have received #{method}, but that " \
179
+ "object is not a spy or method has not been stubbed."
173
180
  end
174
181
 
175
182
  # @private
176
183
  def raise_expectation_on_mocked_method(method)
177
- __raise "#{intro} expected to have received #{method}, but that " +
178
- "method has been mocked instead of stubbed."
184
+ __raise "#{intro} expected to have received #{method}, but that " \
185
+ "method has been mocked instead of stubbed or spied."
179
186
  end
180
187
 
181
188
  def self.raise_double_negation_error(wrapped_expression)
182
- raise "Isn't life confusing enough? You've already set a " +
183
- "negative message expectation and now you are trying to " +
184
- "negate it again with `never`. What does an expression like " +
189
+ raise "Isn't life confusing enough? You've already set a " \
190
+ "negative message expectation and now you are trying to " \
191
+ "negate it again with `never`. What does an expression like " \
185
192
  "`#{wrapped_expression}.not_to receive(:msg).never` even mean?"
186
193
  end
187
194
 
188
- private
195
+ private
189
196
 
190
197
  def intro
191
198
  if @name
@@ -203,7 +210,7 @@ module RSpec
203
210
 
204
211
  def __raise(message)
205
212
  message = opts[:message] unless opts[:message].nil?
206
- Kernel::raise(RSpec::Mocks::MockExpectationError, message)
213
+ Kernel.raise(RSpec::Mocks::MockExpectationError, message)
207
214
  end
208
215
 
209
216
  def arg_message(*args)
@@ -215,7 +222,7 @@ module RSpec
215
222
  end
216
223
 
217
224
  def arg_list(*args)
218
- args.collect {|arg| arg_has_valid_description(arg) ? arg.description : arg.inspect }.join(", ")
225
+ args.map { |arg| arg_has_valid_description(arg) ? arg.description : arg.inspect }.join(", ")
219
226
  end
220
227
 
221
228
  def arg_has_valid_description(arg)
@@ -229,19 +236,18 @@ module RSpec
229
236
  end
230
237
 
231
238
  def received_arg_list(*args)
232
- args.collect(&:inspect).join(", ")
239
+ args.map(&:inspect).join(", ")
233
240
  end
234
241
 
235
242
  def count_message(count, expectation_count_type=nil)
236
243
  return "at least #{times(count.abs)}" if count < 0 || expectation_count_type == :at_least
237
244
  return "at most #{times(count)}" if expectation_count_type == :at_most
238
- return times(count)
245
+ times(count)
239
246
  end
240
247
 
241
248
  def times(count)
242
249
  "#{count} time#{count == 1 ? '' : 's'}"
243
250
  end
244
-
245
251
  end
246
252
  end
247
253
  end
@@ -84,6 +84,73 @@ module RSpec
84
84
  ExampleMethods.declare_verifying_double(ObjectVerifyingDouble, ref, *args)
85
85
  end
86
86
 
87
+ # @overload spy()
88
+ # @overload spy(name)
89
+ # @param name [String/Symbol] used to clarify intent
90
+ # @overload spy(stubs)
91
+ # @param stubs (Hash) hash of message/return-value pairs
92
+ # @overload spy(name, stubs)
93
+ # @param name [String/Symbol] used to clarify intent
94
+ # @param stubs (Hash) hash of message/return-value pairs
95
+ # @return (Double)
96
+ #
97
+ # Constructs a test double that is optimized for use with
98
+ # `have_received`. With a normal double one has to stub methods in order
99
+ # to be able to spy them. A spy automatically spies on all methods.
100
+ def spy(*args)
101
+ double(*args).as_null_object
102
+ end
103
+
104
+ # @overload instance_spy(doubled_class)
105
+ # @param doubled_class [String, Class]
106
+ # @overload instance_spy(doubled_class, stubs)
107
+ # @param doubled_class [String, Class]
108
+ # @param stubs [Hash] hash of message/return-value pairs
109
+ # @return InstanceVerifyingDouble
110
+ #
111
+ # Constructs a test double that is optimized for use with `have_received`
112
+ # against a specific class. If the given class name has been loaded, only
113
+ # instance methods defined on the class are allowed to be stubbed. With
114
+ # a normal double one has to stub methods in order to be able to spy
115
+ # them. An instance_spy automatically spies on all instance methods to
116
+ # which the class responds.
117
+ def instance_spy(*args)
118
+ instance_double(*args).as_null_object
119
+ end
120
+
121
+ # @overload object_spy(object_or_name)
122
+ # @param object_or_name [String, Object]
123
+ # @overload object_spy(object_or_name, stubs)
124
+ # @param object_or_name [String, Object]
125
+ # @param stubs [Hash] hash of message/return-value pairs
126
+ # @return ObjectVerifyingDouble
127
+ #
128
+ # Constructs a test double that is optimized for use with `have_received`
129
+ # against a specific object. Only instance methods defined on the object
130
+ # are allowed to be stubbed. With a normal double one has to stub
131
+ # methods in order to be able to spy them. An object_spy automatically
132
+ # spies on all methods to which the object responds.
133
+ def object_spy(*args)
134
+ object_double(*args).as_null_object
135
+ end
136
+
137
+ # @overload class_spy(doubled_class)
138
+ # @param doubled_class [String, Module]
139
+ # @overload class_spy(doubled_class, stubs)
140
+ # @param doubled_class [String, Module]
141
+ # @param stubs [Hash] hash of message/return-value pairs
142
+ # @return ClassVerifyingDouble
143
+ #
144
+ # Constructs a test double that is optimized for use with `have_received`
145
+ # against a specific class. If the given class name has been loaded,
146
+ # only class methods defined on the class are allowed to be stubbed.
147
+ # With a normal double one has to stub methods in order to be able to spy
148
+ # them. An class_spy automatically spies on all class methods to which the
149
+ # class responds.
150
+ def class_spy(*args)
151
+ class_double(*args).as_null_object
152
+ end
153
+
87
154
  # Disables warning messages about expectations being set on nil.
88
155
  #
89
156
  # By default warning messages are issued when expectations are set on
@@ -130,7 +197,7 @@ module RSpec
130
197
  # stub_const("CardDeck", Class.new, :transfer_nested_constants => [:SUITS])
131
198
  # CardDeck::SUITS # => our suits array
132
199
  # CardDeck::NUM_CARDS # => uninitialized constant error
133
- def stub_const(constant_name, value, options = {})
200
+ def stub_const(constant_name, value, options={})
134
201
  ConstantMutator.stub(constant_name, value, options)
135
202
  end
136
203
 
@@ -151,8 +218,9 @@ module RSpec
151
218
  end
152
219
 
153
220
  # Verifies that the given object received the expected message during the
154
- # course of the test. The method must have previously been stubbed in
155
- # order for messages to be verified.
221
+ # course of the test. On a spy objects or as null object doubles this
222
+ # works for any method, on other objects the method must have
223
+ # been stubbed beforehand in order for messages to be verified.
156
224
  #
157
225
  # Stubbing and verifying messages received in this way implements the
158
226
  # Test Spy pattern.
@@ -288,9 +356,9 @@ module RSpec
288
356
  !ref.defined?
289
357
 
290
358
  raise VerifyingDoubleNotDefinedError,
291
- "#{ref.description} is not a defined constant. " +
292
- "Perhaps you misspelt it? " +
293
- "Disable check with verify_doubled_constant_names configuration option."
359
+ "#{ref.description} is not a defined constant. " \
360
+ "Perhaps you misspelt it? " \
361
+ "Disable check with verify_doubled_constant_names configuration option."
294
362
  end
295
363
 
296
364
  declare_double(type, ref, *args)
@@ -53,7 +53,7 @@ module RSpec
53
53
 
54
54
  # @private
55
55
  def stash
56
- return if !method_defined_directly_on_klass?
56
+ return unless method_defined_directly_on_klass?
57
57
  @original_method ||= ::RSpec::Support.method_handle_for(@object, @method)
58
58
  end
59
59
 
@@ -80,9 +80,9 @@ module RSpec
80
80
  yield
81
81
  rescue TypeError
82
82
  RSpec.warn_with(
83
- "RSpec failed to properly restore a partial double (#{@object.inspect}) " +
84
- "to its original state due to a known bug in MRI 2.0.0-p195 & p247 " +
85
- "(https://bugs.ruby-lang.org/issues/8686). This object may remain " +
83
+ "RSpec failed to properly restore a partial double (#{@object.inspect}) " \
84
+ "to its original state due to a known bug in MRI 2.0.0-p195 & p247 " \
85
+ "(https://bugs.ruby-lang.org/issues/8686). This object may remain " \
86
86
  "screwed up for the rest of this process. Please upgrade to 2.0.0-p353 or above.",
87
87
  :call_site => nil, :use_spec_location_as_call_site => true
88
88
  )
@@ -102,7 +102,7 @@ module RSpec
102
102
  end
103
103
 
104
104
  # @private
105
- def method_defined_on_klass?(klass = @klass)
105
+ def method_defined_on_klass?(klass=@klass)
106
106
  MethodReference.method_defined_at_any_visibility?(klass, @method)
107
107
  end
108
108
 
@@ -3,9 +3,9 @@ module RSpec
3
3
  module Matchers
4
4
  # @private
5
5
  class HaveReceived
6
- COUNT_CONSTRAINTS = %w(exactly at_least at_most times once twice)
7
- ARGS_CONSTRAINTS = %w(with)
8
- CONSTRAINTS = COUNT_CONSTRAINTS + ARGS_CONSTRAINTS + %w(ordered)
6
+ COUNT_CONSTRAINTS = %w[exactly at_least at_most times once twice thrice]
7
+ ARGS_CONSTRAINTS = %w[with]
8
+ CONSTRAINTS = COUNT_CONSTRAINTS + ARGS_CONSTRAINTS + %w[ordered]
9
9
 
10
10
  def initialize(method_name, &block)
11
11
  @method_name = method_name
@@ -71,14 +71,13 @@ module RSpec
71
71
  end
72
72
 
73
73
  def ensure_count_unconstrained
74
- if count_constraint
75
- raise RSpec::Mocks::MockExpectationError,
76
- "can't use #{count_constraint} when negative"
77
- end
74
+ return unless count_constraint
75
+ raise RSpec::Mocks::MockExpectationError,
76
+ "can't use #{count_constraint} when negative"
78
77
  end
79
78
 
80
79
  def count_constraint
81
- @constraints.map(&:first).detect do |constraint|
80
+ @constraints.map(&:first).find do |constraint|
82
81
  COUNT_CONSTRAINTS.include?(constraint)
83
82
  end
84
83
  end
@@ -61,14 +61,14 @@ module RSpec
61
61
  private
62
62
 
63
63
  def warn_if_any_instance(expression, subject)
64
- if AnyInstance::Proxy === subject
65
- RSpec.warning(
66
- "`#{expression}(#{subject.klass}.any_instance).to` " <<
67
- "is probably not what you meant, it does not operate on " <<
68
- "any instance of `#{subject.klass}`. " <<
69
- "Use `#{expression}_any_instance_of(#{subject.klass}).to` instead."
70
- )
71
- end
64
+ return unless AnyInstance::Proxy === subject
65
+
66
+ RSpec.warning(
67
+ "`#{expression}(#{subject.klass}.any_instance).to` " \
68
+ "is probably not what you meant, it does not operate on " \
69
+ "any instance of `#{subject.klass}`. " \
70
+ "Use `#{expression}_any_instance_of(#{subject.klass}).to` instead."
71
+ )
72
72
  end
73
73
 
74
74
  def setup_mock_proxy_method_substitute(subject, method, block)
@@ -44,11 +44,10 @@ module RSpec
44
44
  replay_customizations(chain)
45
45
  end
46
46
 
47
- def setup_negative_expectation(*args)
48
- raise NegationUnsupportedError.new(
49
- "`expect(...).not_to receive_message_chain` is not supported " +
50
- "since it doesn't really make sense. What would it even mean?"
51
- )
47
+ def setup_negative_expectation(*_args)
48
+ raise NegationUnsupportedError,
49
+ "`expect(...).not_to receive_message_chain` is not supported " \
50
+ "since it doesn't really make sense. What would it even mean?"
52
51
  end
53
52
 
54
53
  alias matches? setup_expectation
@@ -3,7 +3,6 @@ module RSpec
3
3
  module Matchers
4
4
  # @private
5
5
  class ReceiveMessages
6
-
7
6
  def initialize(message_return_value_hash)
8
7
  @message_return_value_hash = message_return_value_hash
9
8
  @backtrace_line = CallerFilter.first_non_rspec_line
@@ -15,29 +14,29 @@ module RSpec
15
14
 
16
15
  def setup_expectation(subject)
17
16
  warn_about_block if block_given?
18
- each_message_on( proxy_on(subject) ) do |host, message, return_value|
17
+ each_message_on(proxy_on(subject)) do |host, message, return_value|
19
18
  host.add_simple_expectation(message, return_value, @backtrace_line)
20
19
  end
21
20
  end
22
21
  alias matches? setup_expectation
23
22
 
24
- def setup_negative_expectation(subject)
23
+ def setup_negative_expectation(_subject)
25
24
  raise NegationUnsupportedError,
26
- "`expect(...).to_not receive_messages` is not supported since it " +
27
- "doesn't really make sense. What would it even mean?"
25
+ "`expect(...).to_not receive_messages` is not supported since it " \
26
+ "doesn't really make sense. What would it even mean?"
28
27
  end
29
28
  alias does_not_match? setup_negative_expectation
30
29
 
31
30
  def setup_allowance(subject)
32
31
  warn_about_block if block_given?
33
- each_message_on( proxy_on(subject) ) do |host, message, return_value|
32
+ each_message_on(proxy_on(subject)) do |host, message, return_value|
34
33
  host.add_simple_stub(message, return_value)
35
34
  end
36
35
  end
37
36
 
38
37
  def setup_any_instance_expectation(subject)
39
38
  warn_about_block if block_given?
40
- each_message_on( any_instance_of(subject) ) do |host, message, return_value|
39
+ each_message_on(any_instance_of(subject)) do |host, message, return_value|
41
40
  host.should_receive(message).and_return(return_value)
42
41
  end
43
42
  end
@@ -12,10 +12,10 @@ module RSpec
12
12
  # @api private
13
13
  def setup_chain
14
14
  if chain.length > 1
15
- if matching_stub = find_matching_stub
15
+ if (matching_stub = find_matching_stub)
16
16
  chain.shift
17
17
  chain_on(matching_stub.invoke(nil), *chain, &@block)
18
- elsif matching_expectation = find_matching_expectation
18
+ elsif (matching_expectation = find_matching_expectation)
19
19
  chain.shift
20
20
  chain_on(matching_expectation.invoke_without_incrementing_received_count(nil), *chain, &@block)
21
21
  else
@@ -30,8 +30,8 @@ module RSpec
30
30
 
31
31
  private
32
32
 
33
- def expectation(object, message, &return_block)
34
- raise NotImplementedError.new
33
+ def expectation(_object, _message, &_return_block)
34
+ raise NotImplementedError
35
35
  end
36
36
 
37
37
  def chain_on(object, *chain, &block)
@@ -42,7 +42,7 @@ module RSpec
42
42
  def format_chain(*chain, &blk)
43
43
  if Hash === chain.last
44
44
  hash = chain.pop
45
- hash.each do |k,v|
45
+ hash.each do |k, v|
46
46
  chain << k
47
47
  blk = Proc.new { v }
48
48
  end
@@ -1,6 +1,5 @@
1
1
  module RSpec
2
2
  module Mocks
3
-
4
3
  # A message expectation that only allows concrete return values to be set
5
4
  # for a message. While this same effect can be achieved using a standard
6
5
  # MessageExpecation, this version is much faster and so can be used as an
@@ -8,8 +7,7 @@ module RSpec
8
7
  #
9
8
  # @private
10
9
  class SimpleMessageExpectation
11
-
12
- def initialize(message, response, error_generator, backtrace_line = nil)
10
+ def initialize(message, response, error_generator, backtrace_line=nil)
13
11
  @message, @response, @error_generator, @backtrace_line = message.to_sym, response, error_generator, backtrace_line
14
12
  @received = false
15
13
  end
@@ -45,6 +43,7 @@ module RSpec
45
43
  attr_writer :expected_received_count, :expected_from, :argument_list_matcher
46
44
  protected :expected_received_count=, :expected_from=, :error_generator, :error_generator=, :implementation=
47
45
 
46
+ # rubocop:disable Style/ParameterLists
48
47
  # @private
49
48
  def initialize(error_generator, expectation_ordering, expected_from, method_double,
50
49
  type=:expectation, opts={}, &implementation_block)
@@ -70,6 +69,7 @@ module RSpec
70
69
  @implementation = Implementation.new
71
70
  self.inner_implementation_action = implementation_block
72
71
  end
72
+ # rubocop:enable Style/ParameterLists
73
73
 
74
74
  # @private
75
75
  def expected_args
@@ -110,7 +110,7 @@ module RSpec
110
110
  end
111
111
 
112
112
  values.unshift(first_value)
113
- @expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 and @at_least)
113
+ @expected_received_count = [@expected_received_count, values.size].max unless ignoring_args? || (@expected_received_count == 0 && @at_least)
114
114
  self.terminal_implementation_action = AndReturnImplementation.new(values)
115
115
 
116
116
  nil
@@ -137,11 +137,30 @@ module RSpec
137
137
  # counter.increment
138
138
  # expect(counter.count).to eq(original_count + 1)
139
139
  def and_call_original
140
+ and_wrap_original do |original, *args, &block|
141
+ original.call(*args, &block)
142
+ end
143
+ end
144
+
145
+ # Decorates the stubbed method with the supplied block. The original
146
+ # unmodified method is passed to the block along with any method call
147
+ # arguments so you can delegate to it, whilst still being able to
148
+ # change what args are passed to it and/or change the return value.
149
+ #
150
+ # @note This is only available on partial doubles.
151
+ #
152
+ # @example
153
+ #
154
+ # expect(api).to receive(:large_list).and_wrap_original do |original_method, *args, &block|
155
+ # original_method.call(*args, &block).first(10)
156
+ # end
157
+ #
158
+ def and_wrap_original(&block)
140
159
  if RSpec::Mocks::TestDouble === @method_double.object
141
160
  @error_generator.raise_only_valid_on_a_partial_double(:and_call_original)
142
161
  else
143
162
  warn_about_stub_override if implementation.inner_action
144
- @implementation = AndCallOriginalImplementation.new(@method_double.original_method)
163
+ @implementation = AndWrapOriginalImplementation.new(@method_double.original_method, block)
145
164
  @yield_receiver_to_implementation_block = false
146
165
  end
147
166
  end
@@ -167,7 +186,7 @@ module RSpec
167
186
  # allow(car).to receive(:go).and_raise(OutOfGas)
168
187
  # allow(car).to receive(:go).and_raise(OutOfGas, "At least 2 oz of gas needed to drive")
169
188
  # allow(car).to receive(:go).and_raise(OutOfGas.new(2, :oz))
170
- def and_raise(exception = RuntimeError, message = nil)
189
+ def and_raise(exception=RuntimeError, message=nil)
171
190
  if exception.respond_to?(:exception)
172
191
  exception = message ? exception.exception(message) : exception.exception
173
192
  end
@@ -234,7 +253,7 @@ module RSpec
234
253
 
235
254
  # @private
236
255
  def matches_name_but_not_args(message, *args)
237
- @message == message and not @argument_list_matcher.args_match?(*args)
256
+ @message == message && !@argument_list_matcher.args_match?(*args)
238
257
  end
239
258
 
240
259
  # @private
@@ -296,7 +315,7 @@ module RSpec
296
315
  def expectation_count_type
297
316
  return :at_least if @at_least
298
317
  return :at_most if @at_most
299
- return nil
318
+ nil
300
319
  end
301
320
 
302
321
  # @private
@@ -335,7 +354,7 @@ module RSpec
335
354
  def with(*args, &block)
336
355
  if args.empty?
337
356
  raise ArgumentError,
338
- "`with` must have at least one argument. Use `no_args` matcher to set the expectation of receiving no arguments."
357
+ "`with` must have at least one argument. Use `no_args` matcher to set the expectation of receiving no arguments."
339
358
  end
340
359
 
341
360
  self.inner_implementation_action = block
@@ -430,6 +449,17 @@ module RSpec
430
449
  self
431
450
  end
432
451
 
452
+ # Expect a message to be received exactly three times.
453
+ #
454
+ # @example
455
+ #
456
+ # expect(car).to receive(:go).thrice
457
+ def thrice(&block)
458
+ self.inner_implementation_action = block
459
+ set_expected_received_count :exactly, 3
460
+ self
461
+ end
462
+
433
463
  # Expect messages to be received in a specific order.
434
464
  #
435
465
  # @example
@@ -452,7 +482,6 @@ module RSpec
452
482
  @expected_received_count - 1
453
483
  end
454
484
 
455
-
456
485
  # @private
457
486
  def ordered?
458
487
  @ordered
@@ -476,15 +505,13 @@ module RSpec
476
505
  private
477
506
 
478
507
  def invoke_incrementing_actual_calls_by(increment, parent_stub, *args, &block)
479
- if yield_receiver_to_implementation_block?
480
- args.unshift(orig_object)
481
- end
508
+ args.unshift(orig_object) if yield_receiver_to_implementation_block?
482
509
 
483
510
  if negative? || ((@exactly || @at_most) && (@actual_received_count == @expected_received_count))
484
511
  @actual_received_count += increment
485
512
  @failed_fast = true
486
- #args are the args we actually received, @argument_list_matcher is the
487
- #list of args we were expecting
513
+ # args are the args we actually received, @argument_list_matcher is the
514
+ # list of args we were expecting
488
515
  @error_generator.raise_expectation_error(@message, @expected_received_count, @argument_list_matcher, @actual_received_count, expectation_count_type, *args)
489
516
  end
490
517
 
@@ -513,6 +540,7 @@ module RSpec
513
540
  when Numeric then n
514
541
  when :once then 1
515
542
  when :twice then 2
543
+ when :thrice then 3
516
544
  end
517
545
  end
518
546
 
@@ -532,7 +560,7 @@ module RSpec
532
560
 
533
561
  def warn_about_stub_override
534
562
  RSpec.warning(
535
- "You're overriding a previous stub implementation of `#{@message}`. " +
563
+ "You're overriding a previous stub implementation of `#{@message}`. " \
536
564
  "Called from #{CallerFilter.first_non_rspec_line}."
537
565
  )
538
566
  end
@@ -547,7 +575,7 @@ module RSpec
547
575
  @error_generator = error_generator
548
576
  end
549
577
 
550
- def call(*args_to_ignore, &block)
578
+ def call(*_args_to_ignore, &block)
551
579
  return if @args_to_yield.empty? && @eval_context.nil?
552
580
 
553
581
  @error_generator.raise_missing_block_error @args_to_yield unless block
@@ -555,7 +583,7 @@ module RSpec
555
583
  block_signature = Support::BlockSignature.new(block)
556
584
 
557
585
  @args_to_yield.each do |args|
558
- unless Support::MethodSignatureVerifier.new(block_signature, args).valid?
586
+ unless Support::StrictSignatureVerifier.new(block_signature, args).valid?
559
587
  @error_generator.raise_wrong_arity_error(args, block_signature)
560
588
  end
561
589
 
@@ -572,7 +600,7 @@ module RSpec
572
600
  @values_to_return = values_to_return
573
601
  end
574
602
 
575
- def call(*args_to_ignore, &block)
603
+ def call(*_args_to_ignore, &_block)
576
604
  if @values_to_return.size > 1
577
605
  @values_to_return.shift
578
606
  else
@@ -606,22 +634,23 @@ module RSpec
606
634
 
607
635
  # Represents an `and_call_original` implementation.
608
636
  # @private
609
- class AndCallOriginalImplementation
610
- def initialize(method)
637
+ class AndWrapOriginalImplementation
638
+ def initialize(method, block)
611
639
  @method = method
640
+ @block = block
612
641
  end
613
642
 
614
643
  CannotModifyFurtherError = Class.new(StandardError)
615
644
 
616
- def initial_action=(value)
645
+ def initial_action=(_value)
617
646
  raise cannot_modify_further_error
618
647
  end
619
648
 
620
- def inner_action=(value)
649
+ def inner_action=(_value)
621
650
  raise cannot_modify_further_error
622
651
  end
623
652
 
624
- def terminal_action=(value)
653
+ def terminal_action=(_value)
625
654
  raise cannot_modify_further_error
626
655
  end
627
656
 
@@ -634,13 +663,13 @@ module RSpec
634
663
  end
635
664
 
636
665
  def call(*args, &block)
637
- @method.call(*args, &block)
666
+ @block.call(@method, *args, &block)
638
667
  end
639
668
 
640
669
  private
641
670
 
642
671
  def cannot_modify_further_error
643
- CannotModifyFurtherError.new "This method has already been configured " +
672
+ CannotModifyFurtherError.new "This method has already been configured " \
644
673
  "to call the original implementation, and cannot be modified further."
645
674
  end
646
675
  end
@@ -653,9 +682,8 @@ module RSpec
653
682
  yield
654
683
  rescue RSpec::Mocks::MockExpectationError => error
655
684
  error.backtrace.insert(0, location)
656
- Kernel::raise error
685
+ Kernel.raise error
657
686
  end
658
687
  end
659
-
660
688
  end
661
689
  end