much-stub 0.1.0 → 0.1.5

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
- ---
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