mocktail 0.0.1

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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +18 -0
  3. data/.gitignore +8 -0
  4. data/.standard.yml +1 -0
  5. data/CHANGELOG.md +3 -0
  6. data/Gemfile +10 -0
  7. data/Gemfile.lock +62 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.md +557 -0
  10. data/Rakefile +11 -0
  11. data/bin/console +35 -0
  12. data/bin/setup +8 -0
  13. data/lib/mocktail/dsl.rb +21 -0
  14. data/lib/mocktail/errors.rb +15 -0
  15. data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +16 -0
  16. data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +21 -0
  17. data/lib/mocktail/handles_dry_call/logs_call.rb +7 -0
  18. data/lib/mocktail/handles_dry_call/validates_arguments.rb +57 -0
  19. data/lib/mocktail/handles_dry_call.rb +19 -0
  20. data/lib/mocktail/handles_dry_new_call.rb +36 -0
  21. data/lib/mocktail/imitates_type/ensures_imitation_support.rb +11 -0
  22. data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +95 -0
  23. data/lib/mocktail/imitates_type/makes_double.rb +18 -0
  24. data/lib/mocktail/imitates_type.rb +19 -0
  25. data/lib/mocktail/initializes_mocktail.rb +17 -0
  26. data/lib/mocktail/matcher_presentation.rb +15 -0
  27. data/lib/mocktail/matchers/any.rb +18 -0
  28. data/lib/mocktail/matchers/base.rb +25 -0
  29. data/lib/mocktail/matchers/captor.rb +52 -0
  30. data/lib/mocktail/matchers/includes.rb +24 -0
  31. data/lib/mocktail/matchers/is_a.rb +11 -0
  32. data/lib/mocktail/matchers/matches.rb +13 -0
  33. data/lib/mocktail/matchers/not.rb +11 -0
  34. data/lib/mocktail/matchers/numeric.rb +18 -0
  35. data/lib/mocktail/matchers/that.rb +24 -0
  36. data/lib/mocktail/matchers.rb +14 -0
  37. data/lib/mocktail/records_demonstration.rb +32 -0
  38. data/lib/mocktail/registers_matcher.rb +52 -0
  39. data/lib/mocktail/registers_stubbing.rb +19 -0
  40. data/lib/mocktail/replaces_next.rb +36 -0
  41. data/lib/mocktail/replaces_type/redefines_new.rb +26 -0
  42. data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +39 -0
  43. data/lib/mocktail/replaces_type.rb +26 -0
  44. data/lib/mocktail/resets_state.rb +9 -0
  45. data/lib/mocktail/share/determines_matching_calls.rb +60 -0
  46. data/lib/mocktail/share/simulates_argument_error.rb +28 -0
  47. data/lib/mocktail/value/cabinet.rb +41 -0
  48. data/lib/mocktail/value/call.rb +15 -0
  49. data/lib/mocktail/value/demo_config.rb +10 -0
  50. data/lib/mocktail/value/double.rb +11 -0
  51. data/lib/mocktail/value/matcher_registry.rb +19 -0
  52. data/lib/mocktail/value/stubbing.rb +24 -0
  53. data/lib/mocktail/value/top_shelf.rb +61 -0
  54. data/lib/mocktail/value/type_replacement.rb +11 -0
  55. data/lib/mocktail/value.rb +8 -0
  56. data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +15 -0
  57. data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +10 -0
  58. data/lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb +47 -0
  59. data/lib/mocktail/verifies_call/raises_verification_error.rb +63 -0
  60. data/lib/mocktail/verifies_call.rb +29 -0
  61. data/lib/mocktail/version.rb +3 -0
  62. data/lib/mocktail.rb +63 -0
  63. data/mocktail.gemspec +31 -0
  64. metadata +107 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8792bd9f9b3ccc36c01046557e63c390eccd97c53e25b54da6c54ca2690f839d
