spy 0.1.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +5 -0
- data/Gemfile +1 -0
- data/README.md +22 -7
- data/Rakefile +2 -0
- data/lib/spy.rb +39 -6
- data/lib/spy/agency.rb +42 -27
- data/lib/spy/call_log.rb +26 -0
- data/lib/spy/constant.rb +72 -14
- data/lib/spy/core_ext/marshal.rb +1 -0
- data/lib/spy/double.rb +17 -0
- data/lib/spy/nest.rb +27 -0
- data/lib/spy/subroutine.rb +146 -44
- data/lib/spy/version.rb +1 -1
- data/spec/spy/any_instance_spec.rb +518 -0
- data/spec/spy/mock_spec.rb +46 -554
- data/spec/spy/mutate_const_spec.rb +21 -63
- data/spec/spy/null_object_mock_spec.rb +11 -39
- data/spec/spy/partial_mock_spec.rb +3 -62
- data/spec/spy/stash_spec.rb +30 -37
- data/spec/spy/stub_spec.rb +0 -6
- data/spec/spy/to_ary_spec.rb +5 -5
- data/test/integration/test_constant_spying.rb +1 -1
- data/test/integration/test_instance_method.rb +32 -0
- data/test/integration/test_subroutine_spying.rb +7 -4
- data/test/spy/test_double.rb +4 -0
- data/test/spy/test_subroutine.rb +28 -3
- data/test/support/pen.rb +15 -0
- metadata +8 -30
- data/spec/spy/bug_report_10260_spec.rb +0 -8
- data/spec/spy/bug_report_10263_spec.rb +0 -24
- data/spec/spy/bug_report_496_spec.rb +0 -18
- data/spec/spy/bug_report_600_spec.rb +0 -24
- data/spec/spy/bug_report_7611_spec.rb +0 -16
- data/spec/spy/bug_report_8165_spec.rb +0 -31
- data/spec/spy/bug_report_830_spec.rb +0 -21
- data/spec/spy/bug_report_957_spec.rb +0 -22
- data/spec/spy/double_spec.rb +0 -12
- data/spec/spy/failing_argument_matchers_spec.rb +0 -94
- data/spec/spy/options_hash_spec.rb +0 -35
- data/spec/spy/precise_counts_spec.rb +0 -68
- data/spec/spy/stubbed_message_expectations_spec.rb +0 -47
- data/spec/spy/test_double_spec.rb +0 -54
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -29,11 +29,14 @@ Fail faster, code faster.
|
|
29
29
|
|
30
30
|
## Why not to use this
|
31
31
|
|
32
|
-
*
|
33
|
-
*
|
34
|
-
|
35
|
-
|
36
|
-
*
|
32
|
+
* mocking null objects is not supported(yet)
|
33
|
+
* no argument matchers for Spy::Method#has\_been\_called\_with
|
34
|
+
* cannot watch all calls to an object to check order in which they are called
|
35
|
+
* cannot transfer nested constants when stubbing a constant
|
36
|
+
* i don't think anybody uses this anyway
|
37
|
+
* nobody on github does
|
38
|
+
* #with is not supported yet
|
39
|
+
* this is probably a code smell. You either need to abstract your method more or add separate tests.
|
37
40
|
|
38
41
|
## Installation
|
39
42
|
|
@@ -56,10 +59,13 @@ Or install it yourself as:
|
|
56
59
|
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
60
|
|
58
61
|
Spy support 2 different ways of spying an existing method on an object.
|
62
|
+
|
59
63
|
```ruby
|
60
64
|
Spy.on(book, title: "East of Eden")
|
61
65
|
Spy.on(book, :title).and_return("East of Eden")
|
62
66
|
Spy.on(book, :title).and_return { "East of Eden" }
|
67
|
+
|
68
|
+
book.title #=> "East of Eden"
|
63
69
|
```
|
64
70
|
|
65
71
|
Spy will raise an error if you try to stub on a method that doesn't exist.
|
@@ -69,6 +75,15 @@ You can force the creation of a sstub on method that didn't exist but it really
|
|
69
75
|
Spy.new(book, :flamethrower).hook(force:true).and_return("burnninante")
|
70
76
|
```
|
71
77
|
|
78
|
+
You can also stub instance methods of Classes and Modules
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
Spy.on_instance_method(Book, :title).and_return("Cannery Row")
|
82
|
+
|
83
|
+
Book.new(title: "Siddhartha").title #=> "Cannery Row"
|
84
|
+
Book.new(title: "The Big Cheese").title #=> "Cannery Row"
|
85
|
+
```
|
86
|
+
|
72
87
|
### Test Doubles
|
73
88
|
|
74
89
|
A test double is an object that stands in for a real object.
|
@@ -155,7 +170,7 @@ In spec\_helper.rb
|
|
155
170
|
require "rspec/autorun"
|
156
171
|
require "spy"
|
157
172
|
RSpec.configure do |c|
|
158
|
-
c.
|
173
|
+
c.after { Spy.teardown }
|
159
174
|
end
|
160
175
|
```
|
161
176
|
|
@@ -164,7 +179,7 @@ end
|
|
164
179
|
```ruby
|
165
180
|
require "spy"
|
166
181
|
class Test::Unit::TestCase
|
167
|
-
def
|
182
|
+
def teardown
|
168
183
|
Spy.teardown
|
169
184
|
end
|
170
185
|
end
|
data/Rakefile
CHANGED
data/lib/spy.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "spy/core_ext/marshal"
|
2
2
|
require "spy/agency"
|
3
|
+
require "spy/call_log"
|
3
4
|
require "spy/constant"
|
4
5
|
require "spy/double"
|
5
6
|
require "spy/nest"
|
@@ -7,7 +8,7 @@ require "spy/subroutine"
|
|
7
8
|
require "spy/version"
|
8
9
|
|
9
10
|
module Spy
|
10
|
-
|
11
|
+
|
11
12
|
class << self
|
12
13
|
# create a spy on given object
|
13
14
|
# @param base_object
|
@@ -38,12 +39,34 @@ module Spy
|
|
38
39
|
removed_spies.size > 1 ? removed_spies : removed_spies.first
|
39
40
|
end
|
40
41
|
|
42
|
+
#
|
43
|
+
def on_instance_method(base_class, *method_names)
|
44
|
+
spies = method_names.map do |method_name|
|
45
|
+
create_and_hook_spy(base_class, method_name, false)
|
46
|
+
end.flatten
|
47
|
+
|
48
|
+
spies.size > 1 ? spies : spies.first
|
49
|
+
end
|
50
|
+
|
51
|
+
def off_instance_method(base_object, *method_names)
|
52
|
+
removed_spies = method_names.map do |method_name|
|
53
|
+
spy = Subroutine.get(base_object, method_name, false)
|
54
|
+
if spy
|
55
|
+
spy.unhook
|
56
|
+
else
|
57
|
+
raise "Spy was not found"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
removed_spies.size > 1 ? removed_spies : removed_spies.first
|
62
|
+
end
|
63
|
+
|
41
64
|
# create a stub for constants on given module
|
42
65
|
# @param base_module [Module]
|
43
66
|
# @param constant_names *[Symbol, Hash]
|
44
67
|
# @return [Constant, Array<Constant>]
|
45
68
|
def on_const(base_module, *constant_names)
|
46
|
-
if base_module.is_a? Symbol
|
69
|
+
if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
|
47
70
|
constant_names.unshift(base_module)
|
48
71
|
base_module = Object
|
49
72
|
end
|
@@ -68,6 +91,11 @@ module Spy
|
|
68
91
|
# @param constant_names *[Symbol]
|
69
92
|
# @return [Constant, Array<Constant>]
|
70
93
|
def off_const(base_module, *constant_names)
|
94
|
+
if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
|
95
|
+
constant_names.unshift(base_module)
|
96
|
+
base_module = Object
|
97
|
+
end
|
98
|
+
|
71
99
|
spies = constant_names.map do |constant_name|
|
72
100
|
case constant_name
|
73
101
|
when String, Symbol
|
@@ -112,7 +140,12 @@ module Spy
|
|
112
140
|
# @param constant_names *[Symbol]
|
113
141
|
# @return [Constant, Array<Constant>]
|
114
142
|
def get_const(base_module, *constant_names)
|
115
|
-
|
143
|
+
if base_module.is_a?(Hash) || base_module.is_a?(Symbol)
|
144
|
+
constant_names.unshift(base_module)
|
145
|
+
base_module = Object
|
146
|
+
end
|
147
|
+
|
148
|
+
spies = constant_names.map do |constant_name|
|
116
149
|
Constant.get(base_module, constant_name)
|
117
150
|
end
|
118
151
|
|
@@ -121,13 +154,13 @@ module Spy
|
|
121
154
|
|
122
155
|
private
|
123
156
|
|
124
|
-
def create_and_hook_spy(base_object, method_name,
|
157
|
+
def create_and_hook_spy(base_object, method_name, singleton_method = true, hook_opts = {})
|
125
158
|
case method_name
|
126
159
|
when String, Symbol
|
127
|
-
Subroutine.new(base_object, method_name).hook(
|
160
|
+
Subroutine.new(base_object, method_name, singleton_method).hook(hook_opts)
|
128
161
|
when Hash
|
129
162
|
method_name.map do |name, result|
|
130
|
-
create_and_hook_spy(base_object, name,
|
163
|
+
create_and_hook_spy(base_object, name, singleton_method, hook_opts).and_return(result)
|
131
164
|
end
|
132
165
|
else
|
133
166
|
raise ArgumentError.new "#{method_name.class} is an invalid input, #on only accepts String, Symbol, and Hash"
|
data/lib/spy/agency.rb
CHANGED
@@ -1,65 +1,80 @@
|
|
1
1
|
require 'singleton'
|
2
2
|
|
3
3
|
module Spy
|
4
|
+
# Manages all the spies
|
4
5
|
class Agency
|
5
6
|
include Singleton
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
# @private
|
9
9
|
def initialize
|
10
10
|
clear!
|
11
11
|
end
|
12
12
|
|
13
|
+
|
14
|
+
# given a spy ID it will return the associated spy
|
15
|
+
# @param id [Integer] spy object id
|
16
|
+
# @return [Nil, Subroutine, Constant, Double]
|
17
|
+
def find(id)
|
18
|
+
@spies[id]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Record that a spy was initialized and hooked
|
22
|
+
# @param spy [Subroutine, Constant, Double]
|
23
|
+
# @return [spy]
|
13
24
|
def recruit(spy)
|
14
25
|
case spy
|
15
|
-
when Subroutine
|
16
|
-
|
17
|
-
when Constant
|
18
|
-
constants << spy
|
19
|
-
when Double
|
20
|
-
doubles << spy
|
26
|
+
when Subroutine, Constant, Double
|
27
|
+
@spies[spy.object_id] = spy
|
21
28
|
else
|
22
29
|
raise "Not a spy"
|
23
30
|
end
|
24
|
-
spy
|
25
31
|
end
|
26
32
|
|
33
|
+
# remove spy from the records
|
34
|
+
# @param spy [Subroutine, Constant, Double]
|
35
|
+
# @return [spy]
|
27
36
|
def retire(spy)
|
28
37
|
case spy
|
29
|
-
when Subroutine
|
30
|
-
|
31
|
-
when Constant
|
32
|
-
constants.delete(spy)
|
33
|
-
when Double
|
34
|
-
doubles.delete(spy)
|
38
|
+
when Subroutine, Constant, Double
|
39
|
+
@spies.delete(spy.object_id)
|
35
40
|
else
|
36
41
|
raise "Not a spy"
|
37
42
|
end
|
38
|
-
spy
|
39
43
|
end
|
40
44
|
|
45
|
+
# checks to see if a spy is hooked
|
46
|
+
# @param spy [Subroutine, Constant, Double]
|
47
|
+
# @return [Boolean]
|
41
48
|
def active?(spy)
|
42
49
|
case spy
|
43
|
-
when Subroutine
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
when Double
|
48
|
-
doubles.include?(spy)
|
50
|
+
when Subroutine, Constant, Double
|
51
|
+
@spies.has_key?(spy.object_id)
|
52
|
+
else
|
53
|
+
raise "Not a spy"
|
49
54
|
end
|
50
55
|
end
|
51
56
|
|
57
|
+
# unhooks all spies and clears records
|
58
|
+
# @return [self]
|
52
59
|
def dissolve!
|
53
|
-
|
54
|
-
|
60
|
+
@spies.values.each do |spy|
|
61
|
+
spy.unhook if spy.respond_to?(:unhook)
|
62
|
+
end
|
55
63
|
clear!
|
56
64
|
end
|
57
65
|
|
66
|
+
# clears records
|
67
|
+
# @return [self]
|
58
68
|
def clear!
|
59
|
-
@
|
60
|
-
@constants = []
|
61
|
-
@doubles = []
|
69
|
+
@spies = {}
|
62
70
|
self
|
63
71
|
end
|
72
|
+
|
73
|
+
# returns all the spies that have been initialized since the creation of
|
74
|
+
# this agency
|
75
|
+
# @return [Array<Subroutine, Constant, Double>]
|
76
|
+
def spies
|
77
|
+
@spies.values
|
78
|
+
end
|
64
79
|
end
|
65
80
|
end
|
data/lib/spy/call_log.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Spy
|
2
|
+
class CallLog
|
3
|
+
|
4
|
+
# @!attribute [r] object
|
5
|
+
# @return [Object] object that the method was called from
|
6
|
+
#
|
7
|
+
# @!attribute [r] called_from
|
8
|
+
# @return [String] where the method was called from
|
9
|
+
#
|
10
|
+
# @!attribute [r] args
|
11
|
+
# @return [Array] arguments were sent to the method
|
12
|
+
#
|
13
|
+
# @!attribute [r] block
|
14
|
+
# @return [Proc] the block that was sent to the method
|
15
|
+
#
|
16
|
+
# @!attribute [r] result
|
17
|
+
# @return The result of the method of being stubbed, or called through
|
18
|
+
|
19
|
+
|
20
|
+
attr_reader :object, :called_from, :args, :block, :result
|
21
|
+
|
22
|
+
def initialize(object, called_from, args, block, result)
|
23
|
+
@object, @called_from, @args, @block, @result = object, called_from, args, block, result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/spy/constant.rb
CHANGED
@@ -1,66 +1,124 @@
|
|
1
1
|
module Spy
|
2
2
|
class Constant
|
3
|
-
attr_reader :base_module, :constant_name, :original_value, :new_value
|
4
3
|
|
4
|
+
# @!attribute [r] base_module
|
5
|
+
# @return [Module] the module that is being watched
|
6
|
+
#
|
7
|
+
# @!attribute [r] constant_name
|
8
|
+
# @return [Symbol] the name of the constant that is/will be stubbed
|
9
|
+
#
|
10
|
+
# @!attribute [r] original_value
|
11
|
+
# @return [Object] the original value that was set when it was hooked
|
12
|
+
|
13
|
+
|
14
|
+
attr_reader :base_module, :constant_name, :original_value
|
15
|
+
|
16
|
+
# @param base_module [Module] the module this spy should be on
|
17
|
+
# @param constant_name [Symbol] the constant this spy is watching
|
5
18
|
def initialize(base_module, constant_name)
|
6
19
|
raise "#{base_module.inspect} is not a kind of Module" unless base_module.is_a? Module
|
7
20
|
raise "#{constant_name.inspect} is not a kind of Symbol" unless constant_name.is_a? Symbol
|
8
21
|
@base_module, @constant_name = base_module, constant_name.to_sym
|
9
22
|
@original_value = nil
|
10
23
|
@new_value = nil
|
11
|
-
@
|
24
|
+
@previously_defined = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# full name of spied constant
|
28
|
+
def name
|
29
|
+
"#{base_module.name}::#{constant_name}"
|
12
30
|
end
|
13
31
|
|
32
|
+
# stashes the original constant then overwrites it with nil
|
33
|
+
# @param opts [Hash{force => false}] set :force => true if you want it to ignore if the constant exists
|
34
|
+
# @return [self]
|
14
35
|
def hook(opts = {})
|
36
|
+
Nest.fetch(base_module).add(self)
|
37
|
+
Agency.instance.recruit(self)
|
15
38
|
opts[:force] ||= false
|
16
|
-
@
|
17
|
-
if @
|
39
|
+
@previously_defined = currently_defined?
|
40
|
+
if @previously_defined || !opts[:force]
|
18
41
|
@original_value = base_module.const_get(constant_name, false)
|
19
42
|
end
|
20
43
|
and_return(@new_value)
|
21
|
-
Nest.fetch(base_module).add(self)
|
22
|
-
Agency.instance.recruit(self)
|
23
44
|
self
|
24
45
|
end
|
25
46
|
|
47
|
+
# restores the original value of the constant or unsets it if it was unset
|
48
|
+
# @return [self]
|
26
49
|
def unhook
|
27
|
-
|
50
|
+
Nest.get(base_module).remove(self)
|
51
|
+
Agency.instance.retire(self)
|
52
|
+
|
53
|
+
if @previously_defined
|
28
54
|
and_return(@original_value)
|
29
55
|
end
|
30
56
|
@original_value = nil
|
31
|
-
|
32
|
-
Agency.instance.retire(self)
|
33
|
-
Nest.fetch(base_module).remove(self)
|
34
57
|
self
|
35
58
|
end
|
36
59
|
|
60
|
+
# unsets the constant
|
61
|
+
# @return [self]
|
37
62
|
def and_hide
|
38
|
-
base_module.send(:remove_const, constant_name)
|
63
|
+
base_module.send(:remove_const, constant_name) if currently_defined?
|
39
64
|
self
|
40
65
|
end
|
41
66
|
|
67
|
+
# sets the constant to the requested value
|
68
|
+
# @param value [Object]
|
69
|
+
# @return [self]
|
42
70
|
def and_return(value)
|
43
71
|
@new_value = value
|
44
|
-
|
72
|
+
and_hide
|
45
73
|
base_module.const_set(constant_name, @new_value)
|
46
74
|
self
|
47
75
|
end
|
48
76
|
|
77
|
+
# checks to see if this spy is hooked?
|
78
|
+
# @return [Boolean]
|
49
79
|
def hooked?
|
50
|
-
|
80
|
+
self.class.get(base_module, constant_name) == self
|
81
|
+
end
|
82
|
+
|
83
|
+
# checks to see if the constant is hidden?
|
84
|
+
# @return [Boolean]
|
85
|
+
def hidden?
|
86
|
+
hooked? && currently_defined?
|
87
|
+
end
|
88
|
+
|
89
|
+
# checks to see if the constant is currently defined?
|
90
|
+
# @return [Boolean]
|
91
|
+
def currently_defined?
|
92
|
+
base_module.const_defined?(constant_name, false)
|
93
|
+
end
|
94
|
+
|
95
|
+
# checks to see if the constant is previously defined?
|
96
|
+
# @return [Boolean]
|
97
|
+
def previously_defined?
|
98
|
+
@previously_defined
|
51
99
|
end
|
52
100
|
|
53
101
|
class << self
|
102
|
+
# creates a new constant spy and hooks the constant
|
103
|
+
# @return [Constant]
|
54
104
|
def on(base_module, constant_name)
|
55
105
|
new(base_module, constant_name).hook
|
56
106
|
end
|
57
107
|
|
108
|
+
# retrieves the spy for given constant and module and unhooks the constant
|
109
|
+
# from the module
|
110
|
+
# @return [Constant]
|
58
111
|
def off(base_module, constant_name)
|
59
112
|
get(base_module, constant_name).unhook
|
60
113
|
end
|
61
114
|
|
115
|
+
# retrieves the spy for given constnat and module or returns nil
|
116
|
+
# @return [Nil, Constant]
|
62
117
|
def get(base_module, constant_name)
|
63
|
-
Nest.get(base_module)
|
118
|
+
nest = Nest.get(base_module)
|
119
|
+
if nest
|
120
|
+
nest.hooked_constants[constant_name]
|
121
|
+
end
|
64
122
|
end
|
65
123
|
end
|
66
124
|
end
|