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/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup=markdown
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,23 +1,39 @@
|
|
1
1
|
# Spy
|
2
2
|
|
3
|
-
Spy is a lightweight
|
3
|
+
Spy is a lightweight stubbing framework with support for method spies, constant stubs, and object doubles.
|
4
|
+
|
5
|
+
Spy was designed for 1.9.3+.
|
6
|
+
|
7
|
+
Spy features that were completed were tested against the rspec-mocks tests so it covers all cases that rspec-mocks does.
|
4
8
|
|
5
9
|
Inspired by the spy api of the jasmine javascript testing framework.
|
6
10
|
|
7
11
|
## Why use this instead of rspec-mocks, mocha, or etc
|
8
12
|
|
9
|
-
*
|
13
|
+
* Spy will raise error when you try to stub/spy a method that doesn't exist
|
10
14
|
* when you change your method name your unit tests will break
|
15
|
+
* no more fantasy tests
|
11
16
|
* Spy arity matches original method
|
12
17
|
* Your tests will raise an error if you use the wrong arity
|
13
18
|
* Spy visibility matches original method
|
14
19
|
* Your tests will raise an error if you try to call the method incorrectly
|
15
20
|
* Simple call log api
|
16
21
|
* easier to read tests
|
17
|
-
*
|
22
|
+
* use ruby to test ruby instead of a dsl
|
18
23
|
* no expectations
|
19
24
|
* really who thought that was a good idea?
|
20
|
-
* absolutely no polution of global object space
|
25
|
+
* absolutely no polution of global object space
|
26
|
+
* no polution of instance variables for stubbed objects
|
27
|
+
|
28
|
+
Fail faster, code faster.
|
29
|
+
|
30
|
+
## Why not to use this
|
31
|
+
|
32
|
+
* Api is not stable
|
33
|
+
* missing these features
|
34
|
+
* Mocking null objects
|
35
|
+
* argument matchers for Spy::Method#has\_been\_called\_with
|
36
|
+
* watch all calls to an object to check order in which they are called
|
21
37
|
|
22
38
|
## Installation
|
23
39
|
|
@@ -35,63 +51,93 @@ Or install it yourself as:
|
|
35
51
|
|
36
52
|
## Usage
|
37
53
|
|
54
|
+
### Method Stubs
|
55
|
+
|
56
|
+
A method stub overrides a pre-existing method and records all calls to specified method. You can set the spy to return either the original method or your own custom implementation.
|
57
|
+
|
58
|
+
Spy support 2 different ways of spying an existing method on an object.
|
38
59
|
```ruby
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
60
|
+
Spy.on(book, title: "East of Eden")
|
61
|
+
Spy.on(book, :title).and_return("East of Eden")
|
62
|
+
Spy.on(book, :title).and_return { "East of Eden" }
|
63
|
+
```
|
43
64
|
|
44
|
-
|
45
|
-
|
46
|
-
end
|
65
|
+
Spy will raise an error if you try to stub on a method that doesn't exist.
|
66
|
+
You can force the creation of a sstub on method that didn't exist but it really isn't suggested.
|
47
67
|
|
48
|
-
|
49
|
-
|
50
|
-
|
68
|
+
```ruby
|
69
|
+
Spy.new(book, :flamethrower).hook(force:true).and_return("burnninante")
|
70
|
+
```
|
51
71
|
|
52
|
-
|
53
|
-
|
54
|
-
|
72
|
+
### Test Doubles
|
73
|
+
|
74
|
+
A test double is an object that stands in for a real object.
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
Spy.double("book")
|
78
|
+
```
|
79
|
+
|
80
|
+
Spy will let you stub on any method even if it doesn't exist if the object is a double.
|
81
|
+
|
82
|
+
Spy comes with a shortcut to define an object with methods.
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
Spy.double("book", title: "Grapes of Wrath", author: "John Steinbeck")
|
86
|
+
```
|
87
|
+
|
88
|
+
### Arbitrary Handling
|
89
|
+
|
90
|
+
If you need to have a custom method based in the method inputs just send a block to #and\_return
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
Spy.on(book, :read_page).and_return do |page, &block|
|
94
|
+
block.call
|
95
|
+
"awesome " * page
|
55
96
|
end
|
56
97
|
```
|
57
98
|
|
58
|
-
|
99
|
+
An error will raise if the arity of the block is larger than the arity of the original method. However this can be overidden with the force argument.
|
59
100
|
|
60
101
|
```ruby
|
61
|
-
|
102
|
+
Spy.on(book, :read_page).and_return(force: true) do |a, b, c, d|
|
103
|
+
end
|
104
|
+
```
|
62
105
|
|
63
|
-
|
64
|
-
person.first_name #=> nil
|
65
|
-
first_name_spy.called? #=> true
|
106
|
+
### Method Spies
|
66
107
|
|
67
|
-
|
108
|
+
When you stub a method it returns a spy. A spy records what calls have been made to a given method.
|
68
109
|
|
69
|
-
|
70
|
-
|
110
|
+
```ruby
|
111
|
+
validator = Spy.double("validator")
|
112
|
+
validate_spy = Spy.on(validator, :validate)
|
113
|
+
validate_spy.has_been_called? #=> false
|
114
|
+
validator.validate("01234") #=> nil
|
115
|
+
validate_spy.has_been_called? #=> true
|
116
|
+
validate_spy.has_been_called_with?("01234) #=> true
|
117
|
+
```
|
71
118
|
|
72
|
-
|
73
|
-
|
74
|
-
person.first_name #=> "Bob"
|
119
|
+
### Calling through
|
120
|
+
If you just want to make sure if a method is called and not override the output you can just use the and\_call\_through method
|
75
121
|
|
76
|
-
|
77
|
-
|
122
|
+
```ruby
|
123
|
+
Spy.on(book, :read_page).and_call_through
|
124
|
+
```
|
78
125
|
|
79
|
-
|
80
|
-
person.say("hello") {
|
81
|
-
"everything accepts a block in ruby"
|
82
|
-
}
|
83
|
-
say_spy.say("world")
|
126
|
+
### Call Logs
|
84
127
|
|
85
|
-
|
86
|
-
say_spy.calls.count #=> 1
|
87
|
-
say_spy.calls.first.args #=> ["hello"]
|
88
|
-
say_spy.calls.last.args #=> ["world"]
|
128
|
+
When a spy is called on it records a call log. A call log contains the object it was called on, the arguments and block that were sent to method and what it returned.
|
89
129
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
130
|
+
```ruby
|
131
|
+
read_page_spy = Spy.on(book, read_page: "hello world")
|
132
|
+
book.read_page(5) { "this is a block" }
|
133
|
+
book.read_page(3)
|
134
|
+
book.read_page(7)
|
135
|
+
read_page_spy.calls.size #=> 3
|
136
|
+
first_call = read_page_spy.calls.first
|
137
|
+
first_call.object #=> book
|
138
|
+
first_call.args #=> [5]
|
139
|
+
first_call.block #=> Proc.new { "this is a block" }
|
140
|
+
first_call.result #=> "hello world"
|
95
141
|
```
|
96
142
|
|
97
143
|
### MiniTest
|
data/lib/spy.rb
CHANGED
@@ -1,229 +1,119 @@
|
|
1
|
-
require "spy/
|
1
|
+
require "spy/core_ext/marshal"
|
2
|
+
require "spy/agency"
|
3
|
+
require "spy/constant"
|
2
4
|
require "spy/double"
|
3
|
-
require "spy/
|
4
|
-
|
5
|
-
|
6
|
-
CallLog = Struct.new(:object, :args, :block)
|
7
|
-
|
8
|
-
attr_reader :base_object, :method_name, :calls, :original_method
|
9
|
-
def initialize(object, method_name)
|
10
|
-
@base_object, @method_name = object, method_name
|
11
|
-
reset!
|
12
|
-
end
|
13
|
-
|
14
|
-
# hooks the method into the object and stashes original method if it exists
|
15
|
-
# @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
|
16
|
-
# @return self
|
17
|
-
def hook(opts = {})
|
18
|
-
raise "#{method_name} method has already been hooked" if hooked?
|
19
|
-
raise "#{base_object} method '#{method_name}' has already been hooked" if self.class.get(base_object, method_name)
|
20
|
-
opts[:force] ||= base_object.is_a?(Double)
|
21
|
-
if base_object.respond_to?(method_name, true) || !opts[:force]
|
22
|
-
@original_method = base_object.method(method_name)
|
23
|
-
end
|
5
|
+
require "spy/nest"
|
6
|
+
require "spy/subroutine"
|
7
|
+
require "spy/version"
|
24
8
|
|
25
|
-
|
9
|
+
module Spy
|
10
|
+
SECRET_SPY_KEY = Object.new
|
11
|
+
class << self
|
12
|
+
# create a spy on given object
|
13
|
+
# @param base_object
|
14
|
+
# @param method_names *[Hash,Symbol] will spy on these methods and also set default return values
|
15
|
+
# @return [Subroutine, Array<Subroutine>]
|
16
|
+
def on(base_object, *method_names)
|
17
|
+
spies = method_names.map do |method_name|
|
18
|
+
create_and_hook_spy(base_object, method_name)
|
19
|
+
end.flatten
|
26
20
|
|
27
|
-
|
28
|
-
base_object.singleton_class.send(:remove_method, method_name)
|
21
|
+
spies.size > 1 ? spies : spies.first
|
29
22
|
end
|
30
23
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
24
|
+
# removes the spy from the from the given object
|
25
|
+
# @param base_object
|
26
|
+
# @param method_names *[Symbol]
|
27
|
+
# @return [Subroutine, Array<Subroutine>]
|
28
|
+
def off(base_object, *method_names)
|
29
|
+
removed_spies = method_names.map do |method_name|
|
30
|
+
spy = Subroutine.get(base_object, method_name)
|
31
|
+
if spy
|
32
|
+
spy.unhook
|
33
|
+
else
|
34
|
+
raise "Spy was not found"
|
35
|
+
end
|
37
36
|
end
|
38
|
-
end
|
39
|
-
|
40
|
-
base_object.singleton_class.send(opts[:visibility], method_name) if opts[:visibility]
|
41
37
|
|
42
|
-
|
43
|
-
self
|
44
|
-
end
|
45
|
-
|
46
|
-
# unhooks method from object
|
47
|
-
# @return self
|
48
|
-
def unhook
|
49
|
-
raise "#{method_name} method has not been hooked" unless hooked?
|
50
|
-
base_object.singleton_class.send(:remove_method, method_name)
|
51
|
-
if original_method && original_method.owner == base_object.singleton_class
|
52
|
-
base_object.define_singleton_method(method_name, original_method)
|
53
|
-
base_object.singleton_class.send(method_visibility, method_name) if method_visibility
|
54
|
-
end
|
55
|
-
clear_method!
|
56
|
-
self
|
57
|
-
end
|
58
|
-
|
59
|
-
# is the spy hooked?
|
60
|
-
# @return Boolean
|
61
|
-
def hooked?
|
62
|
-
@hooked
|
63
|
-
end
|
64
|
-
|
65
|
-
|
66
|
-
# sets the return value of given spied method
|
67
|
-
# @params return value
|
68
|
-
# @params return block
|
69
|
-
# @return self
|
70
|
-
def and_return(value = nil, &block)
|
71
|
-
if block_given?
|
72
|
-
raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
|
73
|
-
@plan = block
|
74
|
-
else
|
75
|
-
@plan = Proc.new { value }
|
38
|
+
removed_spies.size > 1 ? removed_spies : removed_spies.first
|
76
39
|
end
|
77
|
-
self
|
78
|
-
end
|
79
40
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
base_object.send(:method_missing, method_name, *args, &block)
|
41
|
+
# create a stub for constants on given module
|
42
|
+
# @param base_module [Module]
|
43
|
+
# @param constant_names *[Symbol, Hash]
|
44
|
+
# @return [Constant, Array<Constant>]
|
45
|
+
def on_const(base_module, *constant_names)
|
46
|
+
if base_module.is_a? Symbol
|
47
|
+
constant_names.unshift(base_module)
|
48
|
+
base_module = Object
|
89
49
|
end
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
# check if the method was called with the exact arguments
|
99
|
-
def has_been_called_with?(*args)
|
100
|
-
calls.any? do |call_log|
|
101
|
-
call_log.args == args
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
# record that the method has been called. You really shouldn't use this
|
106
|
-
# method.
|
107
|
-
def record(object, args, block)
|
108
|
-
check_arity!(args.size)
|
109
|
-
calls << CallLog.new(object, args, block)
|
110
|
-
@plan.call(*args, &block) if @plan
|
111
|
-
end
|
112
|
-
|
113
|
-
# reset the call log
|
114
|
-
def reset!
|
115
|
-
@calls = []
|
116
|
-
clear_method!
|
117
|
-
true
|
118
|
-
end
|
119
|
-
|
120
|
-
private
|
121
|
-
|
122
|
-
def clear_method!
|
123
|
-
@hooked = false
|
124
|
-
@original_method = @arity_range = @method_visibility = nil
|
125
|
-
end
|
126
|
-
|
127
|
-
def method_visibility
|
128
|
-
@method_visibility ||=
|
129
|
-
if base_object.respond_to?(method_name)
|
130
|
-
if original_method && original_method.owner.protected_method_defined?(method_name)
|
131
|
-
:protected
|
132
|
-
else
|
133
|
-
:public
|
50
|
+
spies = constant_names.map do |constant_name|
|
51
|
+
case constant_name
|
52
|
+
when String, Symbol
|
53
|
+
Constant.on(base_module, constant_name)
|
54
|
+
when Hash
|
55
|
+
constant_name.map do |name, result|
|
56
|
+
on_const(base_module, name).and_return(result)
|
134
57
|
end
|
135
|
-
|
136
|
-
|
58
|
+
else
|
59
|
+
raise ArgumentError.new "#{constant_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
|
137
60
|
end
|
138
|
-
|
61
|
+
end.flatten
|
139
62
|
|
140
|
-
|
141
|
-
return unless arity_range
|
142
|
-
if arity < arity_range.min
|
143
|
-
raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.min})")
|
144
|
-
elsif arity > arity_range.max
|
145
|
-
raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.max})")
|
63
|
+
spies.size > 1 ? spies : spies.first
|
146
64
|
end
|
147
|
-
end
|
148
65
|
|
149
|
-
|
150
|
-
@
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
max = Float::INFINITY
|
66
|
+
# removes stubs from given module
|
67
|
+
# @param base_module [Module]
|
68
|
+
# @param constant_names *[Symbol]
|
69
|
+
# @return [Constant, Array<Constant>]
|
70
|
+
def off_const(base_module, *constant_names)
|
71
|
+
spies = constant_names.map do |constant_name|
|
72
|
+
case constant_name
|
73
|
+
when String, Symbol
|
74
|
+
Constant.off(base_module, constant_name)
|
75
|
+
when Hash
|
76
|
+
constant_name.map do |name, result|
|
77
|
+
off_const(base_module, name).and_return(result)
|
162
78
|
end
|
79
|
+
else
|
80
|
+
raise ArgumentError.new "#{constant_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
|
163
81
|
end
|
164
|
-
(min..max)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
class << self
|
169
|
-
# create a spy on given object
|
170
|
-
# @params base_object
|
171
|
-
# @params method_names *[Symbol] will spy on these methods
|
172
|
-
# @params method_names [Hash] will spy on these methods and also set default return values
|
173
|
-
# @return [Spy, Array<Spy>]
|
174
|
-
def on(base_object, *method_names)
|
175
|
-
spies = method_names.map do |method_name|
|
176
|
-
create_and_hook_spy(base_object, method_name)
|
177
82
|
end.flatten
|
178
83
|
|
179
84
|
spies.size > 1 ? spies : spies.first
|
180
85
|
end
|
181
86
|
|
182
|
-
# removes the spy from the
|
183
|
-
def off(base_object, *method_names)
|
184
|
-
removed_spies = method_names.map do |method_name|
|
185
|
-
unhook_and_remove_spy(base_object, method_name)
|
186
|
-
end.flatten
|
187
|
-
|
188
|
-
raise "No spies found" if removed_spies.empty?
|
189
|
-
removed_spies.size > 1 ? removed_spies : removed_spies.first
|
190
|
-
end
|
191
|
-
|
192
|
-
# get all hooked methods
|
193
|
-
# @return [Array<Spy>]
|
194
|
-
def all
|
195
|
-
@all ||= []
|
196
|
-
end
|
197
|
-
|
198
87
|
# unhook all methods
|
199
88
|
def teardown
|
200
|
-
|
201
|
-
reset
|
202
|
-
end
|
203
|
-
|
204
|
-
# reset all hooked methods
|
205
|
-
def reset
|
206
|
-
@all = nil
|
89
|
+
Agency.instance.dissolve!
|
207
90
|
end
|
208
91
|
|
209
|
-
#
|
92
|
+
# returns a double
|
93
|
+
# (see Double#initizalize)
|
210
94
|
def double(*args)
|
211
95
|
Double.new(*args)
|
212
96
|
end
|
213
97
|
|
214
|
-
# @private
|
215
|
-
def __secret_method_key__
|
216
|
-
@__secret_method_key__ ||= Object.new
|
217
|
-
end
|
218
|
-
|
219
98
|
# retrieve the spy from an object
|
220
|
-
# @
|
221
|
-
# @method_names *[Symbol
|
99
|
+
# @param base_object
|
100
|
+
# @param method_names *[Symbol]
|
101
|
+
# @return [Subroutine, Array<Subroutine>]
|
222
102
|
def get(base_object, *method_names)
|
223
103
|
spies = method_names.map do |method_name|
|
224
|
-
|
225
|
-
|
226
|
-
|
104
|
+
Subroutine.get(base_object, method_name)
|
105
|
+
end
|
106
|
+
|
107
|
+
spies.size > 1 ? spies : spies.first
|
108
|
+
end
|
109
|
+
|
110
|
+
# retrieve the constant spies from an object
|
111
|
+
# @param base_module
|
112
|
+
# @param constant_names *[Symbol]
|
113
|
+
# @return [Constant, Array<Constant>]
|
114
|
+
def get_const(base_module, *constant_names)
|
115
|
+
spies = constant_names.map do |method_name|
|
116
|
+
Constant.get(base_module, constant_name)
|
227
117
|
end
|
228
118
|
|
229
119
|
spies.size > 1 ? spies : spies.first
|
@@ -234,26 +124,14 @@ class Spy
|
|
234
124
|
def create_and_hook_spy(base_object, method_name, opts = {})
|
235
125
|
case method_name
|
236
126
|
when String, Symbol
|
237
|
-
|
238
|
-
all << spy
|
239
|
-
spy
|
127
|
+
Subroutine.new(base_object, method_name).hook(opts)
|
240
128
|
when Hash
|
241
129
|
method_name.map do |name, result|
|
242
130
|
create_and_hook_spy(base_object, name, opts).and_return(result)
|
243
131
|
end
|
244
132
|
else
|
245
|
-
raise ArgumentError.new "#{method_name.class} is an invalid
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
def unhook_and_remove_spy(base_object, method_name)
|
250
|
-
removed_spies = []
|
251
|
-
all.delete_if do |spy|
|
252
|
-
if spy.base_object == base_object && spy.method_name == method_name
|
253
|
-
removed_spies << spy.unhook
|
254
|
-
end
|
133
|
+
raise ArgumentError.new "#{method_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
|
255
134
|
end
|
256
|
-
removed_spies
|
257
135
|
end
|
258
136
|
end
|
259
137
|
end
|