You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
<a href="https://mike.cousins.io/decoy/">Usage guide and documentation</a>
18
18
</p>
19
19
</div>
20
20
21
-
The Decoy library allows you to create, stub, and verify test double objects for your Python unit tests, so your tests are:
21
+
The Decoy library allows you to create, stub, and verify fully-typed, async/await friendly mocks in your Python unit tests, so your tests are:
22
22
23
23
- Less prone to insufficient tests due to unconditional stubbing
24
-
- Covered by typechecking
25
24
- Easier to fit into the Arrange-Act-Assert pattern
25
+
- Covered by typechecking
26
26
27
27
The Decoy API is heavily inspired by / stolen from the excellent [testdouble.js][] and [Mockito][] projects.
28
28
@@ -41,7 +41,7 @@ poetry add --dev decoy
41
41
42
42
## Setup
43
43
44
-
### Pytest
44
+
### Pytest setup
45
45
46
46
Decoy ships with its own [pytest][] plugin, so once Decoy is installed, you're ready to start using it via its pytest fixture, called `decoy`.
47
47
@@ -59,9 +59,7 @@ The `decoy` fixture is function-scoped and will ensure that all stub and spy sta
59
59
60
60
### Mypy Setup
61
61
62
-
Decoy's rehearsal syntax can be a bit confusing to [mypy][] if a function is supposed to return `None`. Normally, [mypy will complain][] if you try to use a `None`-returning expression as a value, because this is almost always a mistake.
63
-
64
-
In Decoy, however, it's an intentional part of the API and _not_ a mistake. To suppress these errors, Decoy provides a mypy plugin that you should add to your configuration file:
62
+
Decoy's API can be a bit confusing to [mypy][]. To suppress mypy errors that may be emitted during valid usage of the Decoy API, we have a mypy plugin that you should add to your configuration file:
65
63
66
64
```ini
67
65
# mypi.ini
@@ -72,166 +70,3 @@ plugins = decoy.mypy
72
70
```
73
71
74
72
[mypy]: https://mypy.readthedocs.io/
75
-
[mypy will complain]: https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-called-function-returns-a-value-func-returns-value
76
-
77
-
## Usage
78
-
79
-
### Stubbing
80
-
81
-
A stub is an object used in a test that is pre-configured to return a result or raise an error if called according to a specification. In Decoy, you specify a stub's call conditions with a "rehearsal", which is simply a call to the stub inside of a `decoy.when` wrapper.
82
-
83
-
By pre-configuring the stub with specific rehearsals, you get the following benefits:
84
-
85
-
- Your test double will only return your mock value **if it is called correctly**
86
-
- You avoid separate "set up mock return value" and "assert mock called correctly" steps
87
-
- If you annotate your test double with an actual type, the rehearsal will fail typechecking if called incorrectly
If you're coming from `unittest.mock`, you're probably used to calling your code under test and _then_ verifying that your dependency was called correctly. Decoy provides similar call verification using the same "rehearsal" mechanism that the stubbing API uses.
118
-
119
-
```python
120
-
import pytest
121
-
from typing import cast, Optional
122
-
from decoy import Decoy, verify
123
-
124
-
from .logger import Logger
125
-
126
-
deflog_warning(msg: str, logger: Logger) -> None:
127
-
logger.warn(msg)
128
-
129
-
deftest_log_warning(decoy: Decoy):
130
-
logger = decoy.create_decoy(spec=Logger)
131
-
132
-
# call code under test
133
-
some_result = log_warning("oh no!", logger)
134
-
135
-
# verify double called correctly with a rehearsal
136
-
decoy.verify(logger.warn("oh no!"))
137
-
```
138
-
139
-
Asserting that calls happened after the fact can be useful, but **should only be used if the dependency is being called solely for its side-effect(s)**. Verification of interactions in this manner should be considered a last resort, because:
140
-
141
-
- If you're calling a dependency to get data, then you can more precisely describe that relationship using [stubbing](#stubbing)
142
-
- Side-effects are harder to understand and maintain than pure functions, so in general you should try to side-effect sparingly
143
-
144
-
Stubbing and verification of a decoy are **mutually exclusive** within a test. If you find yourself wanting to both stub and verify the same decoy, then one or more of these is true:
145
-
146
-
- The assertions are redundant
147
-
- The dependency is doing too much based on its input (e.g. side-effecting _and_ calculating complex data) and should be refactored
148
-
149
-
#### Verifying order of multiple calls
150
-
151
-
If your code under test must call several dependencies in order, you may pass multiple rehearsals to `verify`. Decoy will search through the list of all calls made to the given spies and look for the exact rehearsal sequence given, in order.
152
-
153
-
```python
154
-
decoy.verify(
155
-
handler.call_first_procedure("hello"),
156
-
handler.call_second_procedure("world"),
157
-
)
158
-
```
159
-
160
-
### Usage with async/await
161
-
162
-
Decoy supports async/await out of the box! Pass your async function or class with async methods to `spec` in `decoy.create_decoy_func` or `decoy.create_decoy`, respectively, and Decoy will figure out the rest.
163
-
164
-
When writing rehearsals on async functions and methods, remember to include the `await` with your rehearsal call:
Sometimes, when you're stubbing or verifying calls (or really when you're doing any sort of equality assertion in a test), you need to loosen a given assertion. For example, you may want to assert that a dependency is called with a string, but you don't care about the full contents of that string.
173
-
174
-
Decoy includes a set of matchers, which are simply Python classes with `__eq__` methods defined, that you can use in rehearsals and/or assertions.
175
-
176
-
```python
177
-
import pytest
178
-
from typing import cast, Optional
179
-
from decoy import Decoy, matchers
180
-
181
-
from .logger import Logger
182
-
183
-
deflog_warning(msg: str, logger: Logger) -> None:
184
-
logger.warn(msg)
185
-
186
-
deftest_log_warning(decoy: Decoy):
187
-
logger = decoy.create_decoy(spec=Logger)
188
-
189
-
# call code under test
190
-
some_result = log_warning(
191
-
"Oh no, something went wrong with request ID abc123efg456",
192
-
logger=logger
193
-
)
194
-
195
-
# verify double called correctly
196
-
decoy.verify(
197
-
logger.warn(matchers.StringMatching("request ID abc123efg456"))
198
-
)
199
-
```
200
-
201
-
#### Capturing values with `matchers.captor`
202
-
203
-
When testing certain APIs, especially callback APIs, it can be helpful to capture the values of arguments passed to a given decoy.
204
-
205
-
For example, our test subject may register an anonymous event listener handler with a dependency, and we want to test our subject's behavior when the event listener is triggered.
This is a pretty verbose way of writing a test, so in general, you may want to approach using `matchers.captor` as a form of potential code smell / test pain. There are often better ways to structure your code for these sorts of interactions that don't involve anonymous / private functions.
236
-
237
-
For further reading on when (or rather, when not) to use argument captors, check out [testdouble's documentation on its argument captor matcher](https://github.com/testdouble/testdouble.js/blob/main/docs/6-verifying-invocations.md#tdmatcherscaptor).
The [`create_decoy` method][decoy.Decoy.create_decoy] is used to create mock class instances. Decoy will inspect type annotations and method signatures to automatically configure methods as synchronous or asynchronous. Decoy mocks are automatically deep.
11
+
12
+
To typecheckers, the mock will appear to have the exact same type as the `spec` argument. The mock will also pass `isinstance` checks.
The [`create_decoy_func` method][decoy.Decoy.create_decoy_func] is used to create a mock function instance. Decoy can inspect a `spec` function signature to automatically configure the function as syncronous or asynchronous. Otherwise, the `is_async` argument can force the mock to be asynchronous.
22
+
23
+
To typecheckers, the mock will appear to have the exact same type as the `spec` argument, if used.
Sometimes, when you're stubbing or verifying calls (or really when you're doing any sort of equality assertion in a test), you need to loosen a given assertion. For example, you may want to assert that a dependency is called with a string, but you don't care about the full contents of that string.
4
+
5
+
Decoy includes a [`matchers` module][decoy.matchers], which has a set of Python classes with `__eq__` methods defined that you can use in rehearsals and/or assertions in place of actual values
6
+
7
+
## Basic usage
8
+
9
+
To use, import `decoy.matchers` and use a matcher wherever you would normally use a value.
10
+
11
+
```python
12
+
import pytest
13
+
from typing import cast, Optional
14
+
from decoy import Decoy, matchers
15
+
16
+
from .logger import Logger
17
+
from .my_thing import MyThing
18
+
19
+
deftest_log_warning(decoy: Decoy):
20
+
logger = decoy.create_decoy(spec=Logger)
21
+
22
+
subject = MyThing(logger=logger)
23
+
24
+
# call code under test
25
+
subject.log_warning("Oh no, something went wrong with request abc123efg456")
When testing certain APIs, especially callback APIs, it can be helpful to capture the values of arguments passed to a given dependency. For this, Decoy provides [`matchers.Captor`][decoy.matchers.captor].
36
+
37
+
For example, our test subject may register an event listener handler, and we want to test our subject's behavior when the event listener is triggered.
This is a pretty verbose way of writing a test, so in general, you may want to approach using `matchers.captor` as a form of potential code smell / test pain. There are often better ways to structure your code for these sorts of interactions that don't involve private functions.
68
+
69
+
For further reading on when (or rather, when not) to use argument captors, check out [testdouble's documentation on its argument captor matcher](https://github.com/testdouble/testdouble.js/blob/main/docs/6-verifying-invocations.md#tdmatcherscaptor).
0 commit comments