4
+ data.tar.gz: 13c9444150fd0bd4098660f5960d268d37d67c8a27058834259dd2f1717933b6
5
+ SHA512:
6
+ metadata.gz: 43a3c90e6edcbc9f04f2d4eb26e930a43dd8ac685f50d98d95f2c6128edd873776d02123258aa5e12766a1940e77e1704a252a18e5541bd52e8be3e5c6605d91
7
+ data.tar.gz: 6c872542db03bc16e548c5d1f48b18229638f2553612c4e9897443e9f43d01d6056428eca76da9814a4c82823d658ce434ec88316d579b6aab7741d8dcbeff1c
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.1
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.15
17
+ bundle install
18
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.standard.yml ADDED
@@ -0,0 +1 @@
1
+ ruby_version: 2.7
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.1
2
+
3
+ Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in mocktail.gemspec
4
+ gemspec
5
+
6
+ gem "rake"
7
+ gem "minitest"
8
+ gem "standard"
9
+ gem "pry"
10
+ gem "simplecov"
data/Gemfile.lock ADDED
@@ -0,0 +1,62 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mocktail (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ coderay (1.1.3)
11
+ docile (1.4.0)
12
+ method_source (1.0.0)
13
+ minitest (5.14.4)
14
+ parallel (1.21.0)
15
+ parser (3.0.2.0)
16
+ ast (~> 2.4.1)
17
+ pry (0.14.1)
18
+ coderay (~> 1.1)
19
+ method_source (~> 1.0)
20
+ rainbow (3.0.0)
21
+ rake (13.0.6)
22
+ regexp_parser (2.1.1)
23
+ rexml (3.2.5)
24
+ rubocop (1.20.0)
25
+ parallel (~> 1.10)
26
+ parser (>= 3.0.0.0)
27
+ rainbow (>= 2.2.2, < 4.0)
28
+ regexp_parser (>= 1.8, < 3.0)
29
+ rexml
30
+ rubocop-ast (>= 1.9.1, < 2.0)
31
+ ruby-progressbar (~> 1.7)
32
+ unicode-display_width (>= 1.4.0, < 3.0)
33
+ rubocop-ast (1.11.0)
34
+ parser (>= 3.0.1.1)
35
+ rubocop-performance (1.11.5)
36
+ rubocop (>= 1.7.0, < 2.0)
37
+ rubocop-ast (>= 0.4.0)
38
+ ruby-progressbar (1.11.0)
39
+ simplecov (0.21.2)
40
+ docile (~> 1.1)
41
+ simplecov-html (~> 0.11)
42
+ simplecov_json_formatter (~> 0.1)
43
+ simplecov-html (0.12.3)
44
+ simplecov_json_formatter (0.1.3)
45
+ standard (1.3.0)
46
+ rubocop (= 1.20.0)
47
+ rubocop-performance (= 1.11.5)
48
+ unicode-display_width (2.1.0)
49
+
50
+ PLATFORMS
51
+ arm64-darwin-20
52
+
53
+ DEPENDENCIES
54
+ minitest
55
+ mocktail!
56
+ pry
57
+ rake
58
+ simplecov
59
+ standard
60
+
61
+ BUNDLED WITH
62
+ 2.2.15
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 Test Double, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,557 @@
1
+ <img
2
+ src="https://user-images.githubusercontent.com/79303/134366631-9c6cfe67-a9c0-4096-bbea-ba1698a85b0b.png"
3
+ width="90%"/>
4
+
5
+ # Mocktail 🍸
6
+
7
+ Mocktail is a [test
8
+ double](https://github.com/testdouble/contributing-tests/wiki/Test-Double)
9
+ library for Ruby. It offers a simple API and robust feature-set.
10
+
11
+ ## First, an aperitif
12
+
13
+ Before getting into the details, let's demonstrate what Mocktail's API looks
14
+ like. Suppose you have a class `Negroni`:
15
+
16
+ ```ruby
17
+ class Negroni
18
+ def self.ingredients
19
+ [:gin, :campari, :sweet_vermouth]
20
+ end
21
+
22
+ def shake!(shaker)
23
+ shaker.mix(self.class.ingredients)
24
+ end
25
+
26
+ def sip(amount)
27
+ raise "unimplemented"
28
+ end
29
+ end
30
+ ```
31
+
32
+ 1. Create a mocked instance: `negroni = Mocktail.of(Negroni)`
33
+ 2. Stub a response with `stubs { negroni.sip(4) }.with { :ahh }`
34
+ * Calling `negroni.sip(4)` will subsequently return `:ahh`
35
+ * Another example: `stubs { |m| negroni.sip(m.numeric) }.with { :nice }`
36
+ 3. Verify a call with `verify { negroni.shake!(:some_shaker) }`
37
+ * `verify` will raise an error unless `negroni.shake!(:some_shaker)` has
38
+ been called
39
+ * Another example: `verify { |m| negroni.shake!(m.that { |arg|
40
+ arg.respond_to?(:mix) }) }`
41
+ 4. Deliver a mock to your code under test with `negroni =
42
+ Mocktail.of_next(Negroni)`
43
+ * `of_next` will return a fake `Negroni`
44
+ * The next call to `Negroni.new` will return _exactly the same_ fake
45
+ instance, allowing the code being tested to seamlessly instantiate and
46
+ interact with it
47
+ * This means no dependency injection is necessary, nor is a sweeping
48
+ override like
49
+ [any_instance](https://relishapp.com/rspec/rspec-mocks/docs/working-with-legacy-code/any-instance)
50
+ * `Negroni.new` will be unaffected on other threads and will continue
51
+ behaving like normal as soon as the next `new` call
52
+
53
+ Mocktail can do a whole lot more than this, and was also designed with
54
+ descriptive error messages and common edge cases in mind:
55
+
56
+ * Entire classes and modules can be replaced with `Mocktail.replace(type)` while
57
+ preserving thread safety
58
+ * Arity of arguments and keyword arguments is enforced on faked methods to
59
+ prevent isolated unit tests from continuing to pass after an API contract
60
+ changes
61
+ * For mocked methods that take a block, `stubs` & `verify` can inspect and
62
+ invoke the passed block to determine whether the call satisfies their
63
+ conditions
64
+ * Dynamic stubbings that return a value based on how the mocked method was
65
+ called
66
+ * Advanced stubbing and verification options like specifying the number of
67
+ `times` a stub can be satisfied or a call should be verified, allowing tests
68
+ to forego specifying arguments and blocks, and temporarily disabling arity
69
+ validation
70
+ * Built-in matchers as well as custom matcher support
71
+ * Argument captors for complex, multi-step call verifications
72
+
73
+ ## Getting started
74
+
75
+ ### Install
76
+
77
+ The main ingredient to add to your Gemfile:
78
+
79
+ ```ruby
80
+ gem "mocktail", group: :test
81
+ ```
82
+
83
+ ### Add the DSL
84
+
85
+ Then, in each of your tests or in a test helper, you'll probably want to include
86
+ Mocktail's DSL. (This is optional, however, as every method in the DSL is also
87
+ available as a singleton method on `Mocktail`.)
88
+
89
+ In Minitest, you might add the DSL with:
90
+
91
+ ```ruby
92
+ class Minitest::Test
93
+ include Mocktail::DSL
94
+ end
95
+ ```
96
+
97
+ Or, in RSpec:
98
+
99
+ ```ruby
100
+ RSpec.configure do |config|
101
+ config.include Mocktail::DSL
102
+ end
103
+ ```
104
+
105
+ ### Clean up after each test
106
+
107
+ When making so many concoctions, it's important to keep a clean bar! To reset
108
+ Mocktail's internal state between tests and avoid test pollution, you should
109
+ also call `Mocktail.reset` after each test:
110
+
111
+ In Minitest:
112
+
113
+ ```ruby
114
+ class Minitest::Test
115
+ # Or, if in a Rails test, in a `teardown do…end` block
116
+ def teardown
117
+ Mocktail.reset
118
+ end
119
+ end
120
+ ```
121
+
122
+ And RSpec:
123
+
124
+ ```ruby
125
+ RSpec.configure do |config|
126
+ config.after(:each) do
127
+ Mocktail.reset
128
+ end
129
+ end
130
+ ```
131
+
132
+ ## API
133
+
134
+ The public API is a pretty quick read of the [top-level module's
135
+ source](lib/mocktail.rb). Here's a longer menu to explain what goes into each
136
+ feature.
137
+
138
+ ### Mocktail.of
139
+
140
+ `Mocktail.of(module_or_class)` takes a module or class and returns an instance
141
+ of an object with fake methods in place of all its instance methods which can
142
+ then be stubbed or verified.
143
+
144
+ ```ruby
145
+ class Clothes; end;
146
+ class Shoe < Clothes
147
+ def tie(laces)
148
+ end
149
+ end
150
+
151
+ shoe = Mocktail.of(Shoe)
152
+ shoe.instance_of?(Shoe) # => true
153
+ shoe.is_a?(Clothes) # => true
154
+ shoe.class == Shoe # => false!
155
+ shoe.to_s # => #<Mocktail of Shoe:0x00000001343b57b0>"
156
+ ```
157
+
158
+ ### Mocktail.of_next
159
+
160
+ `Mocktail.of_next(klass, [count: 1])` takes a class and returns one mock (the
161
+ default) or an array of multiple mocks. It also effectively overrides the
162
+ behavior of that class's constructor to return those mock(s) in order and
163
+ finally restoring its previous behavior.
164
+
165
+ For example, if you wanted to test the `Notifier` class below:
166
+
167
+ ```ruby
168
+ class Notifier
169
+ def initialize
170
+ @mailer = Mailer.new
171
+ end
172
+
173
+ def notify(name)
174
+ @mailer.deliver!("Hello, #{name}")
175
+ end
176
+ end
177
+ ```
178
+
179
+ You could write a test like this:
180
+
181
+ ```ruby
182
+ def test_notifier
183
+ mailer = Mocktail.of_next(Mailer)
184
+ subject = Notifier.new
185
+
186
+ subject.notify("Pants")
187
+
188
+ verify { mailer.deliver!("Hello, Pants") }
189
+ end
190
+ ```
191
+
192
+ There's nothing wrong with creating mocks using `Mocktail.of` and passing them
193
+ to your subject some other way, but this approach allows you to write very terse
194
+ isolation tests without foisting additional indirection or dependency injection
195
+ in for your tests' sake.
196
+
197
+ ### Mocktail.stubs
198
+
199
+ Configuring a fake method to take a certain action or return a particular value
200
+ is called "stubbing". To stub a call with a value, you can call `Mocktail.stubs`
201
+ (or just `stubs` if you've included `Mocktail::DSL`) and then specify an effect
202
+ that will be invoked whenever that call configuration is satisfied using `with`.
203
+
204
+ The API is very simple in the simple case:
205
+
206
+ ```ruby
207
+ class UserRepository
208
+ def find(id, debug: false); end
209
+
210
+ def transaction(&blk); end
211
+ end
212
+ ```
213
+
214
+ You could stub responses to a mock of the `UserRepository` like this:
215
+
216
+ ```ruby
217
+ user_repository = Mocktail.of(UserRepository)
218
+
219
+ stubs { user_repository.find(42) }.with { :a_user }
220
+ user_repository.find(42) # => :a_user
221
+ user_repository.find(43) # => nil
222
+ user_repository.find # => ArgumentError: wrong number of arguments (given 0, expected 1)
223
+ ```
224
+
225
+ The block passed to `stubs` is called the "demonstration", because it represents
226
+ an example of the kind of calls that Mocktail should match.
227
+
228
+ If you want to get fancy, you can use matchers to make your demonstration more
229
+ dynamic. For example, you could match any number with:
230
+
231
+ ```ruby
232
+ stubs { |m| user_repository.find(m.numeric) }.with { :another_user }
233
+ user_repository.find(41) # => :another_user
234
+ user_repository.find(42) # => :another_user
235
+ user_repository.find(43) # => :another_user
236
+ ```
237
+
238
+ Stubbings are last-in-wins, which is why the stubbing above would have
239
+ overridden the earlier-but-more-specific stubbing of `find(42)`.
240
+
241
+ A stubbing's effect can also be changed dynamically based on the actual call
242
+ that satisfied the demonstration by looking at the `call` block argument:
243
+
244
+ ```ruby
245
+ stubs { |m| user_repository.find(m.is_a(Integer)) }.with { |call|
246
+ {id: call.args.first}
247
+ }
248
+ user_repository.find(41) # => {id: 41}
249
+ # Since 42.5 is a Float, the earlier stubbing will win here:
250
+ user_repository.find(42.5) # => :another_user
251
+ user_repository.find(43) # => {id: 43}
252
+ ```
253
+
254
+ It's certainly more complex to think through, but if your stubbed method takes a
255
+ block, your demonstration can pass a block of its own and inspect or invoke it:
256
+
257
+ ```ruby
258
+ stubs {
259
+ user_repository.transaction { |block| block.call == {id: 41} }
260
+ }.with { :successful_transaction }
261
+
262
+ user_repository.transaction {
263
+ user_repository.find(41)
264
+ } # => :successful_transaction
265
+ user_repository.transaction {
266
+ user_repository.find(40)
267
+ } # => nil
268
+ ```
269
+
270
+ There are also several advanced options you can pass to `stubs` to control its
271
+ behavior.
272
+
273
+ `times` will limit the number of times a satisfied stubbing can have its effect:
274
+
275
+ ```ruby
276
+ stubs { |m| user_repository.find(m.any) }.with { :not_found }
277
+ stubs(times: 2) { |m| user_repository.find(1) }.with { :someone }
278
+
279
+ user_repository.find(1) # => :someone
280
+ user_repository.find(1) # => :someone
281
+ user_repository.find(1) # => :not_found
282
+ ```
283
+
284
+ `ignore_extra_args` will allow a demonstration to be considered satisfied even
285
+ if it fails to specify arguments and keyword arguments made by the actual call:
286
+
287
+ ```
288
+ stubs { user_repository.find(4) }.with { :a_person }
289
+ user_repository.find(4, debug: true) # => nil
290
+
291
+ stubs(ignore_extra_args: true) { user_repository.find(4) }.with { :b_person }
292
+ user_repository.find(4, debug: true) # => :b_person
293
+ ```
294
+
295
+ And `ignore_block` will similarly allow a demonstration to not concern itself
296
+ with whether an actual call passed the method a block—it's satisfied either way:
297
+
298
+ ```ruby
299
+ stubs { user_repository.transaction }.with { :transaction }
300
+ user_repository.transaction {} # => nil
301
+
302
+ stubs(ignore_block: true) { user_repository.transaction }.with { :transaction }
303
+ user_repository.transaction {} # => :transaction
304
+ ```
305
+
306
+ ### Mocktail.verify
307
+
308
+ In practice, we've found that we stub far more responses than we explicitly
309
+ verify a particular call took place. That's because our code normally returns
310
+ some observable value that is _influenced_ by our dependencies' behavior, so
311
+ adding additional assertions that they be called would be redundant. That
312
+ said, for cases where a dependency doesn't return a value but just has a
313
+ necessary side effect, the `verify` method exists (and like `stubs` is included
314
+ in `Mocktail::DSL`).
315
+
316
+ Once you've gotten the hang of stubbing, you'll find that the `verify` method is
317
+ intentionally very similar. They almost rhyme.
318
+
319
+ For this example, consider an `Auditor` class that our code might need to call
320
+ to record that certain actions took place.
321
+
322
+ ```ruby
323
+ class Auditor
324
+ def record!(message, user_id:, action: nil); end
325
+ end
326
+ ```
327
+
328
+ Once you've created a mock of the `Auditor`, you can start verifying basic
329
+ calls:
330
+
331
+ ```ruby
332
+ auditor = Mocktail.of(Auditor)
333
+
334
+ verify { auditor.record!("hello", user_id: 42) }
335
+ # => raised Mocktail::VerificationError
336
+ # Expected mocktail of Auditor#record! to be called like:
337
+ #
338
+ # record!("hello", user_id: 42)
339
+ #
340
+ # But it was never called.
341
+ ```
342
+
343
+ Wups! Verify will blow up whenever a matching call hasn't occurred, so it
344
+ should be called after you've invoked your subject under test along with any
345
+ other assertions you have.
346
+
347
+ If we make a call that satisfies the `verify` call's demonstration, however, you
348
+ won't see that error:
349
+
350
+ ```ruby
351
+ auditor.record!("hello", user_id: 42)
352
+
353
+ verify { auditor.record!("hello", user_id: 42) } # => nil
354
+ ```
355
+
356
+ There, nothing happened! Just like any other assertion library, you only hear
357
+ from `verify` when verification fails.
358
+
359
+ Just like with `stubs`, you can any built-in or custom matchers can serve as
360
+ garnishes for your demonstration:
361
+
362
+ ```ruby
363
+ auditor.record!("hello", user_id: 42)
364
+
365
+ verify { |m| auditor.record!(m.is_a(String), user_id: m.numeric) } # => nil
366
+ # But this will raise a VerificationError:
367
+ verify { |m| auditor.record!(m.is_a(String), user_id: m.that { |arg| arg > 50}) }
368
+ ```
369
+
370
+ When you pass a block to your demonstration, it will be invoked with any block
371
+ that was passed to the actual call to the mock. Truthy responses will satisfy
372
+ the verification and falsey ones will fail:
373
+
374
+ ```ruby
375
+ auditor.record!("ok", user_id: 1) { Time.new }
376
+
377
+ verify { |m| auditor.record!("ok", user_id: 1) { |block| block.call.is_a?(Time) } } # => nil
378
+ # But this will raise a VerificationError:
379
+ verify { |m| auditor.record!("ok", user_id: 1) { |block| block.call.is_a?(Date) } }
380
+ ```
381
+
382
+ `verify` supports the same options as `stubs`:
383
+
384
+ * `times` will require the demonstrated call happened exactly `times` times (by
385
+ default, the call has to happen 1 or more times)
386
+ * `ignore_extra_args` will allow the demonstration to forego specifying optional
387
+ arguments while still being considered satisfied
388
+ * `ignore_block` will similarly allow the demonstration to forego specifying a
389
+ block, even if the actual call receives one
390
+
391
+ ### Mocktail.matchers
392
+
393
+ You'll probably never need to call `Mocktail.matchers` directly, because it's
394
+ the object that is passed to every demonstration block passed to `stubs` and
395
+ `verify`. By default, a stubbing (e.g. `stubs { email.send("text") }`) is only
396
+ considered satisfied if every argument passed to an actual call was passed an
397
+ `==` check. Matchers allow us to relax or change that constraint for both
398
+ regular arguments and keyword arguments so that our demonstrations can match
399
+ more kinds of method invocations.
400
+
401
+ Matchers allow you to specify stubbings and verifications that look like this:
402
+
403
+ ```ruby
404
+ stubs { |m| email.send(m.is_a(String)) }.with { "I'm an email" }
405
+ ```
406
+
407
+ #### Built-in matchers
408
+
409
+ These matchers come out of the box:
410
+
411
+ * `any` - Will match any value (even nil) in the given argument position or
412
+ keyword
413
+ * `is_a(type)` - Will match when its `type` passes an `is_a?` check against the
414
+ actual argument
415
+ * `includes(thing, [**more_things])` - Will match when all of its arguments are
416
+ contained by the corresponding argument—be it a string, array, hash, or
417
+ anything that responds to `includes?`
418
+ * `matches(pattern)` - Will match when the provided string or pattern passes
419
+ a `match?` test on the corresponding argument; usually used to match strings
420
+ that contain a particular substring or pattern, but will work with any
421
+ argument that responds to `match?`
422
+ * `not(thing)` - Will only match when its argument _does not_ equal (via `!=`)
423
+ the actual argument
424
+ * `numeric` - Will match when the actual argument is an instance of `Integer`,
425
+ `Float`, or (if loaded) `BigDecimal`
426
+ * `that { |arg| … }` - Takes a block that will receive the actual argument. If
427
+ the block returns truthy, it's considered a match; otherwise, it's not a
428
+ match.
429
+
430
+ #### Custom matchers
431
+
432
+ If you want to write your own matchers, check out [the source for
433
+ examples](lib/mocktail/matchers/includes.rb). Once you've implemented a class,
434
+ just pass it to `Mocktail.register_matcher` in your test helper.
435
+
436
+ ```ruby
437
+ class MyAwesomeMatcher < Mocktail::Matchers::Base
438
+ def self.matcher_name
439
+ :awesome
440
+ end
441
+
442
+ def match?(actual)
443
+ "#{@expected}✨" == actual
444
+ end
445
+ end
446
+
447
+ Mocktail.register_matcher(MyAwesomeMatcher)
448
+ ```
449
+
450
+ Then, a stubbing like this:
451
+
452
+ ```ruby
453
+ stubs { |m| user_repository.find(m.awesome(11)) }.with { :awesome_user }
454
+
455
+ user_repository.find("11")) # => nil
456
+ user_repository.find("11✨")) # => :awesome_user
457
+ ```
458
+
459
+ ### Mocktail.captor
460
+
461
+ An argument captor is a special kind of matcher… really, it's a matcher factory.
462
+ Suppose you have a `verify` call for which one of the expected arguments is
463
+ _really_ complicated. Since `verify` tends to be paired with fire-and-forget
464
+ APIs that are being invoked for the side effect, this is a pretty common case.
465
+ You want to be able to effectively snag that value and then run any number of
466
+ specific assertions against it.
467
+
468
+ That's what `Mocktail.captor` is for. It's easiest to make sense of this by
469
+ example. Given this `BigApi` class that's presumably being called by your
470
+ subject at the end of a lot of other work building up a payload:
471
+
472
+ ```ruby
473
+ class BigApi
474
+ def send(payload); end
475
+ end
476
+ ```
477
+
478
+ You could capture the value of that payload as part of the verification of the
479
+ call:
480
+
481
+ ```ruby
482
+ big_api = Mocktail.of(BigApi)
483
+
484
+ big_api.send({imagine: "that", this: "is", a: "huge", object: "!"})
485
+
486
+ payload_captor = Mocktail.captor
487
+ verify { big_api.send(payload_captor.capture) } # => nil!
488
+ ```
489
+
490
+ The `verify` above will pass because _a_ call did happen, but we haven't
491
+ asserted anything beyond that yet. What really happened is that
492
+ `payload_captor.capture` actually returned a matcher that will return true for
493
+ any argument _while also sneakily storing a copy of the argument value_.
494
+
495
+ That's why we instantiated `payload_captor` with `Mocktail.captor` outside the
496
+ demonstration block, so we can inspect its `value` after the `verify` call:
497
+
498
+ ```ruby
499
+ payload_captor = Mocktail.captor
500
+ verify { big_api.send(payload_captor.capture) } # => nil!
501
+
502
+ payload = payload_captor.value # {:imagine=>"that", :this=>"is", :a=>"huge", :object=>"!"}
503
+ assert_equal "huge", payload[:a]
504
+ ```
505
+
506
+ ### Mocktail.replace
507
+
508
+ Mocktail was written to support isolated test-driven development, which usually
509
+ results in a lot of boring classes and instance methods. But sometimes you need
510
+ to mock singleton methods on classes or modules, and we support that too.
511
+
512
+ When you call `Mocktail.replace(type)`, all of the singleton methods on the
513
+ provided type are replaced with fake methods available for stubbing and
514
+ verification. It's really that simple.
515
+
516
+ [**Obligatory warning:** Mocktail does its best to ensure that other threads
517
+ won't be affected when you replace the singleton methods on a type, but your
518
+ mileage may very! Singleton methods are global and code that introspects or
519
+ invokes a replaced method in a peculiar-enough way could lead to hard-to-track
520
+ down bugs. (If this concerns you, then the fact that class methods are
521
+ effectively global state may be a great reason not to rely too heavily on
522
+ them!)]
523
+
524
+ ### Mocktail.reset
525
+
526
+ This one's simple: you probably want to call `Mocktail.reset` after each test,
527
+ but you _definitely_ want to call it if you're using `Mocktail.replace` or
528
+ `Mocktail.of_next` anywhere, since those will affect state that is shared across
529
+ tests.
530
+
531
+ ## Acknowledgements
532
+
533
+ Mocktail is created & maintained by the software agency [Test
534
+ Double](https://twitter.com). If you've ever come across our eponymously-named
535
+ [testdouble.js](https://github.com/testdouble/testdouble.js/), you might find
536
+ Mocktail's API to be quite similar. The term "test double" was originally coined
537
+ by Gerard Meszaros in his book [xUnit Test
538
+ Patterns](http://xunitpatterns.com/Test%20Double.html).
539
+
540
+ The name is inspired by the innovative Java mocking library
541
+ [Mockito](https://site.mockito.org). Mocktail also the spiritual successor to
542
+ [gimme](https://github.com/searls/gimme), which offers a similar API but which
543
+ fell victim to the limitations of Ruby 1.8.7 (and
544
+ [@searls](https://twitter.com/searls)'s Ruby chops). Gimme was also one of the
545
+ final projects we collaborated with [Jim Weirich](https://github.com/jimweirich)
546
+ on, so this approach to isolated unit testing holds a special significance to
547
+ us.
548
+
549
+ ## Code of Conduct
550
+
551
+ This project follows Test Double's [code of
552
+ conduct](https://testdouble.com/code-of-conduct) for all community interactions,
553
+ including (but not limited to) one-on-one communications, public posts/comments,
554
+ code reviews, pull requests, and GitHub issues. If violations occur, Test Double
555
+ will take any action they deem appropriate for the infraction, up to and
556
+ including blocking a user from the organization's repositories.
557
+