Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/mcous/decoy

🦆 Opinionated mocking library for Python
https://github.com/mcous/decoy

magicmock mock mockito spy stub tdd test testdouble

Last synced: 3 months ago
JSON representation

🦆 Opinionated mocking library for Python

Awesome Lists containing this project

README

        


Decoy logo

Decoy


Opinionated mocking library for Python











Usage guide and documentation


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.

## Install

```bash
# pip
pip install decoy

# poetry
poetry add --dev decoy

# pipenv
pipenv install --dev decoy
```

## Setup

### Pytest setup

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

```python
# test_my_thing.py
from decoy import Decoy

def test_my_thing_works(decoy: Decoy) -> None:
...
```

### Mypy setup

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](https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-called-function-returns-a-value-func-returns-value) during correct usage of the Decoy API. To suppress these errors, add Decoy's plugin to your mypy configuration.

```ini
# mypy.ini
plugins = decoy.mypy
```

### Other testing libraries

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:

1. Create a new instance of [`Decoy()`](https://michael.cousins.io/decoy/api/#decoy.Decoy) before each test
2. Call [`decoy.reset()`](https://michael.cousins.io/decoy/api/#decoy.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`](https://github.com/mcous/decoy/blob/main/tests/test_unittest.py).

## Basic Usage

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.

```python
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):
...
```

### Create a mock

Use `decoy.mock` to create a mock based on some specification. From there, inject the mock into your test subject.

```python
def test_add_todo(decoy: Decoy) -> None:
todo_store = decoy.mock(cls=TodoStore)
subject = TodoAPI(store=todo_store)
...
```

See [creating mocks][] for more details.

### Stub a behavior

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

```python
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.

### Verify a call

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.

```python
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.

[testdouble.js]: https://github.com/testdouble/testdouble.js
[mockito]: https://site.mockito.org/
[pytest]: https://docs.pytest.org/
[unittest]: https://docs.python.org/3/library/unittest.html
[typing]: https://docs.python.org/3/library/typing.html
[mypy]: https://mypy.readthedocs.io/
[api reference]: https://michael.cousins.io/decoy/api/
[usage guide]: https://michael.cousins.io/decoy/usage/create/
[creating mocks]: https://michael.cousins.io/decoy/usage/create/
[stubbing with when]: https://michael.cousins.io/decoy/usage/when/
[spying with verify]: https://michael.cousins.io/decoy/usage/verify/