much-stub 0.1.2 → 0.1.7
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.
- checksums.yaml +4 -4
- data/.l.yml +8 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -0
- data/.t.yml +6 -0
- data/Gemfile +5 -1
- data/README.md +78 -12
- data/lib/much-stub.rb +124 -50
- data/lib/much-stub/call.rb +17 -0
- data/lib/much-stub/call_spy.rb +125 -0
- data/lib/much-stub/version.rb +3 -1
- data/much-stub.gemspec +20 -11
- data/test/helper.rb +3 -1
- data/test/support/factory.rb +2 -0
- data/test/system/much-stub_tests.rb +115 -77
- data/test/unit/call_spy_tests.rb +111 -0
- data/test/unit/call_tests.rb +70 -0
- data/test/unit/much-stub_tests.rb +220 -82
- metadata +31 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 15204360d9518246e4abaa703670f75489b26364a262f61397956bcd6292bb91
|
4
|
+
data.tar.gz: e53db7a648e52be63ea31d6dc930d7c009e65604d05a4e1e447edcfb3ef7b296
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5892e22dc64f46400a115d42fb7b7da3d5b82387fda5b8c2ec5f807d6a9b47e4dca5a92d86a3a97898d5f6e85885808065d591e53aba7d78303afc1396fce4a4
|
7
|
+
data.tar.gz: 4476a777e7ee869f59080f5c0bc40d2d8736d77d01588f193ea6e826f424d3abf110ca49a60437267e7e16d5578c2f6f74bb24ce810940af82154ce0e08e7199
|
data/.l.yml
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.8
|
data/.t.yml
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -110,24 +110,49 @@ my_object = my_class.new
|
|
110
110
|
|
111
111
|
basic_method_called_with = nil
|
112
112
|
MuchStub.(my_object, :basic_method) { |*args|
|
113
|
-
basic_method_called_with = args
|
113
|
+
basic_method_called_with = MuchStub::Call.new(*args)
|
114
|
+
}
|
115
|
+
# OR
|
116
|
+
MuchStub.(my_object, :basic_method).on_call { |call|
|
117
|
+
basic_method_called_with = call
|
118
|
+
}
|
119
|
+
# OR
|
120
|
+
MuchStub.on_call(my_object, :basic_method) { |call|
|
121
|
+
# MucStub.on_call(...) { ... } is equivalent to
|
122
|
+
# MuchStub.(...).on_call { ... }
|
123
|
+
basic_method_called_with = call
|
114
124
|
}
|
115
125
|
|
116
126
|
my_object.basic_method(123)
|
117
|
-
basic_method_called_with
|
127
|
+
basic_method_called_with.args
|
118
128
|
# => [123]
|
119
129
|
|
120
|
-
|
121
|
-
|
130
|
+
basic_method_called_with = nil
|
131
|
+
MuchStub.(my_object, :basic_method).with(4, 5, 6) { |*args|
|
132
|
+
basic_method_called_with = MuchStub::Call.new(*args)
|
133
|
+
}
|
134
|
+
# OR
|
135
|
+
MuchStub.(my_object, :basic_method).with(4, 5, 6).on_call { |call|
|
136
|
+
basic_method_called_with = call
|
137
|
+
}
|
138
|
+
|
139
|
+
my_object.basic_method(4, 5, 6)
|
140
|
+
basic_method_called_with.args
|
141
|
+
# => [4,5,6]
|
142
|
+
|
143
|
+
iterator_method_called_with = nil
|
122
144
|
MuchStub.(my_object, :iterator_method) { |*args, &block|
|
123
|
-
|
124
|
-
|
145
|
+
iterator_method_called_with = MuchStub::Call.new(*args)
|
146
|
+
}
|
147
|
+
# OR
|
148
|
+
MuchStub.(my_object, :iterator_method).on_call { |call|
|
149
|
+
iterator_method_called_with = call
|
125
150
|
}
|
126
151
|
|
127
152
|
my_object.iterator_method([1, 2, 3], &:to_s)
|
128
|
-
|
153
|
+
iterator_method_called_with.args
|
129
154
|
# => [[1, 2, 3]]
|
130
|
-
|
155
|
+
iterator_method_called_with.block
|
131
156
|
# => #<Proc:0x00007fb083a6feb0(&:to_s)>
|
132
157
|
|
133
158
|
# Count method calls for spying.
|
@@ -145,13 +170,17 @@ basic_method_call_count
|
|
145
170
|
|
146
171
|
basic_method_calls = []
|
147
172
|
MuchStub.(my_object, :basic_method) { |*args|
|
148
|
-
basic_method_calls << args
|
173
|
+
basic_method_calls << MuchStub::Call.new(*args)
|
174
|
+
}
|
175
|
+
# OR
|
176
|
+
MuchStub.(my_object, :basic_method).on_call { |call|
|
177
|
+
basic_method_calls << call
|
149
178
|
}
|
150
179
|
|
151
180
|
my_object.basic_method(123)
|
152
181
|
basic_method_calls.size
|
153
182
|
# => 1
|
154
|
-
basic_method_calls.first
|
183
|
+
basic_method_calls.first.args
|
155
184
|
# => [123]
|
156
185
|
```
|
157
186
|
|
@@ -218,12 +247,16 @@ basic_method_called_with
|
|
218
247
|
|
219
248
|
basic_method_called_with = nil
|
220
249
|
MuchStub.tap(my_object, :basic_method) { |value, *args|
|
221
|
-
basic_method_called_with = args
|
250
|
+
basic_method_called_with = MuchStub::Call.new(*args)
|
251
|
+
}
|
252
|
+
# OR
|
253
|
+
MuchStub.tap_on_call(my_object, :basic_method) { |value, call|
|
254
|
+
basic_method_called_with = call
|
222
255
|
}
|
223
256
|
|
224
257
|
my_object.basic_method(123)
|
225
258
|
# => "123"
|
226
|
-
basic_method_called_with
|
259
|
+
basic_method_called_with.args
|
227
260
|
# => [123]
|
228
261
|
```
|
229
262
|
|
@@ -266,6 +299,39 @@ thing.value
|
|
266
299
|
# => 456
|
267
300
|
```
|
268
301
|
|
302
|
+
### `MuchStub.spy`
|
303
|
+
|
304
|
+
Use the `.spy` method to spy on method calls. This is especially helpful for spying on _chained_ method calls.
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
# Given this object/API
|
308
|
+
|
309
|
+
myclass = Class.new do
|
310
|
+
def one; self; end
|
311
|
+
def two(val); self; end
|
312
|
+
def three; self; end
|
313
|
+
def ready?; false; end
|
314
|
+
end
|
315
|
+
myobj = myclass.new
|
316
|
+
|
317
|
+
spy =
|
318
|
+
MuchStub.spy(myobj :one, :two, :three, ready?: true)
|
319
|
+
|
320
|
+
assert_equal spy, myobj.one
|
321
|
+
assert_equal spy, myobj.two("a")
|
322
|
+
assert_equal spy, myobj.three
|
323
|
+
|
324
|
+
assert_true myobj.one.two("b").three.ready?
|
325
|
+
|
326
|
+
assert_kind_of MuchStub::CallSpy, spy
|
327
|
+
assert_equal 2, spy.one_call_count
|
328
|
+
assert_equal 2, spy.two_call_count
|
329
|
+
assert_equal 2, spy.three_call_count
|
330
|
+
assert_equal 1, spy.ready_predicate_call_count
|
331
|
+
assert_equal ["b"], spy.two_last_called_with.args
|
332
|
+
assert_true spy.ready_predicate_called?
|
333
|
+
```
|
334
|
+
|
269
335
|
## Installation
|
270
336
|
|
271
337
|
Add this line to your application's Gemfile:
|
data/lib/much-stub.rb
CHANGED
@@ -1,5 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "much-stub/version"
|
2
4
|
|
5
|
+
require "much-stub/call"
|
6
|
+
require "much-stub/call_spy"
|
7
|
+
|
3
8
|
module MuchStub
|
4
9
|
def self.stubs
|
5
10
|
@stubs ||= {}
|
@@ -9,39 +14,72 @@ module MuchStub
|
|
9
14
|
MuchStub::Stub.key(obj, meth)
|
10
15
|
end
|
11
16
|
|
12
|
-
def self.
|
13
|
-
|
17
|
+
def self.arity_matches?(method, args)
|
18
|
+
# mandatory args
|
19
|
+
return true if method.arity == args.size
|
20
|
+
# variable args
|
21
|
+
return true if method.arity < 0 && args.size >= (method.arity + 1).abs
|
22
|
+
|
23
|
+
false
|
14
24
|
end
|
15
25
|
|
16
26
|
def self.stub(obj, meth, &block)
|
17
|
-
key =
|
18
|
-
|
19
|
-
|
27
|
+
key = stub_key(obj, meth)
|
28
|
+
stubs[key] ||= MuchStub::Stub.new(obj, meth, caller_locations)
|
29
|
+
stubs[key].tap{ |s| s.do = block }
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.call(*args, &block)
|
33
|
+
stub(*args, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.stub_on_call(*args, &on_call_block)
|
37
|
+
stub(*args).on_call(&on_call_block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.on_call(*args, &on_call_block)
|
41
|
+
stub_on_call(*args, &on_call_block)
|
20
42
|
end
|
21
43
|
|
22
44
|
def self.unstub(obj, meth)
|
23
|
-
key =
|
24
|
-
(
|
45
|
+
key = stub_key(obj, meth)
|
46
|
+
(stubs.delete(key) || MuchStub::NullStub.new).teardown
|
25
47
|
end
|
26
48
|
|
27
49
|
def self.unstub!
|
28
|
-
|
50
|
+
stubs.keys.each{ |key| stubs.delete(key).teardown }
|
29
51
|
end
|
30
52
|
|
31
53
|
def self.stub_send(obj, meth, *args, &block)
|
32
54
|
orig_caller = caller_locations
|
33
|
-
stub =
|
55
|
+
stub = stubs.fetch(MuchStub::Stub.key(obj, meth)) do
|
34
56
|
raise NotStubbedError, "`#{meth}` not stubbed.", orig_caller.map(&:to_s)
|
35
57
|
end
|
36
58
|
stub.call_method(args, &block)
|
37
59
|
end
|
38
60
|
|
39
61
|
def self.tap(obj, meth, &tap_block)
|
40
|
-
|
41
|
-
|
42
|
-
tap_block
|
43
|
-
|
44
|
-
|
62
|
+
stub(obj, meth) do |*args, &block|
|
63
|
+
stub_send(obj, meth, *args, &block).tap do |value|
|
64
|
+
tap_block&.call(value, *args, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.tap_on_call(obj, meth, &on_call_block)
|
70
|
+
tap(obj, meth) do |value, *args, &block|
|
71
|
+
on_call_block&.call(value, MuchStub::Call.new(*args, &block))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.spy(obj, *meths, **return_values)
|
76
|
+
MuchStub::CallSpy.new(**return_values).call_spy_tap do |spy|
|
77
|
+
meths.each do |meth|
|
78
|
+
stub(obj, meth) do |*args, &block|
|
79
|
+
spy.__send__(meth, *args, &block)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
45
83
|
end
|
46
84
|
|
47
85
|
class Stub
|
@@ -75,11 +113,15 @@ module MuchStub
|
|
75
113
|
|
76
114
|
def call(args, orig_caller = nil, &block)
|
77
115
|
orig_caller ||= caller_locations
|
78
|
-
unless arity_matches?(args)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
116
|
+
unless MuchStub.arity_matches?(@method, args)
|
117
|
+
raise(
|
118
|
+
StubArityError.new(
|
119
|
+
@method,
|
120
|
+
args,
|
121
|
+
method_name: @method_name,
|
122
|
+
backtrace: orig_caller,
|
123
|
+
),
|
124
|
+
)
|
83
125
|
end
|
84
126
|
lookup(args, orig_caller).call(*args, &block)
|
85
127
|
rescue NotStubbedError
|
@@ -89,13 +131,31 @@ module MuchStub
|
|
89
131
|
|
90
132
|
def with(*args, &block)
|
91
133
|
orig_caller = caller_locations
|
92
|
-
unless arity_matches?(args)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
134
|
+
unless MuchStub.arity_matches?(@method, args)
|
135
|
+
raise(
|
136
|
+
StubArityError.new(
|
137
|
+
@method,
|
138
|
+
args,
|
139
|
+
method_name: @method_name,
|
140
|
+
backtrace: orig_caller,
|
141
|
+
),
|
142
|
+
)
|
97
143
|
end
|
98
144
|
@lookup[args] = block
|
145
|
+
self
|
146
|
+
end
|
147
|
+
|
148
|
+
def on_call(&on_call_block)
|
149
|
+
stub_block =
|
150
|
+
->(*args, &block){
|
151
|
+
on_call_block&.call(MuchStub::Call.new(*args, &block))
|
152
|
+
}
|
153
|
+
if @lookup.empty?
|
154
|
+
@do = stub_block
|
155
|
+
elsif @lookup.value?(nil)
|
156
|
+
@lookup.transform_values!{ |value| value.nil? ? stub_block : value }
|
157
|
+
end
|
158
|
+
self
|
99
159
|
end
|
100
160
|
|
101
161
|
def teardown
|
@@ -106,7 +166,7 @@ module MuchStub
|
|
106
166
|
end
|
107
167
|
|
108
168
|
def inspect
|
109
|
-
"#<#{self.class}:#{"0x0%x"
|
169
|
+
"#<#{self.class}:#{format("0x0%x", (object_id << 1))}" \
|
110
170
|
" @method_name=#{@method_name.inspect}" \
|
111
171
|
">"
|
112
172
|
end
|
@@ -118,7 +178,7 @@ module MuchStub
|
|
118
178
|
msg = "#{object.inspect} does not respond to `#{@method_name}`"
|
119
179
|
raise StubError, msg, orig_caller.map(&:to_s)
|
120
180
|
end
|
121
|
-
is_constant = object.
|
181
|
+
is_constant = object.is_a?(Module)
|
122
182
|
local_object_methods = object.methods(false).map(&:to_s)
|
123
183
|
all_object_methods = object.methods.map(&:to_s)
|
124
184
|
if (is_constant && !local_object_methods.include?(@method_name)) ||
|
@@ -129,7 +189,8 @@ module MuchStub
|
|
129
189
|
method
|
130
190
|
end
|
131
191
|
|
132
|
-
|
192
|
+
# already stubbed
|
193
|
+
unless local_object_methods.include?(@name)
|
133
194
|
@metaclass.send(:alias_method, @name, @method_name)
|
134
195
|
end
|
135
196
|
@method = object.method(@name)
|
@@ -144,20 +205,20 @@ module MuchStub
|
|
144
205
|
|
145
206
|
def lookup(args, orig_caller)
|
146
207
|
@lookup.fetch(args) do
|
147
|
-
self.do ||
|
208
|
+
self.do ||
|
209
|
+
begin
|
148
210
|
msg = "#{inspect_call(args)} not stubbed."
|
149
211
|
inspect_lookup_stubs.tap do |stubs|
|
150
|
-
msg += "\nStubs:\n#{stubs}"
|
212
|
+
msg += "\nStubs:\n#{stubs}" unless stubs.empty?
|
151
213
|
end
|
152
214
|
raise NotStubbedError, msg, orig_caller.map(&:to_s)
|
153
215
|
end
|
154
|
-
end
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
return false
|
216
|
+
end ||
|
217
|
+
raise(
|
218
|
+
StubError,
|
219
|
+
"#{inspect_call(args)} stubbed with no block.",
|
220
|
+
orig_caller.map(&:to_s),
|
221
|
+
)
|
161
222
|
end
|
162
223
|
|
163
224
|
def inspect_lookup_stubs
|
@@ -167,23 +228,38 @@ module MuchStub
|
|
167
228
|
def inspect_call(args)
|
168
229
|
"`#{@method_name}(#{args.map(&:inspect).join(",")})`"
|
169
230
|
end
|
170
|
-
|
171
|
-
def number_of_args(arity)
|
172
|
-
if arity < 0
|
173
|
-
"at least #{(arity + 1).abs}"
|
174
|
-
else
|
175
|
-
arity
|
176
|
-
end
|
177
|
-
end
|
178
231
|
end
|
179
232
|
|
180
233
|
StubError = Class.new(ArgumentError)
|
181
234
|
NotStubbedError = Class.new(StubError)
|
182
|
-
StubArityError =
|
235
|
+
StubArityError =
|
236
|
+
Class.new(StubError) do
|
237
|
+
def initialize(method, args, method_name:, backtrace:)
|
238
|
+
msg = "arity mismatch on `#{method_name}`: " \
|
239
|
+
"expected #{number_of_args(method.arity)}, " \
|
240
|
+
"called with #{args.size}"
|
183
241
|
|
184
|
-
|
185
|
-
|
186
|
-
|
242
|
+
super(msg)
|
243
|
+
set_backtrace(Array(backtrace).map(&:to_s))
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def number_of_args(arity)
|
249
|
+
if arity < 0
|
250
|
+
"at least #{(arity + 1).abs}"
|
251
|
+
else
|
252
|
+
arity
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
NullStub =
|
258
|
+
Class.new do
|
259
|
+
def teardown
|
260
|
+
# no-op
|
261
|
+
end
|
262
|
+
end
|
187
263
|
|
188
264
|
module ParameterList
|
189
265
|
LETTERS = ("a".."z").to_a.freeze
|
@@ -196,8 +272,6 @@ module MuchStub
|
|
196
272
|
params.join(", ")
|
197
273
|
end
|
198
274
|
|
199
|
-
private
|
200
|
-
|
201
275
|
def self.get_arity(object, method_name)
|
202
276
|
object.method(method_name).arity
|
203
277
|
rescue NameError
|