-
-
Notifications
You must be signed in to change notification settings - Fork 156
/
mock.rb
364 lines (339 loc) · 14.1 KB
/
mock.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
require 'metaclass'
require 'mocha/expectation'
require 'mocha/expectation_list'
require 'mocha/names'
require 'mocha/receivers'
require 'mocha/method_matcher'
require 'mocha/parameters_matcher'
require 'mocha/unexpected_invocation'
require 'mocha/argument_iterator'
require 'mocha/expectation_error_factory'
require 'mocha/deprecation'
require 'mocha/ruby_version'
module Mocha
# Traditional mock object.
#
# All methods return an {Expectation} which can be further modified by
# methods on {Expectation}.
#
# Stubs and expectations are basically the same thing. A stub is just an
# expectation of zero or more invocations. The {#stubs} method is syntactic
# sugar to make the intent of the test more explicit.
#
# When a method is invoked on a mock object, the mock object searches through
# its expectations from newest to oldest to find one that matches the
# invocation. After the invocation, the matching expectation might stop
# matching further invocations. For example, an +expects(:foo).once+
# expectation only matches once and will be ignored on future invocations
# while an +expects(:foo).at_least_once+ expectation will always be matched
# against invocations.
#
# This scheme allows you to:
#
# - Set up default stubs in your the +setup+ method of your test class and
# override some of those stubs in individual tests.
# - Set up different +once+ expectations for the same method with different
# action per invocation. However, it's better to use the
# {Expectation#returns} method with multiple arguments to do this, as
# described below.
#
# However, there are some possible "gotchas" caused by this scheme:
#
# - if you create an expectation and then a stub for the same method, the
# stub will always override the expectation and the expectation will never
# be met.
# - if you create a stub and then an expectation for the same method, the
# expectation will match, and when it stops matching the stub will be used
# instead, possibly masking test failures.
# - if you create different expectations for the same method, they will be
# invoked in the opposite order than that in which they were specified,
# rather than the same order.
#
# The best thing to do is not set up multiple expectations and stubs for the
# same method with exactly the same matchers. Instead, use the
# {Expectation#returns} method with multiple arguments to create multiple
# actions for a method. You can also chain multiple calls to
# {Expectation#returns} and {Expectation#raises} (along with syntactic sugar
# {Expectation#then} if desired).
#
# @example
# object = mock()
# object.stubs(:expected_method).returns(1, 2).then.raises(Exception)
# object.expected_method # => 1
# object.expected_method # => 2
# object.expected_method # => raises exception of class Exception1
#
# If you want to specify more complex ordering or order invocations across
# different mock objects, use the {Expectation#in_sequence} method to
# explicitly define a total or partial ordering of invocations.
class Mock
# Adds an expectation that the specified method must be called exactly once with any parameters.
#
# @param [Symbol,String] method_name name of expected method
# @param [Hash] expected_methods_vs_return_values expected method name symbols as keys and corresponding return values as values - these expectations are setup as if {#expects} were called multiple times.
#
# @overload def expects(method_name)
# @overload def expects(expected_methods_vs_return_values)
# @return [Expectation] last-built expectation which can be further modified by methods on {Expectation}.
#
# @example Expected method invoked once so no error raised
# object = mock()
# object.expects(:expected_method)
# object.expected_method
#
# @example Expected method not invoked so error raised
# object = mock()
# object.expects(:expected_method)
# # error raised when test completes, because expected_method not called exactly once
#
# @example Expected method invoked twice so error raised
# object = mock()
# object.expects(:expected_method)
# object.expected_method
# object.expected_method # => error raised when expected method invoked second time
#
# @example Setup multiple expectations using +expected_methods_vs_return_values+.
# object = mock()
# object.expects(:expected_method_one => :result_one, :expected_method_two => :result_two)
#
# # is exactly equivalent to
#
# object = mock()
# object.expects(:expected_method_one).returns(:result_one)
# object.expects(:expected_method_two).returns(:result_two)
def expects(method_name_or_hash, backtrace = nil)
iterator = ArgumentIterator.new(method_name_or_hash)
iterator.each { |*args|
method_name = args.shift
ensure_method_not_already_defined(method_name)
expectation = Expectation.new(self, method_name, backtrace)
expectation.returns(args.shift) if args.length > 0
@expectations.add(expectation)
}
end
# Adds an expectation that the specified method may be called any number of times with any parameters.
#
# @param [Symbol,String] method_name name of stubbed method
# @param [Hash] stubbed_methods_vs_return_values stubbed method name symbols as keys and corresponding return values as values - these stubbed methods are setup as if {#stubs} were called multiple times.
#
# @overload def stubs(method_name)
# @overload def stubs(stubbed_methods_vs_return_values)
# @return [Expectation] last-built expectation which can be further modified by methods on {Expectation}.
#
# @example No error raised however many times stubbed method is invoked
# object = mock()
# object.stubs(:stubbed_method)
# object.stubbed_method
# object.stubbed_method
# # no error raised
#
# @example Setup multiple expectations using +stubbed_methods_vs_return_values+.
# object = mock()
# object.stubs(:stubbed_method_one => :result_one, :stubbed_method_two => :result_two)
#
# # is exactly equivalent to
#
# object = mock()
# object.stubs(:stubbed_method_one).returns(:result_one)
# object.stubs(:stubbed_method_two).returns(:result_two)
def stubs(method_name_or_hash, backtrace = nil)
iterator = ArgumentIterator.new(method_name_or_hash)
iterator.each { |*args|
method_name = args.shift
ensure_method_not_already_defined(method_name)
expectation = Expectation.new(self, method_name, backtrace)
expectation.at_least(0)
expectation.returns(args.shift) if args.length > 0
@expectations.add(expectation)
}
end
# Removes the specified stubbed method (added by calls to {#expects} or {#stubs}) and all expectations associated with it.
#
# @param [Symbol] method_name name of method to unstub.
#
# @example Invoking an unstubbed method causes error to be raised
# object = mock('mock') do
# object.stubs(:stubbed_method).returns(:result_one)
# object.stubbed_method # => :result_one
# object.unstub(:stubbed_method)
# object.stubbed_method # => unexpected invocation: #<Mock:mock>.stubbed_method()
def unstub(method_name)
@expectations.remove_all_matching_method(method_name)
end
# Constrains the {Mock} instance so that it can only expect or stub methods to which +responder+ responds. The constraint is only applied at method invocation time.
#
# A +NoMethodError+ will be raised if the +responder+ does not +#respond_to?+ a method invocation (even if the method has been expected or stubbed).
#
# The {Mock} instance will delegate its +#respond_to?+ method to the +responder+.
#
# Note that the methods on +responder+ are never actually invoked.
#
# @param [Object, #respond_to?] responder an object used to determine whether {Mock} instance should +#respond_to?+ to an invocation.
# @return [Mock] the same {Mock} instance, thereby allowing invocations of other {Mock} methods to be chained.
# @see #responds_like_instance_of
#
# @example Normal mocking
# sheep = mock('sheep')
# sheep.expects(:chew)
# sheep.expects(:foo)
# sheep.respond_to?(:chew) # => true
# sheep.respond_to?(:foo) # => true
# sheep.chew
# sheep.foo
# # no error raised
#
# @example Using {#responds_like} with an instance method
# class Sheep
# def chew(grass); end
# end
#
# sheep = mock('sheep')
# sheep.responds_like(Sheep.new)
# sheep.expects(:chew)
# sheep.expects(:foo)
# sheep.respond_to?(:chew) # => true
# sheep.respond_to?(:foo) # => false
# sheep.chew
# sheep.foo # => raises NoMethodError exception
#
# @example Using {#responds_like} with a class method
# class Sheep
# def self.number_of_legs; end
# end
#
# sheep_class = mock('sheep_class')
# sheep_class.responds_like(Sheep)
# sheep_class.stubs(:number_of_legs).returns(4)
# sheep_class.expects(:foo)
# sheep_class.respond_to?(:number_of_legs) # => true
# sheep_class.respond_to?(:foo) # => false
# sheep_class.number_of_legs # => 4
# sheep_class.foo # => raises NoMethodError exception
def responds_like(responder)
@responder = responder
self
end
# Constrains the {Mock} instance so that it can only expect or stub methods to which an instance of the +responder_class+ responds. The constraint is only applied at method invocation time. Note that the responder instance is instantiated using +Class#allocate+.
#
# A +NoMethodError+ will be raised if the responder instance does not +#respond_to?+ a method invocation (even if the method has been expected or stubbed).
#
# The {Mock} instance will delegate its +#respond_to?+ method to the responder instance.
#
# Note that the methods on the responder instance are never actually invoked.
#
# @param [Class] responder_class a class used to determine whether {Mock} instance should +#respond_to?+ to an invocation.
# @return [Mock] the same {Mock} instance, thereby allowing invocations of other {Mock} methods to be chained.
# @see #responds_like
#
# @example Using {#responds_like_instance_of}
# class Sheep
# def initialize
# raise "some awkward code we don't want to call"
# end
# def chew(grass); end
# end
#
# sheep = mock('sheep')
# sheep.responds_like_instance_of(Sheep)
# sheep.expects(:chew)
# sheep.expects(:foo)
# sheep.respond_to?(:chew) # => true
# sheep.respond_to?(:foo) # => false
# sheep.chew
# sheep.foo # => raises NoMethodError exception
def responds_like_instance_of(responder_class)
responds_like(responder_class.allocate)
end
# @private
def initialize(mockery, name = nil, receiver = nil, &block)
@mockery = mockery
@name = name || DefaultName.new(self)
@receiver = receiver || DefaultReceiver.new(self)
@expectations = ExpectationList.new
@everything_stubbed = false
@responder = nil
@unexpected_invocation = nil
if block
Deprecation.warning('Passing a block is deprecated. Use Object#tap or define stubs/expectations with an explicit receiver instead.')
instance_eval(&block)
end
end
# @private
attr_reader :everything_stubbed
alias_method :__expects__, :expects
alias_method :__stubs__, :stubs
alias_method :quacks_like, :responds_like
alias_method :quacks_like_instance_of, :responds_like_instance_of
# @private
def __expectations__
@expectations
end
# @private
def stub_everything
@everything_stubbed = true
end
# @private
def all_expectations
@receiver.mocks.inject(ExpectationList.new) { |e, m| e + m.__expectations__ }
end
# @private
def method_missing(symbol, *arguments, &block)
if @responder and not @responder.respond_to?(symbol)
raise NoMethodError, "undefined method `#{symbol}' for #{self.mocha_inspect} which responds like #{@responder.mocha_inspect}"
end
if matching_expectation_allowing_invocation = all_expectations.match_allowing_invocation(symbol, *arguments)
matching_expectation_allowing_invocation.invoke(&block)
else
if (matching_expectation = all_expectations.match(symbol, *arguments)) || (!matching_expectation && !@everything_stubbed)
if @unexpected_invocation.nil?
@unexpected_invocation = UnexpectedInvocation.new(self, symbol, *arguments)
matching_expectation.invoke(&block) if matching_expectation
message = @unexpected_invocation.full_description
message << @mockery.mocha_inspect
else
message = @unexpected_invocation.short_description
end
raise ExpectationErrorFactory.build(message, caller)
end
end
end
# @private
def respond_to_missing?(symbol, include_private = false)
if @responder then
if @responder.method(:respond_to?).arity > 1
@responder.respond_to?(symbol, include_private)
else
@responder.respond_to?(symbol)
end
else
@everything_stubbed || all_expectations.matches_method?(symbol)
end
end
# @private
if PRE_RUBY_V19
def respond_to?(symbol, include_private = false)
respond_to_missing?(symbol, include_private)
end
end
# @private
def __verified__?(assertion_counter = nil)
@expectations.verified?(assertion_counter)
end
# @private
def mocha_inspect
@name.mocha_inspect
end
# @private
def inspect
mocha_inspect
end
# @private
def ensure_method_not_already_defined(method_name)
self.__metaclass__.send(:undef_method, method_name) if self.__metaclass__.method_defined?(method_name) || self.__metaclass__.private_method_defined?(method_name)
end
# @private
def any_expectations?
@expectations.any?
end
end
end