spy 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/.travis.yml +5 -0
  2. data/Gemfile +1 -0
  3. data/README.md +22 -7
  4. data/Rakefile +2 -0
  5. data/lib/spy.rb +39 -6
  6. data/lib/spy/agency.rb +42 -27
  7. data/lib/spy/call_log.rb +26 -0
  8. data/lib/spy/constant.rb +72 -14
  9. data/lib/spy/core_ext/marshal.rb +1 -0
  10. data/lib/spy/double.rb +17 -0
  11. data/lib/spy/nest.rb +27 -0
  12. data/lib/spy/subroutine.rb +146 -44
  13. data/lib/spy/version.rb +1 -1
  14. data/spec/spy/any_instance_spec.rb +518 -0
  15. data/spec/spy/mock_spec.rb +46 -554
  16. data/spec/spy/mutate_const_spec.rb +21 -63
  17. data/spec/spy/null_object_mock_spec.rb +11 -39
  18. data/spec/spy/partial_mock_spec.rb +3 -62
  19. data/spec/spy/stash_spec.rb +30 -37
  20. data/spec/spy/stub_spec.rb +0 -6
  21. data/spec/spy/to_ary_spec.rb +5 -5
  22. data/test/integration/test_constant_spying.rb +1 -1
  23. data/test/integration/test_instance_method.rb +32 -0
  24. data/test/integration/test_subroutine_spying.rb +7 -4
  25. data/test/spy/test_double.rb +4 -0
  26. data/test/spy/test_subroutine.rb +28 -3
  27. data/test/support/pen.rb +15 -0
  28. metadata +8 -30
  29. data/spec/spy/bug_report_10260_spec.rb +0 -8
  30. data/spec/spy/bug_report_10263_spec.rb +0 -24
  31. data/spec/spy/bug_report_496_spec.rb +0 -18
  32. data/spec/spy/bug_report_600_spec.rb +0 -24
  33. data/spec/spy/bug_report_7611_spec.rb +0 -16
  34. data/spec/spy/bug_report_8165_spec.rb +0 -31
  35. data/spec/spy/bug_report_830_spec.rb +0 -21
  36. data/spec/spy/bug_report_957_spec.rb +0 -22
  37. data/spec/spy/double_spec.rb +0 -12
  38. data/spec/spy/failing_argument_matchers_spec.rb +0 -94
  39. data/spec/spy/options_hash_spec.rb +0 -35
  40. data/spec/spy/precise_counts_spec.rb +0 -68
  41. data/spec/spy/stubbed_message_expectations_spec.rb +0 -47
  42. data/spec/spy/test_double_spec.rb +0 -54
@@ -1,5 +1,6 @@
1
1
  module Marshal
2
2
  class << self
3
+ # @private
3
4
  def dump_with_mocks(*args)
4
5
  object = args.shift
5
6
  spies = Spy::Subroutine.get_spies(object)
@@ -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
@@ -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
@@ -1,55 +1,69 @@
1
1
  module Spy
2
2
  class Subroutine
3
- CallLog = Struct.new(:object, :args, :block, :result)
4
- attr_reader :base_object, :method_name, :calls, :original_method, :opts
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
- def initialize(object, method_name)
10
- @was_hooked = false
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 opts [Hash{force => false, visibility => nil}] set :force => true if you want it to ignore if the method exists, or visibility to [:public, :protected, :private] to overwride current visibility
17
- # @return self
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
- @opts = opts
37
+ @hook_opts = opts
20
38
  raise "#{base_object} method '#{method_name}' has already been hooked" if hooked?
21
- opts[:force] ||= base_object.is_a?(Double)
22
- if base_object.respond_to?(method_name, true) || !opts[:force]
23
- @original_method = base_object.method(method_name)
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
- opts[:visibility] ||= method_visibility
46
+ base_object.send(define_method_with, method_name, override_method)
27
47
 
28
- __method_spy__ = self
29
- base_object.define_singleton_method(method_name) do |*__spy_args, &block|
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
- if original_method && original_method.owner == base_object.singleton_class
49
- base_object.define_singleton_method(method_name, original_method)
50
- base_object.singleton_class.send(method_visibility, method_name) if method_visibility
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
- base_object.singleton_class.send(:remove_method, method_name)
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
- calls << CallLog.new(object, args, block, result)
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
- @opts = @original_method = @arity_range = @method_visibility = nil
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 base_object.respond_to?(method_name)
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 base_object.respond_to?(method_name, true)
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
- SPY_METHOD_PARAMS = [[:rest, :__spy_args], [:block, :block]]
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
- def get(base_object, method_name)
225
- if (base_object.singleton_methods + base_object.singleton_class.private_instance_methods(false)).include?(method_name.to_sym) && base_object.method(method_name).parameters == SPY_METHOD_PARAMS
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.singleton_methods.map do |method_name|
232
- if base_object.method(method_name).parameters == SPY_METHOD_PARAMS
233
- base_object.send(method_name, SECRET_SPY_KEY)
234
- end
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
@@ -1,3 +1,3 @@
1
1
  module Spy
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -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