Skip to content

Commit 4ce74e0

Browse files
committed
docs(v3): add v3 preview docs
1 parent 2466753 commit 4ce74e0

File tree

15 files changed

+787
-39
lines changed

15 files changed

+787
-39
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2020-2023, Michael Cousins
3+
Copyright (c) 2020-2026, Michael Cousins
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

codebook.toml

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

decoy/errors.py

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@
1212

1313

1414
class MockNameRequiredError(ValueError):
15-
"""An error raised if a name is not provided for a mock.
16-
17-
See the [MockNameRequiredError guide][] for more details.
18-
19-
[MockNameRequiredError guide]: usage/errors-and-warnings.md#mocknamerequirederror
20-
"""
15+
"""A name was not provided for a mock."""
2116

2217
@classmethod
2318
def create(cls) -> "MockNameRequiredError":
@@ -26,20 +21,16 @@ def create(cls) -> "MockNameRequiredError":
2621

2722

2823
class MockSpecInvalidError(TypeError):
29-
"""An value passed as a mock spec is not valid."""
24+
"""A value passed as a mock spec is not valid."""
3025

3126

3227
class MissingRehearsalError(ValueError):
33-
"""An error raised when a Decoy method is called without rehearsal(s).
28+
"""A Decoy method was called without rehearsal(s).
3429
3530
This error is raised if you use [`when`][decoy.Decoy.when],
3631
[`verify`][decoy.Decoy.verify], or [`prop`][decoy.Decoy.prop] incorrectly
3732
in your tests. When using async/await, this error can be triggered if you
3833
forget to include `await` with your rehearsal.
39-
40-
See the [MissingRehearsalError guide][] for more details.
41-
42-
[MissingRehearsalError guide]: usage/errors-and-warnings.md#missingrehearsalerror
4334
"""
4435

4536
@classmethod
@@ -57,13 +48,10 @@ class ThenDoActionNotCallableError(TypeError):
5748

5849

5950
class MockNotAsyncError(TypeError):
60-
"""An error raised when an asynchronous function is used with a synchronous mock.
51+
"""An asynchronous function was passed to a synchronous mock.
6152
6253
This error is raised if you pass an `async def` function
63-
to a synchronous stub's `then_do` method.
64-
See the [MockNotAsyncError guide][] for more details.
65-
66-
[MockNotAsyncError guide]: usage/errors-and-warnings.md#mocknotasyncerror
54+
to a synchronous stub's [`then_do`][decoy.Stub.then_do] method.
6755
"""
6856

6957

@@ -72,17 +60,7 @@ class SignatureMismatchError(TypeError):
7260

7361

7462
class VerifyError(AssertionError):
75-
"""An error raised when actual calls do not match rehearsals given to `verify`.
76-
77-
See [spying with verify][] for more details.
78-
79-
[spying with verify]: usage/verify.md
80-
81-
Attributes:
82-
rehearsals: Rehearsals that were being verified.
83-
calls: Actual calls to the mock(s).
84-
times: The expected number of calls to the mock, if any.
85-
"""
63+
"""A [`Decoy.verify`][decoy.Decoy.verify] assertion failed."""
8664

8765
rehearsals: Sequence[VerifyRehearsal]
8866
calls: Sequence[SpyEvent]
@@ -119,9 +97,4 @@ def create(
11997

12098

12199
class VerifyOrderError(VerifyError):
122-
"""An error raised when the order of calls do not match expectations.
123-
124-
See [spying with verify][] for more details.
125-
126-
[spying with verify]: usage/verify.md
127-
"""
100+
"""A [`Decoy.verify_order`][decoy.next.Decoy.verify_order] assertion failed."""

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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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**. You don't need to configure anything explicitly if you need a `@property` getter to return another mock; Decoy will inspect type annotations and configure the proper child mock 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 attribute access
39+
40+
### Stub a getter
41+
42+
Use [`When.get`][decoy.next.When.get] stub a return value for an attribute instead of returning a child mock.
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, because set and delete expressions do not return a value in Python, 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+
## Verify property access
91+
92+
You can verify calls to property setters and deleters with [`Verify.set`][decoy.next.Verify.set] and [`Verify.delete`][decoy.next.Verify.delete].
93+
94+
!!! tip
95+
96+
Use this feature sparingly! If you're designing a dependency that triggers a side-effect, consider using a regular method, instead.
97+
98+
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.
99+
100+
### Verify a setter
101+
102+
Use [`Verify.set`][decoy.next.Verify.set] to check that an attribute was set.
103+
104+
```python
105+
dependency = decoy.mock(name="dependency")
106+
107+
dependency.some_property = 42
108+
109+
decoy.verify(dependency.some_property).set(42)
110+
```
111+
112+
### Verify a deleter
113+
114+
Use [`Verify.delete`][decoy.next.Verify.delete] to check that an attribute was deleted.
115+
116+
```python
117+
dependency = decoy.mock(name="dependency")
118+
119+
del dependency.some_property
120+
121+
decoy.verify(dependency.some_property).delete)
122+
```

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+
```

0 commit comments

Comments
 (0)