mocktail 0.0.1

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