Skip to content

Commit fb83fd8

Browse files
authored
refactor: rearchitect decoy via dogfooding (#33)
1 parent f8807f4 commit fb83fd8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1991
-1174
lines changed

.flake8

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ extend-ignore =
1313
E203,
1414
# do not require type annotations for self nor cls
1515
ANN101,
16-
ANN102
16+
ANN102,
17+
# do not require docstrings for __init__ methods
18+
D107,
1719

1820
# configure flake8-docstrings
1921
# https://pypi.org/project/flake8-docstrings/

CONTRIBUTING.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Contributing Guide
2+
3+
## Development Setup
4+
5+
This project uses [poetry][] to manage dependencies and builds, and you will need to install it before working on Decoy.
6+
7+
Once poetry is installed, you should be good to set up a virtual environment and install development dependencies. While Decoy is supported on Python >= 3.7, Python >= 3.8 is recommended for development.
8+
9+
```bash
10+
git clone https://github.com/mcous/decoy.git
11+
cd decoy
12+
poetry install
13+
```
14+
15+
## Development Tasks
16+
17+
### Tests
18+
19+
Decoy's tests are run using [pytest][].
20+
21+
```bash
22+
poetry run pytest
23+
```
24+
25+
You can also run tests in watch mode using [pytest-xdist][].
26+
27+
```bash
28+
poetry run pytest --looponfail
29+
```
30+
31+
In an exciting twist, since version 1.6.0, Decoy's tests rely on Decoy itself to test (and more importantly, design) the relationships between Decoy's internal APIs. This means:
32+
33+
- Decoy's unit test suite serves as an end-to-end test of Decoy by virtue of existing (wow, very meta, actually kind of cool).
34+
- Changes that break a small part of Decoy may result in a large number of test failures, because if Decoy breaks it can't be used to test itself.
35+
36+
If you find yourself in a situation where Decoy's test suite has blown up, **concentrate on getting the test suites that don't use Decoy to pass**. From there, lean on the type-checker to guide you to any components that aren't properly hooked up. Decoy also has a end-to-end smoke test suite (`tests/test_decoy.py`) that can be helpful in getting things back to green.
37+
38+
### Checks
39+
40+
Decoy's source code is typechecked with [mypy][] and linted with [flake8][].
41+
42+
```bash
43+
poetry run mypy
44+
poetry run flake8
45+
```
46+
47+
### Formatting
48+
49+
Decoy's source code is formatted using [black][].
50+
51+
```bash
52+
poetry run black .
53+
```
54+
55+
### Documentation
56+
57+
Decoy's documentation is built with [mkdocs][], which you can use to preview the documentation site locally.
58+
59+
```bash
60+
poetry run mkdocs serve
61+
```
62+
63+
## Deploying
64+
65+
The library and documentation will be deployed to PyPI and GitHub Pages, respectively, by CI. To trigger the deploy, cut a new version and push it to GitHub.
66+
67+
Deploy adheres to [semantic versioning][], so care should be taken to bump accurately.
68+
69+
```bash
70+
# checkout the main branch and pull down latest changes
71+
git checkout main
72+
git pull
73+
74+
# bump the version
75+
# replace ${bump_version} with a bump specifier, like "minor"
76+
poetry version ${bump_version}
77+
78+
# add the bumped pyproject.toml
79+
git add pyproject.toml
80+
81+
# commit and tag the bump
82+
# replace ${release_version} with the actual version string
83+
git commit -m "chore(release): ${release_version}"
84+
git tag -a v${release_version} -m "chore(release): ${release_version}"
85+
git push --follow-tags
86+
```
87+
88+
[poetry]: https://python-poetry.org/
89+
[pytest]: https://docs.pytest.org/
90+
[pytest-xdist]: https://github.com/pytest-dev/pytest-xdist
91+
[mypy]: https://mypy.readthedocs.io
92+
[flake8]: https://flake8.pycqa.org
93+
[black]: https://black.readthedocs.io
94+
[mkdocs]: https://www.mkdocs.org/
95+
[semantic versioning]: https://semver.org/

README.md

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div align="center">
22
<h1>Decoy</h1>
33
<img src="https://mike.cousins.io/decoy/img/decoy.png" width="256px">
4-
<p>Opinionated, typed stubbing and verification library for Python</p>
4+
<p>Opinionated mocking library for Python</p>
55
<p>
66
<a href="https://github.com/mcous/decoy/actions">
77
<img title="CI Status" src="https://flat.badgen.net/github/checks/mcous/decoy/main">
@@ -18,17 +18,14 @@
1818
</p>
1919
</div>
2020

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:
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:
2222

2323
- Less prone to insufficient tests due to unconditional stubbing
2424
- Easier to fit into the Arrange-Act-Assert pattern
2525
- Covered by typechecking
2626

2727
The Decoy API is heavily inspired by / stolen from the excellent [testdouble.js][] and [Mockito][] projects.
2828

29-
[testdouble.js]: https://github.com/testdouble/testdouble.js
30-
[mockito]: https://site.mockito.org/
31-
3229
## Install
3330

3431
```bash
@@ -55,18 +52,92 @@ def test_my_thing_works(decoy: Decoy) -> None:
5552

5653
The `decoy` fixture is function-scoped and will ensure that all stub and spy state is reset between every test.
5754

58-
[pytest]: https://docs.pytest.org/
59-
6055
### Mypy Setup
6156

6257
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:
6358

6459
```ini
65-
# mypi.ini
60+
# mypy.ini
6661

6762
# ...
6863
plugins = decoy.mypy
6964
# ...
7065
```
7166

67+
## Basic Usage
68+
69+
This example assumes you are using [pytest][]. See Decoy's [documentation][] for a more detailed usage guide and API reference.
70+
71+
### Define your test
72+
73+
Decoy will add a `decoy` fixture that provides its mock creation API.
74+
75+
```python
76+
from decoy import Decoy
77+
from todo import TodoAPI, TodoItem
78+
from todo.store TodoStore
79+
80+
def test_add_todo(decoy: Decoy) -> None:
81+
...
82+
```
83+
84+
### Create a mock
85+
86+
Use `decoy.create_decoy` to create a mock based on some specification. From there, inject the mock into your test subject.
87+
88+
```python
89+
def test_add_todo(decoy: Decoy) -> None:
90+
todo_store = decoy.create_decoy(spec=TodoStore)
91+
subject = TodoAPI(store=todo_store)
92+
...
93+
```
94+
95+
See [creating mocks][] for more details.
96+
97+
### Stub a behavior
98+
99+
Use `decoy.when` to configure your mock's behaviors. For example, you can set the mock to return a certain value when called in a certain way using `then_return`:
100+
101+
```python
102+
def test_add_todo(decoy: Decoy) -> None:
103+
"""Adding a todo should create a TodoItem in the TodoStore."""
104+
todo_store = decoy.create_decoy(spec=TodoStore)
105+
subject = TodoAPI(store=todo_store)
106+
107+
decoy.when(
108+
todo_store.add(name="Write a test for adding a todo")
109+
).then_return(
110+
TodoItem(id="abc123", name="Write a test for adding a todo")
111+
)
112+
113+
result = subject.add("Write a test for adding a todo")
114+
assert result == TodoItem(id="abc123", name="Write a test for adding a todo")
115+
```
116+
117+
See [stubbing with when][] for more details.
118+
119+
### Verify a call
120+
121+
Use `decoy.verify` to assert that a mock was called in a certain way. This is best used with dependencies that are being used for their side-effects and don't return a useful value.
122+
123+
```python
124+
def test_remove_todo(decoy: Decoy) -> None:
125+
"""Removing a todo should remove the item from the TodoStore."""
126+
todo_store = decoy.create_decoy(spec=TodoStore)
127+
subject = TodoAPI(store=todo_store)
128+
129+
subject.remove("abc123")
130+
131+
decoy.verify(todo_store.remove(id="abc123"))
132+
```
133+
134+
See [spying with verify][] for more details.
135+
136+
[testdouble.js]: https://github.com/testdouble/testdouble.js
137+
[mockito]: https://site.mockito.org/
138+
[pytest]: https://docs.pytest.org/
72139
[mypy]: https://mypy.readthedocs.io/
140+
[documentation]: https://mike.cousins.io/decoy/
141+
[creating mocks]: https://mike.cousins.io/decoy/usage/create/
142+
[stubbing with when]: https://mike.cousins.io/decoy/usage/when/
143+
[spying with verify]: https://mike.cousins.io/decoy/usage/verify/

0 commit comments

Comments
 (0)