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