Decoy is a mocking library designed for effective and productive test-driven development in Python. If you want to use tests to guide the structure of your code, Decoy might be for you!
Decoy mocks are async/await and type-checking friendly. Decoy is heavily inspired by (and/or stolen from) the excellent testdouble.js and Mockito projects. The Decoy API is powerful, easy to read, and strives to help you make good decisions about your code.
# pip
pip install decoy
# poetry
poetry add --dev decoy
# pipenv
pipenv install --dev decoy
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
.
# test_my_thing.py
from decoy import Decoy
def test_my_thing_works(decoy: Decoy) -> None:
...
By default, Decoy is compatible with Python typing and type-checkers like mypy. However, stubbing functions that return None
can trigger a type checking error during correct usage of the Decoy API. To suppress these errors, add Decoy's plugin to your mypy configuration.
# mypy.ini
plugins = decoy.mypy
Decoy works well with pytest, but if you use another testing library or framework, you can still use Decoy! You just need to do two things:
- Create a new instance of
Decoy()
before each test - Call
decoy.reset()
after each test
For example, using the built-in unittest framework, you would use the setUp
fixture method to do self.decoy = Decoy()
and the tearDown
method to call self.decoy.reset()
. For a working example, see tests/test_unittest.py
.
This basic example assumes you are using pytest. For more detailed documentation, see Decoy's usage guide and API reference.
Decoy will add a decoy
fixture to pytest that provides its mock creation API.
from decoy import Decoy
def test_something(decoy: Decoy) -> None:
...
!!! note
Importing the `Decoy` interface for type annotations is recommended, but optional. If your project does not use type annotations, you can simply write:
```python
def test_something(decoy):
...
```
Use decoy.mock
to create a mock based on some specification. From there, inject the mock into your test subject.
def test_add_todo(decoy: Decoy) -> None:
todo_store = decoy.mock(cls=TodoStore)
subject = TodoAPI(store=todo_store)
...
See creating mocks for more details.
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
:
def test_add_todo(decoy: Decoy) -> None:
"""Adding a todo should create a TodoItem in the TodoStore."""
todo_store = decoy.mock(cls=TodoStore)
subject = TodoAPI(store=todo_store)
decoy.when(
todo_store.add(name="Write a test for adding a todo")
).then_return(
TodoItem(id="abc123", name="Write a test for adding a todo")
)
result = subject.add("Write a test for adding a todo")
assert result == TodoItem(id="abc123", name="Write a test for adding a todo")
See stubbing with when for more details.
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.
def test_remove_todo(decoy: Decoy) -> None:
"""Removing a todo should remove the item from the TodoStore."""
todo_store = decoy.mock(cls=TodoStore)
subject = TodoAPI(store=todo_store)
subject.remove("abc123")
decoy.verify(todo_store.remove(id="abc123"), times=1)
See spying with verify for more details.