spy 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.yardopts +1 -0
- data/Gemfile +2 -0
- data/README.md +90 -44
- data/lib/spy.rb +85 -207
- data/lib/spy/agency.rb +65 -0
- data/lib/spy/constant.rb +67 -0
- data/lib/spy/core_ext/marshal.rb +28 -0
- data/lib/spy/double.rb +1 -1
- data/lib/spy/nest.rb +45 -0
- data/lib/spy/subroutine.rb +239 -0
- data/lib/spy/version.rb +2 -2
- data/spec/spec_helper.rb +4 -2
- data/spec/spy/and_call_original_spec.rb +1 -1
- data/spec/spy/and_yield_spec.rb +23 -14
- data/spec/spy/hash_excluding_matcher_spec.rb +55 -59
- data/spec/spy/hash_including_matcher_spec.rb +69 -73
- data/spec/spy/mock_spec.rb +585 -589
- data/spec/spy/mutate_const_spec.rb +407 -367
- data/spec/spy/partial_mock_spec.rb +106 -162
- data/spec/spy/passing_argument_matchers_spec.rb +134 -136
- data/spec/spy/serialization_spec.rb +80 -74
- data/spec/spy/stub_implementation_spec.rb +14 -12
- data/spec/spy/stub_spec.rb +12 -12
- data/spec/spy/test_double_spec.rb +38 -41
- data/spy.gemspec +2 -1
- data/test/integration/test_constant_spying.rb +58 -0
- data/test/integration/test_subroutine_spying.rb +40 -0
- data/test/spy/test_double.rb +1 -1
- data/test/spy/test_subroutine.rb +191 -0
- data/test/support/pen.rb +50 -0
- data/test/test_helper.rb +4 -0
- metadata +33 -9
- data/TODO.md +0 -8
- data/lib/spy/dsl.rb +0 -7
- data/spec/spy/multiple_return_value_spec.rb +0 -119
- data/test/test_spy.rb +0 -258
data/lib/spy/agency.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
|
3
|
+
module Spy
|
4
|
+
class Agency
|
5
|
+
include Singleton
|
6
|
+
|
7
|
+
attr_reader :subroutines, :constants, :doubles
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
clear!
|
11
|
+
end
|
12
|
+
|
13
|
+
def recruit(spy)
|
14
|
+
case spy
|
15
|
+
when Subroutine
|
16
|
+
subroutines << spy
|
17
|
+
when Constant
|
18
|
+
constants << spy
|
19
|
+
when Double
|
20
|
+
doubles << spy
|
21
|
+
else
|
22
|
+
raise "Not a spy"
|
23
|
+
end
|
24
|
+
spy
|
25
|
+
end
|
26
|
+
|
27
|
+
def retire(spy)
|
28
|
+
case spy
|
29
|
+
when Subroutine
|
30
|
+
subroutines.delete(spy)
|
31
|
+
when Constant
|
32
|
+
constants.delete(spy)
|
33
|
+
when Double
|
34
|
+
doubles.delete(spy)
|
35
|
+
else
|
36
|
+
raise "Not a spy"
|
37
|
+
end
|
38
|
+
spy
|
39
|
+
end
|
40
|
+
|
41
|
+
def active?(spy)
|
42
|
+
case spy
|
43
|
+
when Subroutine
|
44
|
+
subroutines.include?(spy)
|
45
|
+
when Constant
|
46
|
+
constants.include?(spy)
|
47
|
+
when Double
|
48
|
+
doubles.include?(spy)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def dissolve!
|
53
|
+
subroutines.each(&:unhook)
|
54
|
+
constants.each(&:unhook)
|
55
|
+
clear!
|
56
|
+
end
|
57
|
+
|
58
|
+
def clear!
|
59
|
+
@subroutines = []
|
60
|
+
@constants = []
|
61
|
+
@doubles = []
|
62
|
+
self
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/spy/constant.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
module Spy
|
2
|
+
class Constant
|
3
|
+
attr_reader :base_module, :constant_name, :original_value, :new_value
|
4
|
+
|
5
|
+
def initialize(base_module, constant_name)
|
6
|
+
raise "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
|
7
|
+
raise "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
|
8
|
+
@base_module, @constant_name = base_module, constant_name.to_sym
|
9
|
+
@original_value = nil
|
10
|
+
@new_value = nil
|
11
|
+
@was_defined = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def hook(opts = {})
|
15
|
+
opts[:force] ||= false
|
16
|
+
@was_defined = base_module.const_defined?(constant_name, false)
|
17
|
+
if @was_defined || !opts[:force]
|
18
|
+
@original_value = base_module.const_get(constant_name, false)
|
19
|
+
end
|
20
|
+
and_return(@new_value)
|
21
|
+
Nest.fetch(base_module).add(self)
|
22
|
+
Agency.instance.recruit(self)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def unhook
|
27
|
+
if @was_defined
|
28
|
+
and_return(@original_value)
|
29
|
+
end
|
30
|
+
@original_value = nil
|
31
|
+
|
32
|
+
Agency.instance.retire(self)
|
33
|
+
Nest.fetch(base_module).remove(self)
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def and_hide
|
38
|
+
base_module.send(:remove_const, constant_name)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def and_return(value)
|
43
|
+
@new_value = value
|
44
|
+
base_module.send(:remove_const, constant_name) if base_module.const_defined?(constant_name, false)
|
45
|
+
base_module.const_set(constant_name, @new_value)
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def hooked?
|
50
|
+
Nest.get(base_module).hooked?(constant_name)
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def on(base_module, constant_name)
|
55
|
+
new(base_module, constant_name).hook
|
56
|
+
end
|
57
|
+
|
58
|
+
def off(base_module, constant_name)
|
59
|
+
get(base_module, constant_name).unhook
|
60
|
+
end
|
61
|
+
|
62
|
+
def get(base_module, constant_name)
|
63
|
+
Nest.get(base_module).hooked_constants[constant_name]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Marshal
|
2
|
+
class << self
|
3
|
+
def dump_with_mocks(*args)
|
4
|
+
object = args.shift
|
5
|
+
spies = Spy::Subroutine.get_spies(object)
|
6
|
+
if spies.empty?
|
7
|
+
return dump_without_mocks(*args.unshift(object))
|
8
|
+
end
|
9
|
+
|
10
|
+
spy_hook_options = spies.map do |spy|
|
11
|
+
opts = spy.opts
|
12
|
+
[spy.unhook, opts]
|
13
|
+
end
|
14
|
+
|
15
|
+
begin
|
16
|
+
dump_without_mocks(*args.unshift(object.dup))
|
17
|
+
ensure
|
18
|
+
spy_hook_options.each do |spy, opts|
|
19
|
+
spy.hook(opts)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
alias_method :dump_without_mocks, :dump
|
25
|
+
undef_method :dump
|
26
|
+
alias_method :dump, :dump_with_mocks
|
27
|
+
end
|
28
|
+
end
|
data/lib/spy/double.rb
CHANGED
data/lib/spy/nest.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Spy
|
2
|
+
class Nest
|
3
|
+
attr_reader :base_module, :hooked_constants
|
4
|
+
|
5
|
+
def initialize(base_module)
|
6
|
+
raise "#{base_module} is not a kind of Module" unless base_module.is_a?(Module)
|
7
|
+
@base_module = base_module
|
8
|
+
@hooked_constants = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(spy)
|
12
|
+
if @hooked_constants[spy.constant_name]
|
13
|
+
raise "#{spy.constant_name} has already been stubbed"
|
14
|
+
else
|
15
|
+
@hooked_constants[spy.constant_name] = spy
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove(spy)
|
21
|
+
if @hooked_constants[spy.constant_name] == spy
|
22
|
+
@hooked_constants.delete(spy.constant_name)
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def hooked?(constant_name)
|
28
|
+
!!@hooked_constants[constant_name]
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def get(base_module)
|
33
|
+
all[base_module.name]
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch(base_module)
|
37
|
+
all[base_module.name] ||= self.new(base_module)
|
38
|
+
end
|
39
|
+
|
40
|
+
def all
|
41
|
+
@all ||= {}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module Spy
|
2
|
+
class Subroutine
|
3
|
+
CallLog = Struct.new(:object, :args, :block, :result)
|
4
|
+
attr_reader :base_object, :method_name, :calls, :original_method, :opts
|
5
|
+
|
6
|
+
# set what object and method the spy should watch
|
7
|
+
# @param object
|
8
|
+
# @param method_name <Symbol>
|
9
|
+
def initialize(object, method_name)
|
10
|
+
@was_hooked = false
|
11
|
+
@base_object, @method_name = object, method_name
|
12
|
+
reset!
|
13
|
+
end
|
14
|
+
|
15
|
+
# 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
|
18
|
+
def hook(opts = {})
|
19
|
+
@opts = opts
|
20
|
+
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)
|
24
|
+
end
|
25
|
+
|
26
|
+
opts[:visibility] ||= method_visibility
|
27
|
+
|
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
|
35
|
+
end
|
36
|
+
|
37
|
+
base_object.singleton_class.send(opts[:visibility], method_name) if opts[:visibility]
|
38
|
+
|
39
|
+
Agency.instance.recruit(self)
|
40
|
+
@was_hooked = true
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# unhooks method from object
|
45
|
+
# @return self
|
46
|
+
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
|
51
|
+
else
|
52
|
+
base_object.singleton_class.send(:remove_method, method_name)
|
53
|
+
end
|
54
|
+
clear_method!
|
55
|
+
Agency.instance.retire(self)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
# is the spy hooked?
|
60
|
+
# @return Boolean
|
61
|
+
def hooked?
|
62
|
+
self == self.class.get(base_object, method_name)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @overload and_return(value)
|
66
|
+
# @overload and_return(&block)
|
67
|
+
#
|
68
|
+
# Tells the spy to return a value when the method is called.
|
69
|
+
#
|
70
|
+
# @return self
|
71
|
+
def and_return(value = nil)
|
72
|
+
if block_given?
|
73
|
+
@plan = Proc.new
|
74
|
+
if value.nil? || value.is_a?(Hash) && value.has_key?(:force)
|
75
|
+
if !(value.is_a?(Hash) && value[:force]) &&
|
76
|
+
original_method &&
|
77
|
+
original_method.arity >=0 &&
|
78
|
+
@plan.arity > original_method.arity
|
79
|
+
raise ArgumentError.new "The original method only has an arity of #{original_method.arity} you have an arity of #{@plan.arity}"
|
80
|
+
end
|
81
|
+
else
|
82
|
+
raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
|
83
|
+
end
|
84
|
+
else
|
85
|
+
@plan = Proc.new { value }
|
86
|
+
end
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
# Tells the object to yield one or more args to a block when the message is received.
|
91
|
+
def and_yield(*args)
|
92
|
+
yield eval_context = Object.new if block_given?
|
93
|
+
@plan = Proc.new do |&block|
|
94
|
+
eval_context.instance_exec(*args, &block)
|
95
|
+
end
|
96
|
+
self
|
97
|
+
end
|
98
|
+
|
99
|
+
# tells the spy to call the original method
|
100
|
+
# @return self
|
101
|
+
def and_call_through
|
102
|
+
raise "can only call through if original method is set" unless method_visibility
|
103
|
+
if original_method
|
104
|
+
@plan = original_method
|
105
|
+
else
|
106
|
+
@plan = Proc.new do |*args, &block|
|
107
|
+
base_object.send(:method_missing, method_name, *args, &block)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
113
|
+
def and_raise(exception = RuntimeError, message = nil)
|
114
|
+
if exception.respond_to?(:exception)
|
115
|
+
exception = message ? exception.exception(message) : exception.exception
|
116
|
+
end
|
117
|
+
|
118
|
+
@plan = Proc.new { raise exception }
|
119
|
+
end
|
120
|
+
|
121
|
+
def and_throw(*args)
|
122
|
+
@plan = Proc.new { throw(*args) }
|
123
|
+
self
|
124
|
+
end
|
125
|
+
|
126
|
+
def has_been_called?
|
127
|
+
raise "was never hooked" unless @was_hooked
|
128
|
+
calls.size > 0
|
129
|
+
end
|
130
|
+
|
131
|
+
# check if the method was called with the exact arguments
|
132
|
+
def has_been_called_with?(*args)
|
133
|
+
raise "was never hooked" unless @was_hooked
|
134
|
+
calls.any? do |call_log|
|
135
|
+
call_log.args == args
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# invoke that the method has been called. You really shouldn't use this
|
140
|
+
# method.
|
141
|
+
def invoke(object, args, block)
|
142
|
+
check_arity!(args.size)
|
143
|
+
result = @plan ? @plan.call(*args, &block) : nil
|
144
|
+
calls << CallLog.new(object, args, block, result)
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
# reset the call log
|
149
|
+
def reset!
|
150
|
+
@calls = []
|
151
|
+
clear_method!
|
152
|
+
true
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def call_with_yield(&block)
|
158
|
+
raise "no block sent" unless block
|
159
|
+
value = nil
|
160
|
+
@args_to_yield.each do |args|
|
161
|
+
if block.arity > -1 && args.length != block.arity
|
162
|
+
@error_generator.raise_wrong_arity_error args, block.arity
|
163
|
+
end
|
164
|
+
value = @eval_context ? @eval_context.instance_exec(*args, &block) : block.call(*args)
|
165
|
+
end
|
166
|
+
value
|
167
|
+
end
|
168
|
+
|
169
|
+
def clear_method!
|
170
|
+
@hooked = false
|
171
|
+
@opts = @original_method = @arity_range = @method_visibility = nil
|
172
|
+
end
|
173
|
+
|
174
|
+
def method_visibility
|
175
|
+
@method_visibility ||=
|
176
|
+
if base_object.respond_to?(method_name)
|
177
|
+
if original_method && original_method.owner.protected_method_defined?(method_name)
|
178
|
+
:protected
|
179
|
+
else
|
180
|
+
:public
|
181
|
+
end
|
182
|
+
elsif base_object.respond_to?(method_name, true)
|
183
|
+
:private
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def check_arity!(arity)
|
188
|
+
self.class.check_arity_against_range!(arity_range, arity)
|
189
|
+
end
|
190
|
+
|
191
|
+
def arity_range
|
192
|
+
@arity_range ||= self.class.arity_range_of(original_method) if original_method
|
193
|
+
end
|
194
|
+
|
195
|
+
class << self
|
196
|
+
def arity_range_of(block)
|
197
|
+
raise "#{block.inspect} does not respond to :parameters" unless block.respond_to?(:parameters)
|
198
|
+
min = max = 0
|
199
|
+
block.parameters.each do |type,_|
|
200
|
+
case type
|
201
|
+
when :req
|
202
|
+
min += 1
|
203
|
+
max += 1
|
204
|
+
when :opt
|
205
|
+
max += 1
|
206
|
+
when :rest
|
207
|
+
max = Float::INFINITY
|
208
|
+
end
|
209
|
+
end
|
210
|
+
(min..max)
|
211
|
+
end
|
212
|
+
|
213
|
+
def check_arity_against_range!(arity_range, arity)
|
214
|
+
return unless arity_range
|
215
|
+
if arity < arity_range.min
|
216
|
+
raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.min})")
|
217
|
+
elsif arity > arity_range.max
|
218
|
+
raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.max})")
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
SPY_METHOD_PARAMS = [[:rest, :__spy_args], [:block, :block]]
|
223
|
+
|
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)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
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
|
235
|
+
end.compact
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|