Skip to content

Commit b3f1309

Browse files
committed
docs(v3): add v3 preview docs
1 parent ac7756f commit b3f1309

File tree

13 files changed

+776
-4
lines changed

13 files changed

+776
-4
lines changed

codebook.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
words = [
2+
"matchers",
3+
"mundo",
4+
"stubbings",
5+
"verden",
6+
]

docs/usage/matchers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ When testing certain APIs, especially callback APIs, it can be helpful to captur
5151

5252
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.
5353

54-
```py
54+
```python
5555
import pytest
5656
from typing import cast, Optional
5757
from decoy import Decoy, matchers

docs/usage/verify.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ say_hello = decoy.mock(name="say_hello")
6969

7070
say_hello("foobar")
7171

72-
decoy.verify(matchers.StringMatching("^foo"), times=1) # passes
73-
decoy.verify(matchers.StringMatching("^bar"), times=1) # raises
72+
decoy.verify(say_hello(matchers.StringMatching("^foo")), times=1) # passes
73+
decoy.verify(say_hello(matchers.StringMatching("^bar")), times=1) # raises
7474
```
7575

7676
## Verifying with async/await

docs/v3/about.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Decoy v3 Preview
2+
3+
The next major version of Decoy is a ground-up rebuild of the library. In the years since Decoy was first written, the Python typing system has advanced, especially when it comes to typing functions. These advancements, especially the addition of [`ParamSpec`][paramspec], unblocked a much simpler API (as well as internal implementation) for Decoy.
4+
5+
In order to ease the migration to the new API, the v3 API has been added as a preview to the v2 release (starting with `v2.4.0`) as `decoy.next`.
6+
7+
```diff
8+
- from decoy import Decoy
9+
+ from decoy.next import Decoy
10+
```
11+
12+
!!! warning
13+
14+
`decoy.next` is a **preview** and not subject to semver.
15+
No major changes are anticipated, but cannot be guaranteed until
16+
Decoy v3 is released.
17+
18+
## Setup
19+
20+
In order to use the v3 preview, you must be using:
21+
22+
- `decoy >= 2.4.0`
23+
- `python >= 3.10`
24+
25+
Then, start trying out the new API!
26+
27+
```diff
28+
- from decoy import Decoy
29+
+ from decoy.next import Decoy
30+
31+
32+
+ @pytest.fixture()
33+
+ def decoy() -> collections.abc.Iterator[Decoy]:
34+
+ """Create a Decoy v3 preview instance for testing."""
35+
+ with Decoy.create() as decoy:
36+
+ yield decoy
37+
38+
39+
def test_when(decoy: Decoy) -> None:
40+
mock = decoy.mock(cls=SomeClass)
41+
- decoy.when(mock.foo("hello")).then_return("world")
42+
+ decoy.when(mock.foo).called_with("hello").then_return("world")
43+
44+
45+
def test_verify(decoy: Decoy) -> None:
46+
mock = decoy.mock(cls=SomeClass)
47+
mock.foo("hello")
48+
- decoy.verify(mock.foo("hello"))
49+
+ decoy.verify(mock.foo).called_with("hello")
50+
```
51+
52+
See the [migration guide](./migration.md) for more details.
53+
54+
[paramspec]: https://docs.python.org/3/library/typing.html#typing.ParamSpec

docs/v3/api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# API Reference
2+
3+
!!! warning
4+
5+
`decoy.next` is a **preview** and not subject to semver.
6+
No major changes are anticipated, but cannot be guaranteed until
7+
Decoy v3 is released.
8+
9+
::: decoy.next

docs/v3/attributes.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Mock attributes
2+
3+
Python [property attributes][] provide an interface for creating read-only properties and properties with getters, setters, and deleters. You can use Decoy to stub these properties.
4+
5+
[property attributes]: https://docs.python.org/3/library/functions.html#property
6+
7+
## Default behavior
8+
9+
Unlike mock method calls - which have a default return value of `None` - Decoy's default return value for attribute access is **another mock**. So you don't need to configure anything explicitly if you need a `@property` getter to return another mock; Decoy will do this for you.
10+
11+
```python
12+
class Child:
13+
...
14+
15+
class Parent:
16+
@property
17+
def child(self) -> Child:
18+
...
19+
20+
mock_parent = decoy.mock(cls=Parent)
21+
22+
assert isinstance(mock_parent, Dependency)
23+
assert isinstance(mock_parent.child, Child)
24+
```
25+
26+
You may also manually set any mock attribute to "opt out" of Decoy for a given attribute. To opt back into Decoy, delete the attribute.
27+
28+
```python
29+
dep = decoy.mock(cls=Parent)
30+
31+
dep.child = "don't worry about it"
32+
assert dep.child == "don't worry about it"
33+
34+
del dep.child
35+
assert isinstance(mock_parent.child, Child)
36+
```
37+
38+
## Stub property access
39+
40+
### Stub a getter
41+
42+
If you would like to stub a return value for a property that is different than the default behavior, simply use [`When.get`][decoy.next.When.get].
43+
44+
```python
45+
dependency = decoy.mock(name="dependency")
46+
47+
decoy.when(dependency.some_property).get().then_return(42)
48+
49+
assert dep.some_property == 42
50+
```
51+
52+
You can also configure any other behavior, like raising an error.
53+
54+
```python
55+
dependency = decoy.mock(name="dependency")
56+
57+
decoy
58+
.when(dependency.some_property)
59+
.get()
60+
.then_raise(RuntimeError("oh no"))
61+
62+
with pytest.raises(RuntimeError, match="oh no"):
63+
dependency.some_property
64+
```
65+
66+
### Stub a setter or deleter
67+
68+
While you cannot stub a return value for a getter or setter, you can stub a `raise` or a side effect with [`When.set`][decoy.next.When.set] and [`When.delete`][decoy.next.When.delete].
69+
70+
```python
71+
dependency = decoy.mock(name="dependency")
72+
73+
decoy
74+
.when(dependency.some_property)
75+
.set(42)
76+
.then_raise(RuntimeError("oh no"))
77+
78+
decoy
79+
.when(dependency.some_property)
80+
.delete()
81+
.then_raise(RuntimeError("what a disaster"))
82+
83+
with pytest.raises(RuntimeError, match="oh no"):
84+
dependency.some_property = 42
85+
86+
with pytest.raises(RuntimeError, match="what a disaster"):
87+
del dependency.some_property
88+
```
89+
90+
!!! tip
91+
92+
You cannot use [`then_return`][decoy.Stub.then_return] with attribute setters and deleters, because set and delete expressions do not return a value in Python.
93+
94+
## Verify property access
95+
96+
You can verify calls to property setters and deleters with [`Verify.set`][decoy.next.Verify.set] and [`Verify.delete`][decoy.next.Verify.delete].
97+
98+
!!! tip
99+
100+
Use this feature sparingly! If you're designing a dependency that triggers a side-effect, consider using a regular method, instead.
101+
102+
Mocking and verifying property setters and deleters is most useful for testing code that needs to interact with older or legacy dependencies that would be prohibitively expensive to redesign.
103+
104+
### Verify a setter
105+
106+
Use [`Verify.set`][decoy.next.Verify.set] to check that an attribute was set.
107+
108+
```python
109+
dependency = decoy.mock(name="dependency")
110+
111+
dependency.some_property = 42
112+
113+
decoy.verify(dependency.some_property).set(42)
114+
```
115+
116+
### Verify a deleter
117+
118+
Use [`Verify.delete`][decoy.next.Verify.delete] to check that an attribute was deleted.
119+
120+
```python
121+
dependency = decoy.mock(name="dependency")
122+
123+
del dependency.some_property
124+
125+
decoy.verify(dependency.some_property).delete)
126+
```

docs/v3/context-managers.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Mock context managers
2+
3+
In Python, `with` statement [context managers][] provide an interface to execute code inside a given "runtime context." This context can define consistent, failsafe setup and teardown behavior. For example, Python's built-in file objects provide a context manager interface to ensure the underlying file resource is opened and closed cleanly, without the caller having to explicitly deal with it:
4+
5+
```python
6+
with open("hello-world.txt", "r") as f:
7+
contents = f.read()
8+
```
9+
10+
You can use Decoy to mock out your dependencies that provide a context manager interface.
11+
12+
[context managers]: https://docs.python.org/3/reference/datamodel.html#context-managers
13+
14+
## Generator-based context managers
15+
16+
Using the [contextlib][] module, you can [decorate a generator function][] or method to turn its yielded value into a context manager. To mock a generator function context manager, use [`Stub.then_enter_with`][decoy.next.Stub.then_enter_with].
17+
18+
```python
19+
@contextlib.contextmanager
20+
def open_config(path: str) -> collections.abc.Iterator[bool]:
21+
...
22+
23+
24+
def test_generator_context_manager(decoy: Decoy) -> None:
25+
mock_open_config = decoy.mock(func=open_config)
26+
27+
decoy
28+
.when(mock_open_config)
29+
.called_with("some_flag")
30+
.then_enter_with(True)
31+
32+
with mock_open_config("some_flag") as result:
33+
assert result is True
34+
```
35+
36+
[contextlib]: https://docs.python.org/3/library/contextlib.html
37+
[decorate a generator function]: https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager
38+
39+
## Class-based context managers
40+
41+
You can stub out a context manager's `__enter__` and `__exit__` method like any other method.
42+
43+
```python
44+
def test_context_manager(decoy: Decoy) -> None:
45+
subject = decoy.mock(name="cm")
46+
47+
decoy.when(subject.__enter__).called_with().then_return("hello world")
48+
49+
with subject as result:
50+
assert result == "hello world"
51+
52+
decoy.verify(subject.__exit__).called_with(None, None, None)
53+
```
54+
55+
This also works with asynchronous `__aenter__` and `__aexit__`
56+
57+
## Context manager state
58+
59+
You can also configure stubs and verifications to only match calls made while a context manager mock is entered using the `is_entered` option to `called_with`.
60+
61+
| `is_entered` | Matching behavior |
62+
| ------------ | ----------------------------------------------------------- |
63+
| `True` | Only matches calls made between `__enter__` and `__exit__` |
64+
| `False` | Only matches calls made if context manager is _not_ entered |
65+
| `None` | Match the call regardless of context manager entry state |
66+
67+
```python
68+
decoy
69+
.when(subject.read, is_entered=True)
70+
.called_with("some_flag")
71+
.then_return(True)
72+
73+
decoy
74+
.verify(subject.write, is_entered=True)
75+
.called_with("some_flag", "new_value")
76+
```

docs/v3/create.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Create a mock
2+
3+
Decoy mocks are flexible objects that can be used in place of a class instance or a callable object, like a function. Mocks are created using the [`Decoy.mock`][decoy.next.Decoy.mock] method.
4+
5+
## Default behaviors
6+
7+
Decoy mock objects are flexible, callable proxy objects that simply record interactions made with the object. Accessing any property of the mock will return a child mock, and calling the mock itself or any "method" of the mock will return `None`.
8+
9+
```python
10+
my_mock = decoy.mock(name="my_mock")
11+
12+
assert my_mock() is None
13+
assert my_mock.some_method("hello world") is None
14+
assert my_mock.some_property.some_method("hey") is None
15+
```
16+
17+
You can configure a mock's behaviors using [decoy.when](./when.md). You can make assertions about how a mock was called using [decoy.verify](./verify.md).
18+
19+
## Mock a class
20+
21+
To mock a class instance, pass the `cls` argument to `decoy.mock`. Decoy will inspect type annotations and method signatures to set a name for use in assertion messages, configure methods as synchronous or asynchronous, and understand function keyword arguments.
22+
23+
```python
24+
some_dependency = decoy.mock(cls=SomeDependency)
25+
```
26+
27+
To type checkers, the mock will appear to have the exact same type as the `cls` argument. The mock will also pass `isinstance` checks.
28+
29+
## Mock a function
30+
31+
To mock a function, pass the `func` argument to `decoy.mock`. Decoy will inspect `func` to set a name for use in assertion messages, configure the mock as synchronous or asynchronous, and understand function keyword arguments.
32+
33+
```python
34+
mock_function = decoy.mock(func=some_function)
35+
```
36+
37+
To type checkers, the mock will appear to have the exact same type as the `func` argument. The function mock will pass `inspect.signature` checks.
38+
39+
## Creating a mock without a spec
40+
41+
You can call `decoy.mock` without using `cls` or `func`. A spec-less mock is useful for dependency interfaces like callback functions.
42+
43+
When creating a mock without a spec, you must use the `name` argument to give the mock a name to use in assertion messages. You must use the `is_async` argument if the created mock will be used as an asynchronous callable.
44+
45+
```python
46+
callback = decoy.mock(name="callback")
47+
async_callback = decoy.mock(name="async_callback", is_async=True)
48+
```
49+
50+
## Mock an async function or method
51+
52+
If you pass Decoy a `cls` or `func` with asynchronous methods or functions, Decoy will automatically create asynchronous mocks. When creating mocks without `cls` or `func`, you can use the `is_async` option to create an asynchronous mock manually:
53+
54+
```python
55+
async_mock = decoy.mock(name="async_mock", is_async=True)
56+
```

0 commit comments

Comments
 (0)