much-stub 0.1.1 → 0.1.6

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
1
  ---
2
2
  SHA256:
3
- metadata.gz: 405abaae599baf274c0eea900413f3c534c3acf1a19b4a83a356f5c61640025b
4
- data.tar.gz: e68b22427ea8c78b87023576611a94c750a9d4ed75989cc72b7cdc96a56ebf2d
3
+ metadata.gz: a6dc9e4397c7c270a2678eac9f4761adb80bdd842e6708417844855639c6acfa
4
+ data.tar.gz: 57f89b55ddaf3b17a2444f7bbd40d2e8f9b1ad3046d1634021412f8847df605b
5
5
  SHA512:
6
- metadata.gz: d1cafda498eceb26c4d41a4b4867c0376dc36d34149454350e3ac4ef8a04623ee747a6d28639e417cac93eeb1f4950c1fda696153be860c61e9fef8be8f36a3e
7
- data.tar.gz: 416559df2f95b8faa18fac713e924e5af8ac128b2b385a91623f32939396488f9f2a0ecef01649c3099b076e385270cbae2f5f79a84e0e26d54c689c193e7e7e
6
+ metadata.gz: b57f84be1092bba24dfd51ace398e8116941a16e17f8698436158309305672e55076b46bb4b5695763c97530f7cbfce6f974de6b331432336d7b0aad8a2ecfe0
7
+ data.tar.gz: 4705afaee215d93ed0a998949dd70dd2d13f03aa5fac9a31093940be16799ad43b2878df7870a046b79a1a4e2c9625bb0f5195896c371c24728600445c18c55a
data/Gemfile CHANGED
@@ -1,5 +1,7 @@
1
1
  source "https://rubygems.org"
2
2
 
3
+ ruby "~> 2.5"
4
+
3
5
  gemspec
4
6
 
