spy 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|