spy 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.travis.yml +5 -0
- data/Gemfile +1 -0
- data/README.md +22 -7
- data/Rakefile +2 -0
- data/lib/spy.rb +39 -6
- data/lib/spy/agency.rb +42 -27
- data/lib/spy/call_log.rb +26 -0
- data/lib/spy/constant.rb +72 -14
- data/lib/spy/core_ext/marshal.rb +1 -0
- data/lib/spy/double.rb +17 -0
- data/lib/spy/nest.rb +27 -0
- data/lib/spy/subroutine.rb +146 -44
- data/lib/spy/version.rb +1 -1
- data/spec/spy/any_instance_spec.rb +518 -0
- data/spec/spy/mock_spec.rb +46 -554
- data/spec/spy/mutate_const_spec.rb +21 -63
- data/spec/spy/null_object_mock_spec.rb +11 -39
- data/spec/spy/partial_mock_spec.rb +3 -62
- data/spec/spy/stash_spec.rb +30 -37
- data/spec/spy/stub_spec.rb +0 -6
- data/spec/spy/to_ary_spec.rb +5 -5
- data/test/integration/test_constant_spying.rb +1 -1
- data/test/integration/test_instance_method.rb +32 -0
- data/test/integration/test_subroutine_spying.rb +7 -4
- data/test/spy/test_double.rb +4 -0
- data/test/spy/test_subroutine.rb +28 -3
- data/test/support/pen.rb +15 -0
- metadata +8 -30
- data/spec/spy/bug_report_10260_spec.rb +0 -8
- data/spec/spy/bug_report_10263_spec.rb +0 -24
- data/spec/spy/bug_report_496_spec.rb +0 -18
- data/spec/spy/bug_report_600_spec.rb +0 -24
- data/spec/spy/bug_report_7611_spec.rb +0 -16
- data/spec/spy/bug_report_8165_spec.rb +0 -31
- data/spec/spy/bug_report_830_spec.rb +0 -21
- data/spec/spy/bug_report_957_spec.rb +0 -22
- data/spec/spy/double_spec.rb +0 -12
- data/spec/spy/failing_argument_matchers_spec.rb +0 -94
- data/spec/spy/options_hash_spec.rb +0 -35
- data/spec/spy/precise_counts_spec.rb +0 -68
- data/spec/spy/stubbed_message_expectations_spec.rb +0 -47
- data/spec/spy/test_double_spec.rb +0 -54
data/lib/spy/core_ext/marshal.rb
CHANGED
data/lib/spy/double.rb
CHANGED
@@ -7,5 +7,22 @@ module Spy
|
|
7
7
|
Spy.on(self,*args)
|
8
8
|
end
|
9
9
|
end
|
10
|
+
|
11
|
+
# @private
|
12
|
+
def ==(other)
|
13
|
+
other == self
|
14
|
+
end
|
15
|
+
|
16
|
+
# @private
|
17
|
+
def inspect
|
18
|
+
"#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>"
|
19
|
+
end
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def to_s
|
23
|
+
inspect.gsub('<','[').gsub('>',']')
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method :to_str, :to_s
|
10
27
|
end
|
11
28
|
end
|
data/lib/spy/nest.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
module Spy
|
2
|
+
# This class manages all the Constant Mutations for a given Module
|
2
3
|
class Nest
|
4
|
+
|
5
|
+
# @!attribute [r] base_module
|
6
|
+
# @return [Module] The module that the Nest is managing
|
7
|
+
#
|
8
|
+
# @!attribute [r] hooked_constants
|
9
|
+
# @return [Hash<Symbol, Constant>] The module that the Nest is managing
|
10
|
+
|
11
|
+
|
3
12
|
attr_reader :base_module, :hooked_constants
|
4
13
|
|
5
14
|
def initialize(base_module)
|
@@ -8,6 +17,9 @@ module Spy
|
|
8
17
|
@hooked_constants = {}
|
9
18
|
end
|
10
19
|
|
20
|
+
# records that the spy is hooked
|
21
|
+
# @param spy [Constant]
|
22
|
+
# @return [self]
|
11
23
|
def add(spy)
|
12
24
|
if @hooked_constants[spy.constant_name]
|
13
25
|
raise "#{spy.constant_name} has already been stubbed"
|
@@ -17,6 +29,9 @@ module Spy
|
|
17
29
|
self
|
18
30
|
end
|
19
31
|
|
32
|
+
# removes the spy from the records
|
33
|
+
# @param spy [Constant]
|
34
|
+
# @return [self]
|
20
35
|
def remove(spy)
|
21
36
|
if @hooked_constants[spy.constant_name] == spy
|
22
37
|
@hooked_constants.delete(spy.constant_name)
|
@@ -24,19 +39,31 @@ module Spy
|
|
24
39
|
self
|
25
40
|
end
|
26
41
|
|
42
|
+
# checks to see if a given constant is hooked
|
43
|
+
# @param constant_name [Symbol]
|
44
|
+
# @return [Boolean]
|
27
45
|
def hooked?(constant_name)
|
28
46
|
!!@hooked_constants[constant_name]
|
29
47
|
end
|
30
48
|
|
31
49
|
class << self
|
50
|
+
# retrieves the nest for a given module
|
51
|
+
# @param base_module [Module]
|
52
|
+
# @return [Nil, Nest]
|
32
53
|
def get(base_module)
|
33
54
|
all[base_module.name]
|
34
55
|
end
|
35
56
|
|
57
|
+
|
58
|
+
# retrieves the nest for a given module or creates it
|
59
|
+
# @param base_module [Module]
|
60
|
+
# @return [Nest]
|
36
61
|
def fetch(base_module)
|
37
62
|
all[base_module.name] ||= self.new(base_module)
|
38
63
|
end
|
39
64
|
|
65
|
+
# returns all the hooked constants
|
66
|
+
# @return [Hash<String, Constant>]
|
40
67
|
def all
|
41
68
|
@all ||= {}
|
42
69
|
end
|
data/lib/spy/subroutine.rb
CHANGED
@@ -1,55 +1,69 @@
|
|
1
1
|
module Spy
|
2
2
|
class Subroutine
|
3
|
-
|
4
|
-
|
3
|
+
# @!attribute [r] base_object
|
4
|
+
# @return [Object] the object that is being watched
|
5
|
+
#
|
6
|
+
# @!attribute [r] method_name
|
7
|
+
# @return [Symbol] the name of the method that is being watched
|
8
|
+
#
|
9
|
+
# @!attribute [r] calls
|
10
|
+
# @return [Array<CallLog>] the messages that have been sent to the method
|
11
|
+
#
|
12
|
+
# @!attribute [r] original_method
|
13
|
+
# @return [Method] the original method that was hooked if it existed
|
14
|
+
#
|
15
|
+
# @!attribute [r] hook_opts
|
16
|
+
# @return [Hash] the options that were sent when it was hooked
|
17
|
+
|
18
|
+
|
19
|
+
attr_reader :base_object, :method_name, :calls, :original_method, :hook_opts
|
5
20
|
|
6
21
|
# set what object and method the spy should watch
|
7
22
|
# @param object
|
8
23
|
# @param method_name <Symbol>
|
9
|
-
|
10
|
-
|
24
|
+
# @param singleton_method <Boolean> spy on the singleton method or the normal method
|
25
|
+
def initialize(object, method_name, singleton_method = true)
|
11
26
|
@base_object, @method_name = object, method_name
|
27
|
+
@singleton_method = singleton_method
|
12
28
|
reset!
|
13
29
|
end
|
14
30
|
|
15
31
|
# hooks the method into the object and stashes original method if it exists
|
16
|
-
# @param
|
17
|
-
# @
|
32
|
+
# @param [Hash] opts what do do when hooking into a method
|
33
|
+
# @option opts [Boolean] force (false) if set to true will hook the method even if it doesn't exist
|
34
|
+
# @option opts [Symbol<:public, :protected, :private>] visibility overrides visibility with whatever method is given
|
35
|
+
# @return [self]
|
18
36
|
def hook(opts = {})
|
19
|
-
@
|
37
|
+
@hook_opts = opts
|
20
38
|
raise "#{base_object} method '#{method_name}' has already been hooked" if hooked?
|
21
|
-
|
22
|
-
|
23
|
-
|
39
|
+
|
40
|
+
hook_opts[:force] ||= base_object.is_a?(Double)
|
41
|
+
if (base_object_respond_to?(method_name, true)) || !hook_opts[:force]
|
42
|
+
@original_method = current_method
|
24
43
|
end
|
44
|
+
hook_opts[:visibility] ||= method_visibility
|
25
45
|
|
26
|
-
|
46
|
+
base_object.send(define_method_with, method_name, override_method)
|
27
47
|
|
28
|
-
|
29
|
-
|
30
|
-
if __spy_args.first === SECRET_SPY_KEY
|
31
|
-
__method_spy__
|
32
|
-
else
|
33
|
-
__method_spy__.invoke(self,__spy_args,block)
|
34
|
-
end
|
48
|
+
if [:public, :protected, :private].include? hook_opts[:visibility]
|
49
|
+
method_owner.send(hook_opts[:visibility], method_name)
|
35
50
|
end
|
36
51
|
|
37
|
-
base_object.singleton_class.send(opts[:visibility], method_name) if opts[:visibility]
|
38
|
-
|
39
52
|
Agency.instance.recruit(self)
|
40
53
|
@was_hooked = true
|
41
54
|
self
|
42
55
|
end
|
43
56
|
|
44
57
|
# unhooks method from object
|
45
|
-
# @return self
|
58
|
+
# @return [self]
|
46
59
|
def unhook
|
47
|
-
raise "#{method_name} method has not been hooked" unless hooked?
|
48
|
-
|
49
|
-
|
50
|
-
|
60
|
+
raise "'#{method_name}' method has not been hooked" unless hooked?
|
61
|
+
|
62
|
+
if original_method && method_owner == original_method.owner
|
63
|
+
original_method.owner.send(:define_method, method_name, original_method)
|
64
|
+
original_method.owner.send(method_visibility, method_name) if method_visibility
|
51
65
|
else
|
52
|
-
|
66
|
+
method_owner.send(:remove_method, method_name)
|
53
67
|
end
|
54
68
|
clear_method!
|
55
69
|
Agency.instance.retire(self)
|
@@ -57,9 +71,9 @@ module Spy
|
|
57
71
|
end
|
58
72
|
|
59
73
|
# is the spy hooked?
|
60
|
-
# @return Boolean
|
74
|
+
# @return [Boolean]
|
61
75
|
def hooked?
|
62
|
-
self == self.class.get(base_object, method_name)
|
76
|
+
self == self.class.get(base_object, method_name, @singleton_method)
|
63
77
|
end
|
64
78
|
|
65
79
|
# @overload and_return(value)
|
@@ -67,7 +81,7 @@ module Spy
|
|
67
81
|
#
|
68
82
|
# Tells the spy to return a value when the method is called.
|
69
83
|
#
|
70
|
-
# @return self
|
84
|
+
# @return [self]
|
71
85
|
def and_return(value = nil)
|
72
86
|
if block_given?
|
73
87
|
@plan = Proc.new
|
@@ -88,6 +102,7 @@ module Spy
|
|
88
102
|
end
|
89
103
|
|
90
104
|
# Tells the object to yield one or more args to a block when the message is received.
|
105
|
+
# @return [self]
|
91
106
|
def and_yield(*args)
|
92
107
|
yield eval_context = Object.new if block_given?
|
93
108
|
@plan = Proc.new do |&block|
|
@@ -97,7 +112,7 @@ module Spy
|
|
97
112
|
end
|
98
113
|
|
99
114
|
# tells the spy to call the original method
|
100
|
-
# @return self
|
115
|
+
# @return [self]
|
101
116
|
def and_call_through
|
102
117
|
raise "can only call through if original method is set" unless method_visibility
|
103
118
|
if original_method
|
@@ -110,6 +125,22 @@ module Spy
|
|
110
125
|
self
|
111
126
|
end
|
112
127
|
|
128
|
+
# @overload and_raise
|
129
|
+
# @overload and_raise(ExceptionClass)
|
130
|
+
# @overload and_raise(ExceptionClass, message)
|
131
|
+
# @overload and_raise(exception_instance)
|
132
|
+
#
|
133
|
+
# Tells the object to raise an exception when the message is received.
|
134
|
+
#
|
135
|
+
# @note
|
136
|
+
#
|
137
|
+
# When you pass an exception class, the MessageExpectation will raise
|
138
|
+
# an instance of it, creating it with `exception` and passing `message`
|
139
|
+
# if specified. If the exception class initializer requires more than
|
140
|
+
# one parameters, you must pass in an instance and not the class,
|
141
|
+
# otherwise this method will raise an ArgumentError exception.
|
142
|
+
#
|
143
|
+
# @return [self]
|
113
144
|
def and_raise(exception = RuntimeError, message = nil)
|
114
145
|
if exception.respond_to?(:exception)
|
115
146
|
exception = message ? exception.exception(message) : exception.exception
|
@@ -118,17 +149,28 @@ module Spy
|
|
118
149
|
@plan = Proc.new { raise exception }
|
119
150
|
end
|
120
151
|
|
152
|
+
# @overload and_throw(symbol)
|
153
|
+
# @overload and_throw(symbol, object)
|
154
|
+
#
|
155
|
+
# Tells the object to throw a symbol (with the object if that form is
|
156
|
+
# used) when the message is received.
|
157
|
+
#
|
158
|
+
# @return [self]
|
121
159
|
def and_throw(*args)
|
122
160
|
@plan = Proc.new { throw(*args) }
|
123
161
|
self
|
124
162
|
end
|
125
163
|
|
164
|
+
# if the method was called it will return true
|
165
|
+
# @return [Boolean]
|
126
166
|
def has_been_called?
|
127
167
|
raise "was never hooked" unless @was_hooked
|
128
168
|
calls.size > 0
|
129
169
|
end
|
130
170
|
|
131
171
|
# check if the method was called with the exact arguments
|
172
|
+
# @param args Arguments that should have been sent to the method
|
173
|
+
# @return [Boolean]
|
132
174
|
def has_been_called_with?(*args)
|
133
175
|
raise "was never hooked" unless @was_hooked
|
134
176
|
calls.any? do |call_log|
|
@@ -138,15 +180,16 @@ module Spy
|
|
138
180
|
|
139
181
|
# invoke that the method has been called. You really shouldn't use this
|
140
182
|
# method.
|
141
|
-
def invoke(object, args, block)
|
183
|
+
def invoke(object, args, block, called_from)
|
142
184
|
check_arity!(args.size)
|
143
185
|
result = @plan ? @plan.call(*args, &block) : nil
|
144
|
-
|
145
|
-
result
|
186
|
+
ensure
|
187
|
+
calls << CallLog.new(object,called_from, args, block, result)
|
146
188
|
end
|
147
189
|
|
148
190
|
# reset the call log
|
149
191
|
def reset!
|
192
|
+
@was_hooked = false
|
150
193
|
@calls = []
|
151
194
|
clear_method!
|
152
195
|
true
|
@@ -154,6 +197,15 @@ module Spy
|
|
154
197
|
|
155
198
|
private
|
156
199
|
|
200
|
+
def override_method
|
201
|
+
eval <<-METHOD, binding, __FILE__, __LINE__ + 1
|
202
|
+
__method_spy__ = self
|
203
|
+
lambda do |*__spy_args_#{self.object_id}, &block|
|
204
|
+
__method_spy__.invoke(self, __spy_args_#{self.object_id}, block, caller(1)[0])
|
205
|
+
end
|
206
|
+
METHOD
|
207
|
+
end
|
208
|
+
|
157
209
|
def call_with_yield(&block)
|
158
210
|
raise "no block sent" unless block
|
159
211
|
value = nil
|
@@ -168,22 +220,36 @@ module Spy
|
|
168
220
|
|
169
221
|
def clear_method!
|
170
222
|
@hooked = false
|
171
|
-
@
|
223
|
+
@hook_opts = @original_method = @arity_range = @method_visibility = @method_owner= nil
|
172
224
|
end
|
173
225
|
|
174
226
|
def method_visibility
|
175
227
|
@method_visibility ||=
|
176
|
-
if
|
228
|
+
if base_object_respond_to?(method_name)
|
177
229
|
if original_method && original_method.owner.protected_method_defined?(method_name)
|
178
230
|
:protected
|
179
231
|
else
|
180
232
|
:public
|
181
233
|
end
|
182
|
-
elsif
|
234
|
+
elsif base_object_respond_to?(method_name, true)
|
183
235
|
:private
|
184
236
|
end
|
185
237
|
end
|
186
238
|
|
239
|
+
def base_object_respond_to?(method_name, include_private = false)
|
240
|
+
if @singleton_method
|
241
|
+
base_object.respond_to?(method_name, include_private)
|
242
|
+
else
|
243
|
+
base_object.instance_methods.include?(method_name) || (
|
244
|
+
include_private && base_object.private_instance_methods.include?(method_name)
|
245
|
+
)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def define_method_with
|
250
|
+
@singleton_method ? :define_singleton_method : :define_method
|
251
|
+
end
|
252
|
+
|
187
253
|
def check_arity!(arity)
|
188
254
|
self.class.check_arity_against_range!(arity_range, arity)
|
189
255
|
end
|
@@ -192,7 +258,16 @@ module Spy
|
|
192
258
|
@arity_range ||= self.class.arity_range_of(original_method) if original_method
|
193
259
|
end
|
194
260
|
|
261
|
+
def current_method
|
262
|
+
@singleton_method ? base_object.method(method_name) : base_object.instance_method(method_name)
|
263
|
+
end
|
264
|
+
|
265
|
+
def method_owner
|
266
|
+
@method_owner ||= current_method.owner
|
267
|
+
end
|
268
|
+
|
195
269
|
class << self
|
270
|
+
# @private
|
196
271
|
def arity_range_of(block)
|
197
272
|
raise "#{block.inspect} does not respond to :parameters" unless block.respond_to?(:parameters)
|
198
273
|
min = max = 0
|
@@ -210,6 +285,7 @@ module Spy
|
|
210
285
|
(min..max)
|
211
286
|
end
|
212
287
|
|
288
|
+
# @private
|
213
289
|
def check_arity_against_range!(arity_range, arity)
|
214
290
|
return unless arity_range
|
215
291
|
if arity < arity_range.min
|
@@ -219,21 +295,47 @@ module Spy
|
|
219
295
|
end
|
220
296
|
end
|
221
297
|
|
222
|
-
|
298
|
+
# retrieve the method spy from an object
|
299
|
+
# @param base_object
|
300
|
+
# @param method_name [Symbol]
|
301
|
+
# @param singleton_method [Boolean] this a singleton method or a instance method?
|
302
|
+
# @return [Array<Subroutine>]
|
303
|
+
def get(base_object, method_name, singleton_method = true)
|
304
|
+
if singleton_method
|
305
|
+
if base_object.respond_to?(method_name, true)
|
306
|
+
spied_method = base_object.method(method_name)
|
307
|
+
end
|
308
|
+
elsif (base_object.instance_methods + base_object.private_instance_methods).include?(method_name)
|
309
|
+
spied_method = base_object.instance_method(method_name)
|
310
|
+
end
|
223
311
|
|
224
|
-
|
225
|
-
|
226
|
-
base_object.send(method_name, SECRET_SPY_KEY)
|
312
|
+
if spied_method
|
313
|
+
Agency.instance.find(get_spy_id(spied_method))
|
227
314
|
end
|
228
315
|
end
|
229
316
|
|
317
|
+
# retrieve all the spies from a given object
|
318
|
+
# @param base_object
|
319
|
+
# @return [Array<Subroutine>]
|
230
320
|
def get_spies(base_object)
|
231
|
-
base_object.
|
232
|
-
|
233
|
-
|
234
|
-
|
321
|
+
all_methods = base_object.public_methods(false) +
|
322
|
+
base_object.protected_methods(false) +
|
323
|
+
base_object.private_methods(false)
|
324
|
+
all_methods += base_object.instance_methods(false) + base_object.private_instance_methods(false) if base_object.respond_to?(:instance_methods)
|
325
|
+
all_methods.map do |method_name|
|
326
|
+
Agency.instance.find(get_spy_id(base_object.method(method_name)))
|
235
327
|
end.compact
|
236
328
|
end
|
329
|
+
|
330
|
+
private
|
331
|
+
|
332
|
+
def get_spy_id(method)
|
333
|
+
return nil unless method.parameters[0].is_a?(Array)
|
334
|
+
first_param_name = method.parameters[0][1].to_s
|
335
|
+
if first_param_name.include?("__spy_args")
|
336
|
+
first_param_name.split("_").last.to_i
|
337
|
+
end
|
338
|
+
end
|
237
339
|
end
|
238
340
|
end
|
239
341
|
end
|
data/lib/spy/version.rb
CHANGED
@@ -0,0 +1,518 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Spy
|
4
|
+
describe "#any_instance" do
|
5
|
+
class CustomErrorForAnyInstanceSpec < StandardError;end
|
6
|
+
|
7
|
+
let(:klass) do
|
8
|
+
Class.new do
|
9
|
+
def existing_method; :existing_method_return_value; end
|
10
|
+
def existing_method_with_arguments(arg_one, arg_two = nil); :existing_method_with_arguments_return_value; end
|
11
|
+
def another_existing_method; end
|
12
|
+
private
|
13
|
+
def private_method; :private_method_return_value; end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
let(:existing_method_return_value){ :existing_method_return_value }
|
17
|
+
|
18
|
+
context "with #stub" do
|
19
|
+
it "does not suppress an exception when a method that doesn't exist is invoked" do
|
20
|
+
Spy.on_instance_method(klass, :existing_method)
|
21
|
+
expect { klass.new.bar }.to raise_error(NoMethodError)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'multiple methods' do
|
25
|
+
it "allows multiple methods to be stubbed in a single invocation" do
|
26
|
+
Spy.on_instance_method(klass, :existing_method => 'foo', :another_existing_method => 'bar')
|
27
|
+
instance = klass.new
|
28
|
+
expect(instance.existing_method).to eq('foo')
|
29
|
+
expect(instance.another_existing_method).to eq('bar')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "behaves as 'every instance'" do
|
34
|
+
it "stubs every instance in the spec" do
|
35
|
+
Subroutine.new(klass, :foo, false).hook(force: true).and_return(result = Object.new)
|
36
|
+
expect(klass.new.foo).to eq(result)
|
37
|
+
expect(klass.new.foo).to eq(result)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "stubs instance created before any_instance was called" do
|
41
|
+
instance = klass.new
|
42
|
+
Spy.on_instance_method(klass, :existing_method).and_return(result = Object.new)
|
43
|
+
expect(instance.existing_method).to eq(result)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "with #and_return" do
|
48
|
+
it "stubs a method that doesn't exist" do
|
49
|
+
Spy.on_instance_method(klass, :existing_method).and_return(1)
|
50
|
+
expect(klass.new.existing_method).to eq(1)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "stubs a method that exists" do
|
54
|
+
Spy.on_instance_method(klass, :existing_method).and_return(1)
|
55
|
+
expect(klass.new.existing_method).to eq(1)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns the same object for calls on different instances" do
|
59
|
+
return_value = Object.new
|
60
|
+
Spy.on_instance_method(klass, :existing_method).and_return(return_value)
|
61
|
+
expect(klass.new.existing_method).to be(return_value)
|
62
|
+
expect(klass.new.existing_method).to be(return_value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context "with #and_yield" do
|
67
|
+
it "yields the value specified" do
|
68
|
+
yielded_value = Object.new
|
69
|
+
Spy.on_instance_method(klass, :existing_method).and_yield(yielded_value)
|
70
|
+
klass.new.existing_method{|value| expect(value).to be(yielded_value)}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "with #and_raise" do
|
75
|
+
it "stubs a method that doesn't exist" do
|
76
|
+
Spy.on_instance_method(klass, :existing_method).and_raise(CustomErrorForAnyInstanceSpec)
|
77
|
+
expect { klass.new.existing_method}.to raise_error(CustomErrorForAnyInstanceSpec)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "stubs a method that exists" do
|
81
|
+
Spy.on_instance_method(klass, :existing_method).and_raise(CustomErrorForAnyInstanceSpec)
|
82
|
+
expect { klass.new.existing_method}.to raise_error(CustomErrorForAnyInstanceSpec)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "with a block" do
|
87
|
+
it "stubs a method" do
|
88
|
+
Spy.on_instance_method(klass, :existing_method) { 1 }
|
89
|
+
expect(klass.new.existing_method).to eq(1)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "returns the same computed value for calls on different instances" do
|
93
|
+
Spy.on_instance_method(klass, :existing_method) { 1 + 2 }
|
94
|
+
expect(klass.new.existing_method).to eq(klass.new.existing_method)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context "core ruby objects" do
|
99
|
+
it "works uniformly across *everything*" do
|
100
|
+
Object.any_instance.stub(:foo).and_return(1)
|
101
|
+
expect(Object.new.foo).to eq(1)
|
102
|
+
end
|
103
|
+
|
104
|
+
it "works with the non-standard constructor []" do
|
105
|
+
Array.any_instance.stub(:foo).and_return(1)
|
106
|
+
expect([].foo).to eq(1)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "works with the non-standard constructor {}" do
|
110
|
+
Hash.any_instance.stub(:foo).and_return(1)
|
111
|
+
expect({}.foo).to eq(1)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "works with the non-standard constructor \"\"" do
|
115
|
+
String.any_instance.stub(:foo).and_return(1)
|
116
|
+
expect("".foo).to eq(1)
|
117
|
+
end
|
118
|
+
|
119
|
+
it "works with the non-standard constructor \'\'" do
|
120
|
+
String.any_instance.stub(:foo).and_return(1)
|
121
|
+
expect(''.foo).to eq(1)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "works with the non-standard constructor module" do
|
125
|
+
Module.any_instance.stub(:foo).and_return(1)
|
126
|
+
module RSpec::SampleRspecTestModule;end
|
127
|
+
expect(RSpec::SampleRspecTestModule.foo).to eq(1)
|
128
|
+
end
|
129
|
+
|
130
|
+
it "works with the non-standard constructor class" do
|
131
|
+
Class.any_instance.stub(:foo).and_return(1)
|
132
|
+
class RSpec::SampleRspecTestClass;end
|
133
|
+
expect(RSpec::SampleRspecTestClass.foo).to eq(1)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
context "unstub implementation" do
|
139
|
+
it "replaces the stubbed method with the original method" do
|
140
|
+
Spy.on_instance_method(klass, :existing_method)
|
141
|
+
klass.any_instance.unstub(:existing_method)
|
142
|
+
expect(klass.new.existing_method).to eq(:existing_method_return_value)
|
143
|
+
end
|
144
|
+
|
145
|
+
it "removes all stubs with the supplied method name" do
|
146
|
+
Spy.on_instance_method(klass, :existing_method).with(1)
|
147
|
+
Spy.on_instance_method(klass, :existing_method).with(2)
|
148
|
+
klass.any_instance.unstub(:existing_method)
|
149
|
+
expect(klass.new.existing_method).to eq(:existing_method_return_value)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "does not remove any expectations with the same method name" do
|
153
|
+
klass.any_instance.should_receive(:existing_method_with_arguments).with(3).and_return(:three)
|
154
|
+
Spy.on_instance_method(klass, :existing_method_with_arguments).with(1)
|
155
|
+
Spy.on_instance_method(klass, :existing_method_with_arguments).with(2)
|
156
|
+
klass.any_instance.unstub(:existing_method_with_arguments)
|
157
|
+
expect(klass.new.existing_method_with_arguments(3)).to eq(:three)
|
158
|
+
end
|
159
|
+
|
160
|
+
it "raises a MockExpectationError if the method has not been stubbed" do
|
161
|
+
expect {
|
162
|
+
klass.any_instance.unstub(:existing_method)
|
163
|
+
}.to raise_error(RSpec::Mocks::MockExpectationError, 'The method `existing_method` was not stubbed or was already unstubbed')
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "with #should_receive" do
|
168
|
+
let(:foo_expectation_error_message) { 'Exactly one instance should have received the following message(s) but didn\'t: foo' }
|
169
|
+
let(:existing_method_expectation_error_message) { 'Exactly one instance should have received the following message(s) but didn\'t: existing_method' }
|
170
|
+
|
171
|
+
context "with an expectation is set on a method which does not exist" do
|
172
|
+
it "returns the expected value" do
|
173
|
+
klass.any_instance.should_receive(:foo).and_return(1)
|
174
|
+
expect(klass.new.foo(1)).to eq(1)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "fails if an instance is created but no invocation occurs" do
|
178
|
+
expect do
|
179
|
+
klass.any_instance.should_receive(:foo)
|
180
|
+
klass.new
|
181
|
+
klass.rspec_verify
|
182
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, foo_expectation_error_message)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "fails if no instance is created" do
|
186
|
+
expect do
|
187
|
+
klass.any_instance.should_receive(:foo).and_return(1)
|
188
|
+
klass.rspec_verify
|
189
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, foo_expectation_error_message)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "fails if no instance is created and there are multiple expectations" do
|
193
|
+
expect do
|
194
|
+
klass.any_instance.should_receive(:foo)
|
195
|
+
klass.any_instance.should_receive(:bar)
|
196
|
+
klass.rspec_verify
|
197
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, 'Exactly one instance should have received the following message(s) but didn\'t: bar, foo')
|
198
|
+
end
|
199
|
+
|
200
|
+
it "allows expectations on instances to take priority" do
|
201
|
+
klass.any_instance.should_receive(:foo)
|
202
|
+
klass.new.foo
|
203
|
+
|
204
|
+
instance = klass.new
|
205
|
+
instance.should_receive(:foo).and_return(result = Object.new)
|
206
|
+
expect(instance.foo).to eq(result)
|
207
|
+
end
|
208
|
+
|
209
|
+
context "behaves as 'exactly one instance'" do
|
210
|
+
it "passes if subsequent invocations do not receive that message" do
|
211
|
+
klass.any_instance.should_receive(:foo)
|
212
|
+
klass.new.foo
|
213
|
+
klass.new
|
214
|
+
end
|
215
|
+
|
216
|
+
it "fails if the method is invoked on a second instance" do
|
217
|
+
instance_one = klass.new
|
218
|
+
instance_two = klass.new
|
219
|
+
expect do
|
220
|
+
klass.any_instance.should_receive(:foo)
|
221
|
+
|
222
|
+
instance_one.foo
|
223
|
+
instance_two.foo
|
224
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, "The message 'foo' was received by #{instance_two.inspect} but has already been received by #{instance_one.inspect}")
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context "normal expectations on the class object" do
|
229
|
+
it "fail when unfulfilled" do
|
230
|
+
expect do
|
231
|
+
klass.any_instance.should_receive(:foo)
|
232
|
+
klass.should_receive(:woot)
|
233
|
+
klass.new.foo
|
234
|
+
klass.rspec_verify
|
235
|
+
end.to(raise_error(RSpec::Mocks::MockExpectationError) do |error|
|
236
|
+
expect(error.message).not_to eq(existing_method_expectation_error_message)
|
237
|
+
end)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
it "pass when expectations are met" do
|
242
|
+
klass.any_instance.should_receive(:foo)
|
243
|
+
klass.should_receive(:woot).and_return(result = Object.new)
|
244
|
+
klass.new.foo
|
245
|
+
expect(klass.woot).to eq(result)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
context "with an expectation is set on a method that exists" do
|
251
|
+
it "returns the expected value" do
|
252
|
+
klass.any_instance.should_receive(:existing_method).and_return(1)
|
253
|
+
expect(klass.new.existing_method(1)).to eq(1)
|
254
|
+
end
|
255
|
+
|
256
|
+
it "fails if an instance is created but no invocation occurs" do
|
257
|
+
expect do
|
258
|
+
klass.any_instance.should_receive(:existing_method)
|
259
|
+
klass.new
|
260
|
+
klass.rspec_verify
|
261
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, existing_method_expectation_error_message)
|
262
|
+
end
|
263
|
+
|
264
|
+
it "fails if no instance is created" do
|
265
|
+
expect do
|
266
|
+
klass.any_instance.should_receive(:existing_method)
|
267
|
+
klass.rspec_verify
|
268
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, existing_method_expectation_error_message)
|
269
|
+
end
|
270
|
+
|
271
|
+
it "fails if no instance is created and there are multiple expectations" do
|
272
|
+
expect do
|
273
|
+
klass.any_instance.should_receive(:existing_method)
|
274
|
+
klass.any_instance.should_receive(:another_existing_method)
|
275
|
+
klass.rspec_verify
|
276
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, 'Exactly one instance should have received the following message(s) but didn\'t: another_existing_method, existing_method')
|
277
|
+
end
|
278
|
+
|
279
|
+
context "after any one instance has received a message" do
|
280
|
+
it "passes if subsequent invocations do not receive that message" do
|
281
|
+
klass.any_instance.should_receive(:existing_method)
|
282
|
+
klass.new.existing_method
|
283
|
+
klass.new
|
284
|
+
end
|
285
|
+
|
286
|
+
it "fails if the method is invoked on a second instance" do
|
287
|
+
instance_one = klass.new
|
288
|
+
instance_two = klass.new
|
289
|
+
expect do
|
290
|
+
klass.any_instance.should_receive(:existing_method)
|
291
|
+
|
292
|
+
instance_one.existing_method
|
293
|
+
instance_two.existing_method
|
294
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, "The message 'existing_method' was received by #{instance_two.inspect} but has already been received by #{instance_one.inspect}")
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
context "when resetting post-verification" do
|
301
|
+
let(:space) { RSpec::Mocks::Space.new }
|
302
|
+
|
303
|
+
context "existing method" do
|
304
|
+
before(:each) do
|
305
|
+
space.add(klass)
|
306
|
+
end
|
307
|
+
|
308
|
+
context "with stubbing" do
|
309
|
+
context "public methods" do
|
310
|
+
before(:each) do
|
311
|
+
Spy.on_instance_method(klass, :existing_method).and_return(1)
|
312
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_true
|
313
|
+
end
|
314
|
+
|
315
|
+
it "restores the class to its original state after each example when no instance is created" do
|
316
|
+
space.verify_all
|
317
|
+
|
318
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_false
|
319
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
320
|
+
end
|
321
|
+
|
322
|
+
it "restores the class to its original state after each example when one instance is created" do
|
323
|
+
klass.new.existing_method
|
324
|
+
|
325
|
+
space.verify_all
|
326
|
+
|
327
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_false
|
328
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
329
|
+
end
|
330
|
+
|
331
|
+
it "restores the class to its original state after each example when more than one instance is created" do
|
332
|
+
klass.new.existing_method
|
333
|
+
klass.new.existing_method
|
334
|
+
|
335
|
+
space.verify_all
|
336
|
+
|
337
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_false
|
338
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
context "private methods" do
|
343
|
+
before :each do
|
344
|
+
Spy.on_instance_method(klass, :private_method).and_return(:something)
|
345
|
+
space.verify_all
|
346
|
+
end
|
347
|
+
|
348
|
+
it "cleans up the backed up method" do
|
349
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_false
|
350
|
+
end
|
351
|
+
|
352
|
+
it "restores a stubbed private method after the spec is run" do
|
353
|
+
expect(klass.private_method_defined?(:private_method)).to be_true
|
354
|
+
end
|
355
|
+
|
356
|
+
it "ensures that the restored method behaves as it originally did" do
|
357
|
+
expect(klass.new.send(:private_method)).to eq(:private_method_return_value)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
context "with expectations" do
|
363
|
+
context "private methods" do
|
364
|
+
before :each do
|
365
|
+
klass.any_instance.should_receive(:private_method).and_return(:something)
|
366
|
+
klass.new.private_method
|
367
|
+
space.verify_all
|
368
|
+
end
|
369
|
+
|
370
|
+
it "cleans up the backed up method" do
|
371
|
+
expect(klass.method_defined?(:__existing_method_without_any_instance__)).to be_false
|
372
|
+
end
|
373
|
+
|
374
|
+
it "restores a stubbed private method after the spec is run" do
|
375
|
+
expect(klass.private_method_defined?(:private_method)).to be_true
|
376
|
+
end
|
377
|
+
|
378
|
+
it "ensures that the restored method behaves as it originally did" do
|
379
|
+
expect(klass.new.send(:private_method)).to eq(:private_method_return_value)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
context "ensures that the subsequent specs do not see expectations set in previous specs" do
|
384
|
+
context "when the instance created after the expectation is set" do
|
385
|
+
it "first spec" do
|
386
|
+
klass.any_instance.should_receive(:existing_method).and_return(Object.new)
|
387
|
+
klass.new.existing_method
|
388
|
+
end
|
389
|
+
|
390
|
+
it "second spec" do
|
391
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
context "when the instance created before the expectation is set" do
|
396
|
+
before :each do
|
397
|
+
@instance = klass.new
|
398
|
+
end
|
399
|
+
|
400
|
+
it "first spec" do
|
401
|
+
klass.any_instance.should_receive(:existing_method).and_return(Object.new)
|
402
|
+
@instance.existing_method
|
403
|
+
end
|
404
|
+
|
405
|
+
it "second spec" do
|
406
|
+
expect(@instance.existing_method).to eq(existing_method_return_value)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
it "ensures that the next spec does not see that expectation" do
|
412
|
+
klass.any_instance.should_receive(:existing_method).and_return(Object.new)
|
413
|
+
klass.new.existing_method
|
414
|
+
space.verify_all
|
415
|
+
|
416
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
context "with multiple calls to any_instance in the same example" do
|
422
|
+
it "does not prevent the change from being rolled back" do
|
423
|
+
Spy.on_instance_method(klass, :existing_method).and_return(false)
|
424
|
+
Spy.on_instance_method(klass, :existing_method).and_return(true)
|
425
|
+
|
426
|
+
klass.rspec_verify
|
427
|
+
expect(klass.new).to respond_to(:existing_method)
|
428
|
+
expect(klass.new.existing_method).to eq(existing_method_return_value)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
it "adds an class to the current space when #any_instance is invoked" do
|
433
|
+
klass.any_instance
|
434
|
+
expect(RSpec::Mocks::space.send(:receivers)).to include(klass)
|
435
|
+
end
|
436
|
+
|
437
|
+
it "adds an instance to the current space when stubbed method is invoked" do
|
438
|
+
Spy.on_instance_method(klass, :foo)
|
439
|
+
instance = klass.new
|
440
|
+
instance.foo
|
441
|
+
expect(RSpec::Mocks::space.send(:receivers)).to include(instance)
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
context 'when used in conjunction with a `dup`' do
|
446
|
+
it "doesn't cause an infinite loop" do
|
447
|
+
Object.any_instance.stub(:some_method)
|
448
|
+
o = Object.new
|
449
|
+
o.some_method
|
450
|
+
expect { o.dup.some_method }.to_not raise_error(SystemStackError)
|
451
|
+
end
|
452
|
+
|
453
|
+
it "doesn't bomb if the object doesn't support `dup`" do
|
454
|
+
klass = Class.new do
|
455
|
+
undef_method :dup
|
456
|
+
end
|
457
|
+
klass.any_instance
|
458
|
+
end
|
459
|
+
|
460
|
+
it "doesn't fail when dup accepts parameters" do
|
461
|
+
klass = Class.new do
|
462
|
+
def dup(funky_option)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
klass.any_instance
|
467
|
+
|
468
|
+
expect { klass.new.dup('Dup dup dup') }.to_not raise_error(ArgumentError)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
context "when directed at a method defined on a superclass" do
|
473
|
+
let(:sub_klass) { Class.new(klass) }
|
474
|
+
|
475
|
+
it "stubs the method correctly" do
|
476
|
+
Spy.on_instance_method(klass, :existing_method).and_return("foo")
|
477
|
+
expect(sub_klass.new.existing_method).to eq "foo"
|
478
|
+
end
|
479
|
+
|
480
|
+
it "mocks the method correctly" do
|
481
|
+
instance_one = sub_klass.new
|
482
|
+
instance_two = sub_klass.new
|
483
|
+
expect do
|
484
|
+
klass.any_instance.should_receive(:existing_method)
|
485
|
+
instance_one.existing_method
|
486
|
+
instance_two.existing_method
|
487
|
+
end.to raise_error(RSpec::Mocks::MockExpectationError, "The message 'existing_method' was received by #{instance_two.inspect} but has already been received by #{instance_one.inspect}")
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
context "when a class overrides Object#method" do
|
492
|
+
let(:http_request_class) { Struct.new(:method, :uri) }
|
493
|
+
|
494
|
+
it "stubs the method correctly" do
|
495
|
+
http_request_class.any_instance.stub(:existing_method).and_return("foo")
|
496
|
+
expect(http_request_class.new.existing_method).to eq "foo"
|
497
|
+
end
|
498
|
+
|
499
|
+
it "mocks the method correctly" do
|
500
|
+
http_request_class.any_instance.should_receive(:existing_method).and_return("foo")
|
501
|
+
expect(http_request_class.new.existing_method).to eq "foo"
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
context "when used after the test has finished" do
|
506
|
+
it "restores the original behavior of a stubbed method" do
|
507
|
+
Spy.on_instance_method(klass, :existing_method).and_return(:stubbed_return_value)
|
508
|
+
|
509
|
+
instance = klass.new
|
510
|
+
expect(instance.existing_method).to eq :stubbed_return_value
|
511
|
+
|
512
|
+
RSpec::Mocks.verify
|
513
|
+
|
514
|
+
expect(instance.existing_method).to eq :existing_method_return_value
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|