much-stub 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
- ---
2
- SHA1:
3
- metadata.gz: e5d7e06636c2654760be8dee2fbe8c80d9b86ef4
4
- data.tar.gz: ebca2dede9dd9f9305548d3d8eb9db88a4ec1bae
5
- SHA512:
6
- metadata.gz: 6e4b733a9dccaa7babf00419d86f7e6d112d1a84cfe306c854509e9109bedbaae1c22f2ed4d83edd95f05c571c9ce804251bd0ca75a790478703bf4a703497d8
7
- data.tar.gz: b41abee2b1ed680b2df84d1eb08c5854beb98001a5b774c4ef2311e583200b96cbdb2ccf1872068c98d050107a1e3fc23eecc02e283fe01cb8e211ce2365b875
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f4801d3a180d37bd01d02a52d1c524d38f432e4bb287a87d1e7753825efb180a
4
+ data.tar.gz: 422762b6602a2f05725fd61561e745bced7430b3e459ee7198a670ce86e922bd
5
+ SHA512:
6
+ metadata.gz: 3eb227fd853591c057e7869a6331cbd36ba0a308eb4a0a3a32313ea779ec740f89e32a297160fb40bd3400222af83693830c260df3c461d79d40e72d45d1df24
7
+ data.tar.gz: 8b7ab33cb886b60f4242bdaa6a1bcf9a5d1666c2b37a335eb01ab032d59841ff648b5bed5ffded02dfd40f2fe4451e1649ae45488a133c6773ef00670ae760d7
data/Gemfile CHANGED
@@ -2,4 +2,4 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'pry', "~> 0.9.0"
5
+ gem "pry", "~> 0.12.2"
data/README.md CHANGED
@@ -14,62 +14,329 @@ Note: this was originally implemented in and extracted from [Assert](https://git
14
14
  ## Usage
15
15
 
16
16
  ```ruby
17
- myclass = Class.new do
18
- def mymeth; 'meth'; end
19
- def myval(val); val; end
17
+ # Given this object/API
18
+
19
+ my_class = Class.new do
20
+ def my_method
21
+ "my-method"
22
+ end
23
+
24
+ def my_value(value)
25
+ value
26
+ end
20
27
  end
21
- myobj = myclass.new
28
+ my_object = my_class.new
22
29
 
23
- myobj.mymeth
24
- # => 'meth'
25
- myobj.myval(123)
30
+ my_object.my_method
31
+ # => "my-method"
32
+ my_object.my_value(123)
26
33
  # => 123
27
- myobj.myval(456)
34
+ my_object.my_value(456)
28
35
  # => 456
29
36
 
30
- MuchStub.stub(myobj, :mymeth)
31
- myobj.mymeth
32
- # => StubError: `mymeth` not stubbed.
33
- MuchStub.stub(myobj, :mymeth){ 'stub-meth' }
34
- myobj.mymeth
35
- # => 'stub-meth'
36
- myobj.mymeth(123)
37
+ # Create a new stub for the :my_method method
38
+
39
+ MuchStub.(my_object, :my_method)
40
+ my_object.my_method
41
+ # => StubError: `my_method` not stubbed.
42
+ MuchStub.(my_object, :my_method){ "stubbed-method" }
43
+ my_object.my_method
44
+ # => "stubbed-method"
45
+ my_object.my_method(123)
37
46
  # => StubError: arity mismatch
38
- MuchStub.stub(myobj, :mymeth).with(123){ 'stub-meth' }
47
+ MuchStub.(my_object, :my_method).with(123){ "stubbed-method" }
39
48
  # => StubError: arity mismatch
40
- MuchStub.stub_send(myobj, :mymeth) # call to the original method post-stub
41
- # => 'meth'
42
49
 
43
- MuchStub.stub(myobj, :myval){ 'stub-meth' }
50
+ # Call the original method after it has been stubbed.
51
+
52
+ MuchStub.stub_send(my_object, :my_method)
53
+ # => "my-method"
54
+
55
+ # Create a new stub for the :my_value method
56
+
57
+ MuchStub.(my_object, :my_value){ "stubbed-method" }
44
58
  # => StubError: arity mismatch
45
- MuchStub.stub(myobj, :myval).with(123){ |val| val.to_s }
46
- myobj.myval
59
+ MuchStub.(my_object, :my_value).with(123){ |val| val.to_s }
60
+ my_object.my_value
47
61
  # => StubError: arity mismatch
48
- myobj.myval(123)
49
- # => '123'
50
- myobj.myval(456)
51
- # => StubError: `myval(456)` not stubbed.
52
- MuchStub.stub_send(myobj, :myval, 123) # call to the original method post-stub
62
+ my_object.my_value(123)
63
+ # => "123"
64
+ my_object.my_value(456)
65
+ # => StubError: `my_value(456)` not stubbed.
66
+
67
+ # Call the original method after it has been stubbed.
68
+
69
+ MuchStub.stub_send(my_object, :my_value, 123)
53
70
  # => 123
54
- MuchStub.stub_send(myobj, :myval, 456)
71
+ MuchStub.stub_send(my_object, :my_value, 456)
55
72
  # => 456
56
73
 
57
- MuchStub.unstub(myobj, :mymeth)
58
- MuchStub.unstub(myobj, :myval)
74
+ # Unstub individual stubs
75
+
76
+ MuchStub.unstub(my_object, :my_method)
77
+ MuchStub.unstub(my_object, :my_value)
59
78
 
60
- myobj.mymeth
61
- # => 'meth'
62
- myobj.myval(123)
79
+ # OR blanket unstub all stubs
80
+
81
+ MuchStub.unstub!
82
+
83
+ # The original API/behavior is preserved after unstubbing
84
+
85
+ my_object.my_method
86
+ # => "my-method"
87
+ my_object.my_value(123)
63
88
  # => 123
64
- myobj.myval(456)
89
+ my_object.my_value(456)
65
90
  # => 456
66
91
  ```
67
92
 
93
+ ### Stubs for spying
94
+
95
+ ```ruby
96
+ # Given this object/API
97
+
98
+ my_class = Class.new do
99
+ def basic_method(value)
100
+ value
101
+ end
102
+
103
+ def iterator_method(items, &block)
104
+ items.each(&block)
105
+ end
106
+ end
107
+ my_object = my_class.new
108
+
109
+ # Store method call arguments/blocks for spying.
110
+
111
+ basic_method_called_with = nil
112
+ MuchStub.(my_object, :basic_method) { |*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
124
+ }
125
+
126
+ my_object.basic_method(123)
127
+ basic_method_called_with.args
128
+ # => [123]
129
+
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
144
+ MuchStub.(my_object, :iterator_method) { |*args, &block|
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
150
+ }
151
+
152
+ my_object.iterator_method([1, 2, 3], &:to_s)
153
+ iterator_method_called_with.args
154
+ # => [[1, 2, 3]]
155
+ iterator_method_called_with.block
156
+ # => #<Proc:0x00007fb083a6feb0(&:to_s)>
157
+
158
+ # Count method calls for spying.
159
+
160
+ basic_method_call_count = 0
161
+ MuchStub.(my_object, :basic_method) {
162
+ basic_method_call_count += 1
163
+ }
164
+
165
+ my_object.basic_method(123)
166
+ basic_method_call_count
167
+ # => 1
168
+
169
+ # Count method calls and store arguments for spying.
170
+
171
+ basic_method_calls = []
172
+ MuchStub.(my_object, :basic_method) { |*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
178
+ }
179
+
180
+ my_object.basic_method(123)
181
+ basic_method_calls.size
182
+ # => 1
183
+ basic_method_calls.first.args
184
+ # => [123]
185
+ ```
186
+
187
+ ### Stubs for test doubles.
188
+
189
+ ```ruby
190
+ # Given this object/API ...
191
+
192
+ my_class = Class.new do
193
+ def build_thing(thing_value);
194
+ Thing.new(value)
195
+ end
196
+ end
197
+ my_object = my_class.new
198
+
199
+ # ... and this Test Double.
200
+ class FakeThing
201
+ attr_reader :built_with
202
+
203
+ def initialize(*args)
204
+ @built_with = args
205
+ end
206
+ end
207
+
208
+ # Stub in the test double.
209
+
210
+ MuchStub.(my_object, :build_thing) { |*args|
211
+ FakeThing.new(*args)
212
+ }
213
+
214
+ thing = my_object.build_thing(123)
215
+ thing.built_with
216
+ # => [123]
217
+ ```
218
+
219
+ ### `MuchStub.tap`
220
+
221
+ Use the `.tap` method to spy on method calls while preserving the original method return value and behavior.
222
+
223
+ ```ruby
224
+ # Given this object/API
225
+
226
+ my_class = Class.new do
227
+ def basic_method(value)
228
+ value.to_s
229
+ end
230
+ end
231
+ my_object = my_class.new
232
+
233
+ # Normal stubs override the original behavior and return value...
234
+ basic_method_called_with = nil
235
+ MuchStub.(my_object, :basic_method) { |*args|
236
+ basic_method_called_with = args
237
+ }
238
+
239
+ # ... in this case not converting the value to a String and returning it and
240
+ # instead returning the arguments passed to the method.
241
+ my_object.basic_method(123)
242
+ # => [123]
243
+ basic_method_called_with
244
+ # => [123]
245
+
246
+ # Use `MuchStub.tap` to preserve the methods behavior and also spy.
247
+
248
+ basic_method_called_with = nil
249
+ MuchStub.tap(my_object, :basic_method) { |value, *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
255
+ }
256
+
257
+ my_object.basic_method(123)
258
+ # => "123"
259
+ basic_method_called_with.args
260
+ # => [123]
261
+ ```
262
+
263
+ #### Late-bound stubs using `MuchStub.tap`
264
+
265
+ Use the `.tap` method to stub any return values of method calls.
266
+
267
+ ```ruby
268
+ # Given:
269
+
270
+ class Thing
271
+ attr_reader :value
272
+
273
+ def initialize(value)
274
+ @value = value
275
+ end
276
+ end
277
+
278
+ my_class = Class.new do
279
+ def thing(value)
280
+ Thing.new(value)
281
+ end
282
+ end
283
+ my_object = my_class.new
284
+
285
+ # Use `MuchStub.tap` to stub any thing instances created by `my_object.thing`
286
+ # (and also spy on the call arguments)
287
+
288
+ thing_built_with = nil
289
+ MuchStub.tap(my_object, :thing) { |thing, *args|
290
+ thing_built_with = args
291
+ MuchStub.(thing, :value) { 456 }
292
+ }
293
+
294
+ thing = my_object.thing(123)
295
+ # => #<Thing:0x00007fd5ca9df510 @value=123>
296
+ thing_built_with
297
+ # => [123]
298
+ thing.value
299
+ # => 456
300
+ ```
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
+
68
335
  ## Installation
69
336
 
70
337
  Add this line to your application's Gemfile:
71
338
 
72
- gem 'much-stub'
339
+ gem "much-stub"
73
340
 
74
341
  And then execute:
75
342
 
@@ -83,6 +350,6 @@ Or install it yourself as:
83
350
 
84
351
  1. Fork it
85
352
  2. Create your feature branch (`git checkout -b my-new-feature`)
86
- 3. Commit your changes (`git commit -am 'Added some feature'`)
353
+ 3. Commit your changes (`git commit -am "Added some feature"`)
87
354
  4. Push to the branch (`git push origin my-new-feature`)
88
355
  5. Create new Pull Request
@@ -1,19 +1,44 @@
1
1
  require "much-stub/version"
2
2
 
3
- module MuchStub
3
+ require "much-stub/call"
4
+ require "much-stub/call_spy"
4
5
 
6
+ module MuchStub
5
7
  def self.stubs
6
8
  @stubs ||= {}
7
9
  end
8
10
 
11
+ def self.stub_key(obj, meth)
12
+ MuchStub::Stub.key(obj, meth)
13
+ end
14
+
15
+ def self.arity_matches?(method, args)
16
+ return true if method.arity == args.size # mandatory args
17
+ return true if method.arity < 0 && args.size >= (method.arity+1).abs # variable args
18
+ return false
19
+ end
20
+
9
21
  def self.stub(obj, meth, &block)
10
- (self.stubs[MuchStub::Stub.key(obj, meth)] ||= begin
11
- MuchStub::Stub.new(obj, meth, caller_locations)
12
- end).tap{ |s| s.do = block }
22
+ key = self.stub_key(obj, meth)
23
+ self.stubs[key] ||= MuchStub::Stub.new(obj, meth, caller_locations)
24
+ self.stubs[key].tap{ |s| s.do = block }
25
+ end
26
+
27
+ def self.call(*args, &block)
28
+ self.stub(*args, &block)
29
+ end
30
+
31
+ def self.stub_on_call(*args, &on_call_block)
32
+ self.stub(*args).on_call(&on_call_block)
33
+ end
34
+
35
+ def self.on_call(*args, &on_call_block)
36
+ self.stub_on_call(*args, &on_call_block)
13
37
  end
14
38
 
15
39
  def self.unstub(obj, meth)
16
- (self.stubs.delete(MuchStub::Stub.key(obj, meth)) || MuchStub::NullStub.new).teardown
40
+ key = self.stub_key(obj, meth)
41
+ (self.stubs.delete(key) || MuchStub::NullStub.new).teardown
17
42
  end
18
43
 
19
44
  def self.unstub!
@@ -28,8 +53,31 @@ module MuchStub
28
53
  stub.call_method(args, &block)
29
54
  end
30
55
 
31
- class Stub
56
+ def self.tap(obj, meth, &tap_block)
57
+ self.stub(obj, meth) { |*args, &block|
58
+ self.stub_send(obj, meth, *args, &block).tap { |value|
59
+ tap_block.call(value, *args, &block) if tap_block
60
+ }
61
+ }
62
+ end
32
63
 
64
+ def self.tap_on_call(obj, meth, &on_call_block)
65
+ self.tap(obj, meth) { |value, *args, &block|
66
+ on_call_block.call(value, MuchStub::Call.new(*args, &block)) if on_call_block
67
+ }
68
+ end
69
+
70
+ def self.spy(obj, *meths, **return_values)
71
+ MuchStub::CallSpy.new(**return_values).call_spy_tap do |spy|
72
+ meths.each do |meth|
73
+ self.stub(obj, meth) { |*args, &block|
74
+ spy.__send__(meth, *args, &block)
75
+ }
76
+ end
77
+ end
78
+ end
79
+
80
+ class Stub
33
81
  def self.key(object, method_name)
34
82
  "--#{object.object_id}--#{method_name}--"
35
83
  end
@@ -60,27 +108,45 @@ module MuchStub
60
108
 
61
109
  def call(args, orig_caller = nil, &block)
62
110
  orig_caller ||= caller_locations
63
- unless arity_matches?(args)
64
- msg = "arity mismatch on `#{@method_name}`: " \
65
- "expected #{number_of_args(@method.arity)}, " \
66
- "called with #{args.size}"
67
- raise StubArityError, msg, orig_caller.map(&:to_s)
111
+ unless MuchStub.arity_matches?(@method, args)
112
+ raise(
113
+ StubArityError.new(
114
+ @method,
115
+ args,
116
+ method_name: @method_name,
117
+ backtrace: orig_caller))
68
118
  end
69
119
  lookup(args, orig_caller).call(*args, &block)
70
- rescue NotStubbedError => exception
120
+ rescue NotStubbedError
71
121
  @lookup.rehash
72
122
  lookup(args, orig_caller).call(*args, &block)
73
123
  end
74
124
 
75
125
  def with(*args, &block)
76
126
  orig_caller = caller_locations
77
- unless arity_matches?(args)
78
- msg = "arity mismatch on `#{@method_name}`: " \
79
- "expected #{number_of_args(@method.arity)}, " \
80
- "stubbed with #{args.size}"
81
- raise StubArityError, msg, orig_caller.map(&:to_s)
127
+ unless MuchStub.arity_matches?(@method, args)
128
+ raise(
129
+ StubArityError.new(
130
+ @method,
131
+ args,
132
+ method_name: @method_name,
133
+ backtrace: orig_caller))
82
134
  end
83
135
  @lookup[args] = block
136
+ self
137
+ end
138
+
139
+ def on_call(&on_call_block)
140
+ stub_block =
141
+ ->(*args, &block) {
142
+ on_call_block.call(MuchStub::Call.new(*args, &block)) if on_call_block
143
+ }
144
+ if @lookup.empty?
145
+ @do = stub_block
146
+ elsif @lookup.has_value?(nil)
147
+ @lookup.transform_values!{ |value| value.nil? ? stub_block : value }
148
+ end
149
+ self
84
150
  end
85
151
 
86
152
  def teardown
@@ -91,7 +157,7 @@ module MuchStub
91
157
  end
92
158
 
93
159
  def inspect
94
- "#<#{self.class}:#{'0x0%x' % (object_id << 1)}" \
160
+ "#<#{self.class}:#{"0x0%x" % (object_id << 1)}" \
95
161
  " @method_name=#{@method_name.inspect}" \
96
162
  ">"
97
163
  end
@@ -128,7 +194,7 @@ module MuchStub
128
194
  end
129
195
 
130
196
  def lookup(args, orig_caller)
131
- @lookup.fetch(args) do
197
+ @lookup.fetch(args) {
132
198
  self.do || begin
133
199
  msg = "#{inspect_call(args)} not stubbed."
134
200
  inspect_lookup_stubs.tap do |stubs|
@@ -136,13 +202,11 @@ module MuchStub
136
202
  end
137
203
  raise NotStubbedError, msg, orig_caller.map(&:to_s)
138
204
  end
139
- end
140
- end
141
-
142
- def arity_matches?(args)
143
- return true if @method.arity == args.size # mandatory args
144
- return true if @method.arity < 0 && args.size >= (@method.arity+1).abs # variable args
145
- return false
205
+ } ||
206
+ raise(
207
+ StubError,
208
+ "#{inspect_call(args)} stubbed with no block.",
209
+ orig_caller.map(&:to_s))
146
210
  end
147
211
 
148
212
  def inspect_lookup_stubs
@@ -150,37 +214,47 @@ module MuchStub
150
214
  end
151
215
 
152
216
  def inspect_call(args)
153
- "`#{@method_name}(#{args.map(&:inspect).join(',')})`"
217
+ "`#{@method_name}(#{args.map(&:inspect).join(",")})`"
154
218
  end
155
-
156
- def number_of_args(arity)
157
- if arity < 0
158
- "at least #{(arity + 1).abs}"
159
- else
160
- arity
161
- end
162
- end
163
-
164
219
  end
165
220
 
166
221
  StubError = Class.new(ArgumentError)
167
222
  NotStubbedError = Class.new(StubError)
168
- StubArityError = Class.new(StubError)
223
+ StubArityError =
224
+ Class.new(StubError) do
225
+ def initialize(method, args, method_name:, backtrace:)
226
+ msg = "arity mismatch on `#{method_name}`: " \
227
+ "expected #{number_of_args(method.arity)}, " \
228
+ "called with #{args.size}"
229
+
230
+ super(msg)
231
+ set_backtrace(Array(backtrace).map(&:to_s))
232
+ end
233
+
234
+ private
235
+
236
+ def number_of_args(arity)
237
+ if arity < 0
238
+ "at least #{(arity + 1).abs}"
239
+ else
240
+ arity
241
+ end
242
+ end
243
+ end
169
244
 
170
245
  NullStub = Class.new do
171
246
  def teardown; end # no-op
172
247
  end
173
248
 
174
249
  module ParameterList
175
-
176
- LETTERS = ('a'..'z').to_a.freeze
250
+ LETTERS = ("a".."z").to_a.freeze
177
251
 
178
252
  def self.new(object, method_name)
179
253
  arity = get_arity(object, method_name)
180
254
  params = build_params_from_arity(arity)
181
- params << '*args' if arity < 0
182
- params << '&block'
183
- params.join(', ')
255
+ params << "*args" if arity < 0
256
+ params << "&block"
257
+ params.join(", ")
184
258
  end
185
259
 
186
260
  private
@@ -201,9 +275,7 @@ module MuchStub
201
275
  number_of_letters, letter_index = param_index.divmod(LETTERS.size)
202
276
  LETTERS[letter_index] * number_of_letters
203
277
  end
204
-
205
278
  end
206
-
207
279
  end
208
280
 
209
281
  # Kernel#caller_locations polyfill for pre ruby 2.0.0