spy 0.2.4 → 0.2.5
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/.travis.yml +8 -2
- data/README.md +32 -2
- data/lib/spy/agency.rb +5 -4
- data/lib/spy/constant.rb +7 -5
- data/lib/spy/exceptions.rb +33 -0
- data/lib/spy/nest.rb +24 -12
- data/lib/spy/subroutine.rb +22 -6
- data/lib/spy/version.rb +1 -1
- data/lib/spy.rb +9 -8
- data/spec/spy/any_instance_spec.rb +3 -3
- data/spy.gemspec +1 -0
- data/test/spy/test_subroutine.rb +8 -1
- data/test/test_helper.rb +2 -0
- metadata +19 -2
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,8 @@
|
|
1
|
-
# Spy
|
1
|
+
# Spy
|
2
|
+
[](http://badge.fury.io/rb/spy)
|
3
|
+
[](https://travis-ci.org/ryanong/spy)
|
4
|
+
[](https://coveralls.io/r/ryanong/spy)
|
5
|
+
|
2
6
|
|
3
7
|
[Docs](http://rdoc.info/gems/spy/frames)
|
4
8
|
|
@@ -40,6 +44,7 @@ Fail faster, code faster.
|
|
40
44
|
* #with is not supported
|
41
45
|
* you can usually just check the call logs.
|
42
46
|
* if you do need to use this. It is probably a code smell. You either need to abstract your method more or add separate tests.
|
47
|
+
* you use `mock_model` and `stub_model` (I want to impliment this soon)
|
43
48
|
|
44
49
|
## Installation
|
45
50
|
|
@@ -75,7 +80,7 @@ Spy will raise an error if you try to stub on a method that doesn't exist.
|
|
75
80
|
You can force the creation of a stub on method that didn't exist but it really isn't suggested.
|
76
81
|
|
77
82
|
```ruby
|
78
|
-
Spy.new(book, :flamethrower).hook(force:true).and_return("burnninante")
|
83
|
+
Spy::Subroutine.new(book, :flamethrower).hook(force:true).and_return("burnninante")
|
79
84
|
```
|
80
85
|
|
81
86
|
You can also stub instance methods of Classes and Modules. This is equivalent to
|
@@ -176,6 +181,19 @@ In `test_helper.rb`
|
|
176
181
|
```ruby
|
177
182
|
require "spy"
|
178
183
|
MiniTest::TestCase.add_teardown_hook { Spy.teardown }
|
184
|
+
|
185
|
+
|
186
|
+
class TestBook < MiniTest::Unit::TestCase
|
187
|
+
def test_title
|
188
|
+
book = book.new
|
189
|
+
title_spy = spy.on(book, title)
|
190
|
+
book.title
|
191
|
+
book.title
|
192
|
+
|
193
|
+
assert title_spy.has_been_called?
|
194
|
+
assert_equal 2, title_spy.calls.count
|
195
|
+
end
|
196
|
+
end
|
179
197
|
```
|
180
198
|
|
181
199
|
### Rspec
|
@@ -189,6 +207,18 @@ RSpec.configure do |c|
|
|
189
207
|
c.after { Spy.teardown }
|
190
208
|
c.mock_with :absolutely_nothing # this is completely optional.
|
191
209
|
end
|
210
|
+
|
211
|
+
describe Book do
|
212
|
+
it "title can be called" do
|
213
|
+
book = book.new
|
214
|
+
title_spy = spy.on(book, title)
|
215
|
+
book.title
|
216
|
+
book.title
|
217
|
+
|
218
|
+
expect(title_spy).to have_been_called
|
219
|
+
expect(title_spy.calls.count).to eq(2)
|
220
|
+
end
|
221
|
+
end
|
192
222
|
```
|
193
223
|
|
194
224
|
### Test::Unit
|
data/lib/spy/agency.rb
CHANGED
@@ -10,7 +10,6 @@ module Spy
|
|
10
10
|
clear!
|
11
11
|
end
|
12
12
|
|
13
|
-
|
14
13
|
# given a spy ID it will return the associated spy
|
15
14
|
# @param id [Integer] spy object id
|
16
15
|
# @return [Nil, Subroutine, Constant, Double]
|
@@ -22,11 +21,12 @@ module Spy
|
|
22
21
|
# @param spy [Subroutine, Constant, Double]
|
23
22
|
# @return [spy]
|
24
23
|
def recruit(spy)
|
24
|
+
raise AlreadyStubbedError if @spies[spy.object_id]
|
25
25
|
case spy
|
26
26
|
when Subroutine, Constant, Double
|
27
27
|
@spies[spy.object_id] = spy
|
28
28
|
else
|
29
|
-
raise "
|
29
|
+
raise ArgumentError, "#{spy}, was not a spy"
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
@@ -34,11 +34,12 @@ module Spy
|
|
34
34
|
# @param spy [Subroutine, Constant, Double]
|
35
35
|
# @return [spy]
|
36
36
|
def retire(spy)
|
37
|
+
raise NoSpyError unless @spies[spy.object_id]
|
37
38
|
case spy
|
38
39
|
when Subroutine, Constant, Double
|
39
40
|
@spies.delete(spy.object_id)
|
40
41
|
else
|
41
|
-
raise "
|
42
|
+
raise ArgumentError, "#{spy}, was not a spy"
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
@@ -50,7 +51,7 @@ module Spy
|
|
50
51
|
when Subroutine, Constant, Double
|
51
52
|
@spies.has_key?(spy.object_id)
|
52
53
|
else
|
53
|
-
raise "
|
54
|
+
raise ArgumentError, "#{spy}, was not a spy"
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
data/lib/spy/constant.rb
CHANGED
@@ -16,8 +16,8 @@ module Spy
|
|
16
16
|
# @param base_module [Module] the module this spy should be on
|
17
17
|
# @param constant_name [Symbol] the constant this spy is watching
|
18
18
|
def initialize(base_module, constant_name)
|
19
|
-
raise "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
|
20
|
-
raise "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
|
19
|
+
raise ArgumentError, "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
|
20
|
+
raise ArgumentError, "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
|
21
21
|
@base_module, @constant_name = base_module, constant_name.to_sym
|
22
22
|
@original_value = @new_value = @previously_defined = nil
|
23
23
|
end
|
@@ -97,7 +97,7 @@ module Spy
|
|
97
97
|
end
|
98
98
|
|
99
99
|
class << self
|
100
|
-
# creates a new constant spy and hooks the constant
|
100
|
+
# finds existing spy or creates a new constant spy and hooks the constant
|
101
101
|
# @return [Constant]
|
102
102
|
def on(base_module, constant_name)
|
103
103
|
new(base_module, constant_name).hook
|
@@ -107,7 +107,9 @@ module Spy
|
|
107
107
|
# from the module
|
108
108
|
# @return [Constant]
|
109
109
|
def off(base_module, constant_name)
|
110
|
-
get(base_module, constant_name)
|
110
|
+
spy = get(base_module, constant_name)
|
111
|
+
raise NoSpyError, "#{constant_name} was not spied on #{base_module}" unless spy
|
112
|
+
spy.unhook
|
111
113
|
end
|
112
114
|
|
113
115
|
# retrieves the spy for given constnat and module or returns nil
|
@@ -115,7 +117,7 @@ module Spy
|
|
115
117
|
def get(base_module, constant_name)
|
116
118
|
nest = Nest.get(base_module)
|
117
119
|
if nest
|
118
|
-
nest.
|
120
|
+
nest.get(constant_name)
|
119
121
|
end
|
120
122
|
end
|
121
123
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Spy
|
2
|
+
class Error < StandardError; end
|
3
|
+
|
4
|
+
class AlreadyStubbedError < Error
|
5
|
+
def message
|
6
|
+
"Spy is already stubbed."
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class AlreadyHookedError < Error
|
11
|
+
def message
|
12
|
+
"Spy is already hooked."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class NotHookedError < Error
|
17
|
+
def message
|
18
|
+
"Spy was not hooked."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class NeverHookedError < Error
|
23
|
+
def message
|
24
|
+
"Spy was never hooked."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class NoSpyError < Error
|
29
|
+
def message
|
30
|
+
"Spy could not be found"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/spy/nest.rb
CHANGED
@@ -5,26 +5,26 @@ module Spy
|
|
5
5
|
# @!attribute [r] base_module
|
6
6
|
# @return [Module] The module that the Nest is managing
|
7
7
|
#
|
8
|
-
# @!attribute [r]
|
8
|
+
# @!attribute [r] constant_spies
|
9
9
|
# @return [Hash<Symbol, Constant>] The module that the Nest is managing
|
10
10
|
|
11
11
|
|
12
|
-
attr_reader :base_module
|
12
|
+
attr_reader :base_module
|
13
13
|
|
14
14
|
def initialize(base_module)
|
15
|
-
raise "#{base_module} is not a kind of Module" unless base_module.is_a?(Module)
|
15
|
+
raise ArgumentError, "#{base_module} is not a kind of Module" unless base_module.is_a?(Module)
|
16
16
|
@base_module = base_module
|
17
|
-
@
|
17
|
+
@constant_spies = {}
|
18
18
|
end
|
19
19
|
|
20
20
|
# records that the spy is hooked
|
21
21
|
# @param spy [Constant]
|
22
22
|
# @return [self]
|
23
23
|
def add(spy)
|
24
|
-
if @
|
25
|
-
raise "#{spy.constant_name} has already been stubbed"
|
24
|
+
if @constant_spies[spy.constant_name]
|
25
|
+
raise AlreadyStubbedError, "#{spy.constant_name} has already been stubbed"
|
26
26
|
else
|
27
|
-
@
|
27
|
+
@constant_spies[spy.constant_name] = spy
|
28
28
|
end
|
29
29
|
self
|
30
30
|
end
|
@@ -33,19 +33,32 @@ module Spy
|
|
33
33
|
# @param spy [Constant]
|
34
34
|
# @return [self]
|
35
35
|
def remove(spy)
|
36
|
-
if @
|
37
|
-
@
|
36
|
+
if @constant_spies[spy.constant_name] == spy
|
37
|
+
@constant_spies.delete(spy.constant_name)
|
38
38
|
else
|
39
|
-
raise "#{spy.constant_name} was
|
39
|
+
raise NoSpyError, "#{spy.constant_name} was not stubbed on #{base_module.name}"
|
40
40
|
end
|
41
41
|
self
|
42
42
|
end
|
43
43
|
|
44
|
+
# returns a spy if the constant was added
|
45
|
+
# @param constant_name [Symbol]
|
46
|
+
# @return [Constant, nil]
|
47
|
+
def get(constant_name)
|
48
|
+
@constant_spies[constant_name]
|
49
|
+
end
|
50
|
+
|
44
51
|
# checks to see if a given constant is hooked
|
45
52
|
# @param constant_name [Symbol]
|
46
53
|
# @return [Boolean]
|
47
54
|
def hooked?(constant_name)
|
48
|
-
|
55
|
+
!!get(constant_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
# list all the constants that are being stubbed
|
59
|
+
# @return [Array]
|
60
|
+
def hooked_constants
|
61
|
+
@constant_spies.keys
|
49
62
|
end
|
50
63
|
|
51
64
|
class << self
|
@@ -56,7 +69,6 @@ module Spy
|
|
56
69
|
all[base_module.name]
|
57
70
|
end
|
58
71
|
|
59
|
-
|
60
72
|
# retrieves the nest for a given module or creates it
|
61
73
|
# @param base_module [Module]
|
62
74
|
# @return [Nest]
|
data/lib/spy/subroutine.rb
CHANGED
@@ -40,7 +40,7 @@ module Spy
|
|
40
40
|
# @option opts [Symbol<:public, :protected, :private>] visibility overrides visibility with whatever method is given
|
41
41
|
# @return [self]
|
42
42
|
def hook(opts = {})
|
43
|
-
raise "#{base_object} method '#{method_name}' has already been hooked" if
|
43
|
+
raise AlreadyHookedError, "#{base_object} method '#{method_name}' has already been hooked" if self.class.get(base_object, method_name, singleton_method)
|
44
44
|
|
45
45
|
@hook_opts = opts
|
46
46
|
@original_method_visibility = method_visibility_of(method_name)
|
@@ -66,7 +66,7 @@ module Spy
|
|
66
66
|
# unhooks method from object
|
67
67
|
# @return [self]
|
68
68
|
def unhook
|
69
|
-
raise "'#{method_name}' method has not been hooked" unless hooked?
|
69
|
+
raise NeverHookedError, "'#{method_name}' method has not been hooked" unless hooked?
|
70
70
|
|
71
71
|
if original_method && method_owner == original_method.owner
|
72
72
|
method_owner.send(:define_method, method_name, original_method)
|
@@ -111,7 +111,7 @@ module Spy
|
|
111
111
|
if value.is_a?(Hash) && value.has_key?(:force)
|
112
112
|
@do_not_check_plan_arity = !!value[:force]
|
113
113
|
elsif !value.nil?
|
114
|
-
raise ArgumentError
|
114
|
+
raise ArgumentError, "value and block conflict. Choose one"
|
115
115
|
end
|
116
116
|
|
117
117
|
@plan = Proc.new
|
@@ -184,7 +184,7 @@ module Spy
|
|
184
184
|
# if the method was called it will return true
|
185
185
|
# @return [Boolean]
|
186
186
|
def has_been_called?
|
187
|
-
raise
|
187
|
+
raise NeverHookedError unless @was_hooked
|
188
188
|
calls.size > 0
|
189
189
|
end
|
190
190
|
|
@@ -192,7 +192,7 @@ module Spy
|
|
192
192
|
# @param args Arguments that should have been sent to the method
|
193
193
|
# @return [Boolean]
|
194
194
|
def has_been_called_with?(*args)
|
195
|
-
raise
|
195
|
+
raise NeverHookedError unless @was_hooked
|
196
196
|
calls.any? do |call_log|
|
197
197
|
call_log.args == args
|
198
198
|
end
|
@@ -305,7 +305,23 @@ module Spy
|
|
305
305
|
end
|
306
306
|
|
307
307
|
class << self
|
308
|
-
|
308
|
+
|
309
|
+
# retrieve the method spy from an object or create a new one
|
310
|
+
# @param base_object
|
311
|
+
# @param method_name [Symbol]
|
312
|
+
# @param singleton_method [Boolean] this a singleton method or a instance method?
|
313
|
+
# @return [Array<Subroutine>]
|
314
|
+
def on(base_object, method_name, singleton_method = true)
|
315
|
+
new(base_object, method_name, singleton_method).hook
|
316
|
+
end
|
317
|
+
|
318
|
+
def off(base_object, method_name, singleton_method = true)
|
319
|
+
spy = get(base_object, method_name, singleton_method = true)
|
320
|
+
raise NoSpyError, "#{method_name} was not spied on #{base_object}" unless spy
|
321
|
+
spy.unhook
|
322
|
+
end
|
323
|
+
|
324
|
+
# retrieve the method spy from an object or return nil
|
309
325
|
# @param base_object
|
310
326
|
# @param method_name [Symbol]
|
311
327
|
# @param singleton_method [Boolean] this a singleton method or a instance method?
|
data/lib/spy/version.rb
CHANGED
data/lib/spy.rb
CHANGED
@@ -3,6 +3,7 @@ require "spy/agency"
|
|
3
3
|
require "spy/call_log"
|
4
4
|
require "spy/constant"
|
5
5
|
require "spy/double"
|
6
|
+
require "spy/exceptions"
|
6
7
|
require "spy/nest"
|
7
8
|
require "spy/subroutine"
|
8
9
|
require "spy/version"
|
@@ -32,7 +33,7 @@ module Spy
|
|
32
33
|
if spy
|
33
34
|
spy.unhook
|
34
35
|
else
|
35
|
-
raise "#{
|
36
|
+
raise NoSpyError, "#{method_name} was not hooked on #{base_object.inspect}."
|
36
37
|
end
|
37
38
|
end
|
38
39
|
|
@@ -54,7 +55,7 @@ module Spy
|
|
54
55
|
if spy
|
55
56
|
spy.unhook
|
56
57
|
else
|
57
|
-
raise "#{
|
58
|
+
raise NoSpyError, "#{method_name} was not hooked on #{base_object.inspect}."
|
58
59
|
end
|
59
60
|
end
|
60
61
|
|
@@ -79,7 +80,7 @@ module Spy
|
|
79
80
|
Constant.on(base_module, name).and_return(result)
|
80
81
|
end
|
81
82
|
else
|
82
|
-
raise ArgumentError
|
83
|
+
raise ArgumentError, "#{constant_name.class} is an invalid input, #on only accepts Symbol, and Hash"
|
83
84
|
end
|
84
85
|
end.flatten
|
85
86
|
|
@@ -98,7 +99,7 @@ module Spy
|
|
98
99
|
|
99
100
|
spies = constant_names.map do |constant_name|
|
100
101
|
unless constant_name.is_a?(Symbol)
|
101
|
-
raise ArgumentError
|
102
|
+
raise ArgumentError, "#{constant_name.class} is an invalid input, #on only accepts Symbol, and Hash"
|
102
103
|
end
|
103
104
|
Constant.off(base_module, constant_name)
|
104
105
|
end
|
@@ -148,16 +149,16 @@ module Spy
|
|
148
149
|
|
149
150
|
private
|
150
151
|
|
151
|
-
def create_and_hook_spy(base_object, method_name, singleton_method = true
|
152
|
+
def create_and_hook_spy(base_object, method_name, singleton_method = true)
|
152
153
|
case method_name
|
153
154
|
when String, Symbol
|
154
|
-
Subroutine.
|
155
|
+
Subroutine.on(base_object, method_name, singleton_method)
|
155
156
|
when Hash
|
156
157
|
method_name.map do |name, result|
|
157
|
-
|
158
|
+
Subroutine.on(base_object, name, singleton_method).and_return(result)
|
158
159
|
end
|
159
160
|
else
|
160
|
-
raise ArgumentError
|
161
|
+
raise ArgumentError, "#{method_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
|
161
162
|
end
|
162
163
|
end
|
163
164
|
end
|
@@ -444,7 +444,7 @@ module Spy
|
|
444
444
|
|
445
445
|
context 'when used in conjunction with a `dup`' do
|
446
446
|
it "doesn't cause an infinite loop" do
|
447
|
-
|
447
|
+
Spy::Subroutine.new(Object, :some_method, false).hook(force: true)
|
448
448
|
o = Object.new
|
449
449
|
o.some_method
|
450
450
|
expect { o.dup.some_method }.to_not raise_error(SystemStackError)
|
@@ -454,7 +454,7 @@ module Spy
|
|
454
454
|
klass = Class.new do
|
455
455
|
undef_method :dup
|
456
456
|
end
|
457
|
-
|
457
|
+
Spy::Subroutine.new(Object, :some_method, false).hook(force: true)
|
458
458
|
end
|
459
459
|
|
460
460
|
it "doesn't fail when dup accepts parameters" do
|
@@ -463,7 +463,7 @@ module Spy
|
|
463
463
|
end
|
464
464
|
end
|
465
465
|
|
466
|
-
|
466
|
+
Spy::Subroutine.new(Object, :some_method, false).hook(force: true)
|
467
467
|
|
468
468
|
expect { klass.new.dup('Dup dup dup') }.to_not raise_error(ArgumentError)
|
469
469
|
end
|
data/spy.gemspec
CHANGED
data/test/spy/test_subroutine.rb
CHANGED
@@ -226,8 +226,15 @@ module Spy
|
|
226
226
|
def test_spy_get_can_retrieve_a_spy
|
227
227
|
pen_write_spy = spy_on(@pen, :write).and_return(:hello)
|
228
228
|
assert_equal :hello, @pen.write(:world)
|
229
|
-
assert_equal pen_write_spy, Subroutine.get(@pen, :write)
|
230
229
|
assert Subroutine.get(@pen, :write).has_been_called?
|
230
|
+
assert_same pen_write_spy, Subroutine.get(@pen, :write)
|
231
|
+
end
|
232
|
+
|
233
|
+
def test_spy_hook_raises_an_error_on_an_already_hooked_method
|
234
|
+
spy_on(@pen, :write)
|
235
|
+
assert_raises AlreadyHookedError do
|
236
|
+
spy_on(@pen, :write)
|
237
|
+
end
|
231
238
|
end
|
232
239
|
end
|
233
240
|
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-03-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: pry
|
@@ -91,6 +91,22 @@ dependencies:
|
|
91
91
|
- - ! '>='
|
92
92
|
- !ruby/object:Gem::Version
|
93
93
|
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: coveralls
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
94
110
|
description: A simple modern mocking library that uses the spy pattern and checks
|
95
111
|
method's existence and arity.
|
96
112
|
email:
|
@@ -112,6 +128,7 @@ files:
|
|
112
128
|
- lib/spy/constant.rb
|
113
129
|
- lib/spy/core_ext/marshal.rb
|
114
130
|
- lib/spy/double.rb
|
131
|
+
- lib/spy/exceptions.rb
|
115
132
|
- lib/spy/nest.rb
|
116
133
|
- lib/spy/subroutine.rb
|
117
134
|
- lib/spy/version.rb
|