spy 0.1.0 → 0.2.1
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 +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
|