5
- gem 'pry', "~> 0.9.0"
7
+ gem "pry"
data/README.md CHANGED
@@ -16,77 +16,327 @@ Note: this was originally implemented in and extracted from [Assert](https://git
16
16
  ```ruby
17
17
  # Given this object/API
18
18
 
19
- myclass = Class.new do
20
- def mymeth; 'meth'; end
21
- def myval(val); val; end
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
22
27
  end
23
- myobj = myclass.new
28
+ my_object = my_class.new
24
29
 
25
- myobj.mymeth
26
- # => 'meth'
27
- myobj.myval(123)
30
+ my_object.my_method
31
+ # => "my-method"
32
+ my_object.my_value(123)
28
33
  # => 123
29
- myobj.myval(456)
34
+ my_object.my_value(456)
30
35
  # => 456
31
36
 
32
- # Create a new stub for the :mymeth method
37
+ # Create a new stub for the :my_method method
33
38
 
34
- MuchStub.(myobj, :mymeth)
35
- myobj.mymeth
36
- # => StubError: `mymeth` not stubbed.
37
- MuchStub.(myobj, :mymeth){ 'stub-meth' }
38
- myobj.mymeth
39
- # => 'stub-meth'
40
- myobj.mymeth(123)
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)
41
46
  # => StubError: arity mismatch
42
- MuchStub.(myobj, :mymeth).with(123){ 'stub-meth' }
47
+ MuchStub.(my_object, :my_method).with(123){ "stubbed-method" }
43
48
  # => StubError: arity mismatch
44
- MuchStub.stub_send(myobj, :mymeth) # call to the original method post-stub
45
- # => 'meth'
46
49
 
47
- # Create a new stub for the :myval method
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
48
56
 
49
- MuchStub.(myobj, :myval){ 'stub-meth' }
57
+ MuchStub.(my_object, :my_value){ "stubbed-method" }
50
58
  # => StubError: arity mismatch
51
- MuchStub.(myobj, :myval).with(123){ |val| val.to_s }
52
- myobj.myval
59
+ MuchStub.(my_object, :my_value).with(123){ |val| val.to_s }
60
+ my_object.my_value
53
61
  # => StubError: arity mismatch
54
- myobj.myval(123)
55
- # => '123'
56
- myobj.myval(456)
57
- # => StubError: `myval(456)` not stubbed.
62
+ my_object.my_value(123)
63
+ # => "123"
64
+ my_object.my_value(456)
65
+ # => StubError: `my_value(456)` not stubbed.
58
66
 
59
- # Call to the original method post-stub
67
+ # Call the original method after it has been stubbed.
60
68
 
61
- MuchStub.stub_send(myobj, :myval, 123)
69
+ MuchStub.stub_send(my_object, :my_value, 123)
62
70
  # => 123
63
- MuchStub.stub_send(myobj, :myval, 456)
71
+ MuchStub.stub_send(my_object, :my_value, 456)
64
72
  # => 456
65
73
 
66
74
  # Unstub individual stubs
67
75
 
68
- MuchStub.unstub(myobj, :mymeth)
69
- MuchStub.unstub(myobj, :myval)
76
+ MuchStub.unstub(my_object, :my_method)
77
+ MuchStub.unstub(my_object, :my_value)
70
78
 
71
79
  # OR blanket unstub all stubs
72
80
 
73
81
  MuchStub.unstub!
74
82
 
75
- # Original API is preserved after unstubbing
83
+ # The original API/behavior is preserved after unstubbing
76
84
 
77
- myobj.mymeth
78
- # => 'meth'
79
- myobj.myval(123)
85
+ my_object.my_method
86
+ # => "my-method"
87
+ my_object.my_value(123)
80
88
  # => 123
81
- myobj.myval(456)
89
+ my_object.my_value(456)
82
90
  # => 456
83
91
  ```
84
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
+
85
335
  ## Installation
86
336
 
87
337
  Add this line to your application's Gemfile:
88
338
 
89
- gem 'much-stub'
339
+ gem "much-stub"
90
340
 
91
341
  And then execute:
92
342
 
@@ -100,6 +350,6 @@ Or install it yourself as:
100
350
 
101
351
  1. Fork it
102
352
  2. Create your feature branch (`git checkout -b my-new-feature`)
103
- 3. Commit your changes (`git commit -am 'Added some feature'`)
353
+ 3. Commit your changes (`git commit -am "Added some feature"`)
104
354
  4. Push to the branch (`git push origin my-new-feature`)
105
355
  5. Create new Pull Request
@@ -1,7 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "much-stub/version"
2
4
 
3
- module MuchStub
5
+ require "much-stub/call"
6
+ require "much-stub/call_spy"
4
7
 
8
+ module MuchStub
5
9
  def self.stubs
6
10
  @stubs ||= {}
7
11
  end
@@ -10,8 +14,10 @@ module MuchStub
10
14
  MuchStub::Stub.key(obj, meth)
11
15
  end
12
16
 
13
- def self.call(*args, &block)
14
- self.stub(*args, &block)
17
+ def self.arity_matches?(method, args)
18
+ return true if method.arity == args.size # mandatory args
19
+ return true if method.arity < 0 && args.size >= (method.arity+1).abs # variable args
20
+ return false
15
21
  end
16
22
 
17
23
  def self.stub(obj, meth, &block)
@@ -20,6 +26,18 @@ module MuchStub
20
26
  self.stubs[key].tap{ |s| s.do = block }
21
27
  end
22
28
 
29
+ def self.call(*args, &block)
30
+ self.stub(*args, &block)
31
+ end
32
+
33
+ def self.stub_on_call(*args, &on_call_block)
34
+ self.stub(*args).on_call(&on_call_block)
35
+ end
36
+
37
+ def self.on_call(*args, &on_call_block)
38
+ self.stub_on_call(*args, &on_call_block)
39
+ end
40
+
23
41
  def self.unstub(obj, meth)
24
42
  key = self.stub_key(obj, meth)
25
43
  (self.stubs.delete(key) || MuchStub::NullStub.new).teardown
@@ -37,8 +55,31 @@ module MuchStub
37
55
  stub.call_method(args, &block)
38
56
  end
39
57
 
40
- class Stub
58
+ def self.tap(obj, meth, &tap_block)
59
+ self.stub(obj, meth) { |*args, &block|
60
+ self.stub_send(obj, meth, *args, &block).tap { |value|
61
+ tap_block.call(value, *args, &block) if tap_block
62
+ }
63
+ }
64
+ end
65
+
66
+ def self.tap_on_call(obj, meth, &on_call_block)
67
+ self.tap(obj, meth) { |value, *args, &block|
68
+ on_call_block.call(value, MuchStub::Call.new(*args, &block)) if on_call_block
69
+ }
70
+ end
41
71
 
72
+ def self.spy(obj, *meths, **return_values)
73
+ MuchStub::CallSpy.new(**return_values).call_spy_tap do |spy|
74
+ meths.each do |meth|
75
+ self.stub(obj, meth) { |*args, &block|
76
+ spy.__send__(meth, *args, &block)
77
+ }
78
+ end
79
+ end
80
+ end
81
+
82
+ class Stub
42
83
  def self.key(object, method_name)
43
84
  "--#{object.object_id}--#{method_name}--"
44
85
  end
@@ -69,11 +110,13 @@ module MuchStub
69
110
 
70
111
  def call(args, orig_caller = nil, &block)
71
112
  orig_caller ||= caller_locations
72
- unless arity_matches?(args)
73
- msg = "arity mismatch on `#{@method_name}`: " \
74
- "expected #{number_of_args(@method.arity)}, " \
75
- "called with #{args.size}"
76
- raise StubArityError, msg, orig_caller.map(&:to_s)
113
+ unless MuchStub.arity_matches?(@method, args)
114
+ raise(
115
+ StubArityError.new(
116
+ @method,
117
+ args,
118
+ method_name: @method_name,
119
+ backtrace: orig_caller))
77
120
  end
78
121
  lookup(args, orig_caller).call(*args, &block)
79
122
  rescue NotStubbedError
@@ -83,13 +126,29 @@ module MuchStub
83
126
 
84
127
  def with(*args, &block)
85
128
  orig_caller = caller_locations
86
- unless arity_matches?(args)
87
- msg = "arity mismatch on `#{@method_name}`: " \
88
- "expected #{number_of_args(@method.arity)}, " \
89
- "stubbed with #{args.size}"
90
- raise StubArityError, msg, orig_caller.map(&:to_s)
129
+ unless MuchStub.arity_matches?(@method, args)
130
+ raise(
131
+ StubArityError.new(
132
+ @method,
133
+ args,
134
+ method_name: @method_name,
135
+ backtrace: orig_caller))
91
136
  end
92
137
  @lookup[args] = block
138
+ self
139
+ end
140
+
141
+ def on_call(&on_call_block)
142
+ stub_block =
143
+ ->(*args, &block) {
144
+ on_call_block.call(MuchStub::Call.new(*args, &block)) if on_call_block
145
+ }
146
+ if @lookup.empty?
147
+ @do = stub_block
148
+ elsif @lookup.has_value?(nil)
149
+ @lookup.transform_values!{ |value| value.nil? ? stub_block : value }
150
+ end
151
+ self
93
152
  end
94
153
 
95
154
  def teardown
@@ -100,7 +159,7 @@ module MuchStub
100
159
  end
101
160
 
102
161
  def inspect
103
- "#<#{self.class}:#{'0x0%x' % (object_id << 1)}" \
162
+ "#<#{self.class}:#{"0x0%x" % (object_id << 1)}" \
104
163
  " @method_name=#{@method_name.inspect}" \
105
164
  ">"
106
165
  end
@@ -137,7 +196,7 @@ module MuchStub
137
196
  end
138
197
 
139
198
  def lookup(args, orig_caller)
140
- @lookup.fetch(args) do
199
+ @lookup.fetch(args) {
141
200
  self.do || begin
142
201
  msg = "#{inspect_call(args)} not stubbed."
143
202
  inspect_lookup_stubs.tap do |stubs|
@@ -145,13 +204,11 @@ module MuchStub
145
204
  end
146
205
  raise NotStubbedError, msg, orig_caller.map(&:to_s)
147
206
  end
148
- end
149
- end
150
-
151
- def arity_matches?(args)
152
- return true if @method.arity == args.size # mandatory args
153
- return true if @method.arity < 0 && args.size >= (@method.arity+1).abs # variable args
154
- return false
207
+ } ||
208
+ raise(
209
+ StubError,
210
+ "#{inspect_call(args)} stubbed with no block.",
211
+ orig_caller.map(&:to_s))
155
212
  end
156
213
 
157
214
  def inspect_lookup_stubs
@@ -159,37 +216,47 @@ module MuchStub
159
216
  end
160
217
 
161
218
  def inspect_call(args)
162
- "`#{@method_name}(#{args.map(&:inspect).join(',')})`"
219
+ "`#{@method_name}(#{args.map(&:inspect).join(",")})`"
163
220
  end
164
-
165
- def number_of_args(arity)
166
- if arity < 0
167
- "at least #{(arity + 1).abs}"
168
- else
169
- arity
170
- end
171
- end
172
-
173
221
  end
174
222
 
175
223
  StubError = Class.new(ArgumentError)
176
224
  NotStubbedError = Class.new(StubError)
177
- StubArityError = Class.new(StubError)
225
+ StubArityError =
226
+ Class.new(StubError) do
227
+ def initialize(method, args, method_name:, backtrace:)
228
+ msg = "arity mismatch on `#{method_name}`: " \
229
+ "expected #{number_of_args(method.arity)}, " \
230
+ "called with #{args.size}"
231
+
232
+ super(msg)
233
+ set_backtrace(Array(backtrace).map(&:to_s))
234
+ end
235
+
236
+ private
237
+
238
+ def number_of_args(arity)
239
+ if arity < 0
240
+ "at least #{(arity + 1).abs}"
241
+ else
242
+ arity
243
+ end
244
+ end
245
+ end
178
246
 
179
247
  NullStub = Class.new do
180
248
  def teardown; end # no-op
181
249
  end
182
250
 
183
251
  module ParameterList
184
-
185
- LETTERS = ('a'..'z').to_a.freeze
252
+ LETTERS = ("a".."z").to_a.freeze
186
253
 
187
254
  def self.new(object, method_name)
188
255
  arity = get_arity(object, method_name)
189
256
  params = build_params_from_arity(arity)
190
- params << '*args' if arity < 0
191
- params << '&block'
192
- params.join(', ')
257
+ params << "*args" if arity < 0
258
+ params << "&block"
259
+ params.join(", ")
193
260
  end
194
261
 
195
262
  private
@@ -210,9 +277,7 @@ module MuchStub
210
277
  number_of_letters, letter_index = param_index.divmod(LETTERS.size)
211
278
  LETTERS[letter_index] * number_of_letters
212
279
  end
213
-
214
280
  end
215
-
216
281
  end
217
282
 
218
283
  # Kernel#caller_locations polyfill for pre ruby 2.0.0