much-stub 0.1.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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