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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +8 -0
- data/.standard.yml +1 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +20 -0
- data/README.md +557 -0
- data/Rakefile +11 -0
- data/bin/console +35 -0
- data/bin/setup +8 -0
- data/lib/mocktail/dsl.rb +21 -0
- data/lib/mocktail/errors.rb +15 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing/finds_satisfaction.rb +16 -0
- data/lib/mocktail/handles_dry_call/fulfills_stubbing.rb +21 -0
- data/lib/mocktail/handles_dry_call/logs_call.rb +7 -0
- data/lib/mocktail/handles_dry_call/validates_arguments.rb +57 -0
- data/lib/mocktail/handles_dry_call.rb +19 -0
- data/lib/mocktail/handles_dry_new_call.rb +36 -0
- data/lib/mocktail/imitates_type/ensures_imitation_support.rb +11 -0
- data/lib/mocktail/imitates_type/makes_double/declares_dry_class.rb +95 -0
- data/lib/mocktail/imitates_type/makes_double.rb +18 -0
- data/lib/mocktail/imitates_type.rb +19 -0
- data/lib/mocktail/initializes_mocktail.rb +17 -0
- data/lib/mocktail/matcher_presentation.rb +15 -0
- data/lib/mocktail/matchers/any.rb +18 -0
- data/lib/mocktail/matchers/base.rb +25 -0
- data/lib/mocktail/matchers/captor.rb +52 -0
- data/lib/mocktail/matchers/includes.rb +24 -0
- data/lib/mocktail/matchers/is_a.rb +11 -0
- data/lib/mocktail/matchers/matches.rb +13 -0
- data/lib/mocktail/matchers/not.rb +11 -0
- data/lib/mocktail/matchers/numeric.rb +18 -0
- data/lib/mocktail/matchers/that.rb +24 -0
- data/lib/mocktail/matchers.rb +14 -0
- data/lib/mocktail/records_demonstration.rb +32 -0
- data/lib/mocktail/registers_matcher.rb +52 -0
- data/lib/mocktail/registers_stubbing.rb +19 -0
- data/lib/mocktail/replaces_next.rb +36 -0
- data/lib/mocktail/replaces_type/redefines_new.rb +26 -0
- data/lib/mocktail/replaces_type/redefines_singleton_methods.rb +39 -0
- data/lib/mocktail/replaces_type.rb +26 -0
- data/lib/mocktail/resets_state.rb +9 -0
- data/lib/mocktail/share/determines_matching_calls.rb +60 -0
- data/lib/mocktail/share/simulates_argument_error.rb +28 -0
- data/lib/mocktail/value/cabinet.rb +41 -0
- data/lib/mocktail/value/call.rb +15 -0
- data/lib/mocktail/value/demo_config.rb +10 -0
- data/lib/mocktail/value/double.rb +11 -0
- data/lib/mocktail/value/matcher_registry.rb +19 -0
- data/lib/mocktail/value/stubbing.rb +24 -0
- data/lib/mocktail/value/top_shelf.rb +61 -0
- data/lib/mocktail/value/type_replacement.rb +11 -0
- data/lib/mocktail/value.rb +8 -0
- data/lib/mocktail/verifies_call/finds_verifiable_calls.rb +15 -0
- data/lib/mocktail/verifies_call/raises_verification_error/gathers_calls_of_method.rb +10 -0
- data/lib/mocktail/verifies_call/raises_verification_error/stringifies_call.rb +47 -0
- data/lib/mocktail/verifies_call/raises_verification_error.rb +63 -0
- data/lib/mocktail/verifies_call.rb +29 -0
- data/lib/mocktail/version.rb +3 -0
- data/lib/mocktail.rb +63 -0
- data/mocktail.gemspec +31 -0
- 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
data/.standard.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby_version: 2.7
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
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
|
+
|