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.
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