decoy


Namedecoy JSON
Version 1.3.2 PyPI version JSON
download
home_pagehttps://mike.cousins.io/decoy/
SummaryOpinionated, typed stubbing and verification library for Python
upload_time2021-05-04 19:00:33
maintainer
docs_urlNone
authorMike Cousins
requires_python>=3.7,<4.0
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <div align="center">
    <h1>Decoy</h1>
    <img src="https://mike.cousins.io/decoy/img/decoy.png" width="256px">
    <p>Opinionated, typed stubbing and verification library for Python</p>
    <p>
        <a href="https://github.com/mcous/decoy/actions">
            <img title="CI Status" src="https://flat.badgen.net/github/checks/mcous/decoy/main">
        </a>
        <a href="https://pypi.org/project/decoy/">
            <img title="PyPI Version" src="https://flat.badgen.net/pypi/v/decoy">
        </a>
        <a href="https://github.com/mcous/decoy/blob/main/LICENSE">
            <img title="License" src="https://flat.badgen.net/github/license/mcous/decoy">
        </a>
    </p>
    <p>
        <a href="https://mike.cousins.io/decoy/">https://mike.cousins.io/decoy/</a>
    </p>
</div>

The Decoy library allows you to create, stub, and verify test double objects for your Python unit tests, so your tests are:

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

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

[testdouble.js]: https://github.com/testdouble/testdouble.js
[mockito]: https://site.mockito.org/

## Install

```bash
# pip
pip install decoy

# poetry
poetry add --dev decoy
```

## Setup

You'll want to create a test fixture to reset Decoy state between each test run. In [pytest][], you can do this by using a fixture to create a new Decoy instance for every test.

The examples below assume the following global test fixture:

```python
import pytest
from decoy import Decoy

@pytest.fixture
def decoy() -> Decoy:
    return Decoy()
```

Why is this important? The `Decoy` container tracks every test double that is created during a test so that you can define assertions using fully-typed rehearsals. It's important to wipe this slate clean for every test so you don't leak memory or have any state preservation between tests.

[pytest]: https://docs.pytest.org/

### Mypy Setup

Decoy's rehearsal syntax can be a bit confusing to [mypy][] if the mock in question is supposed to return `None`. Normally, [mypy will complain][] if you try to use a `None`-returning expression as a value, because this is almost always a mistake.

In Decoy, however, it's an intentional part of the API and _not_ a mistake. To suppress these errors, Decoy provides a mypy plugin that you should add to your configuration file:

```ini
# mypi.ini

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

[mypy]: https://mypy.readthedocs.io/
[mypy will complain]: https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-called-function-returns-a-value-func-returns-value

## Usage

### Stubbing

A stub is an object used in a test that is pre-configured to return a result or raise an error if called according to a specification. In Decoy, you specify a stub's call conditions with a "rehearsal", which is simply a call to the stub inside of a `decoy.when` wrapper.

By pre-configuring the stub with specific rehearsals, you get the following benefits:

-   Your test double will only return your mock value **if it is called correctly**
-   You avoid separate "set up mock return value" and "assert mock called correctly" steps
-   If you annotate your test double with an actual type, the rehearsal will fail typechecking if called incorrectly

```python
import pytest
from typing import cast, Optional
from decoy import Decoy

from .database import Database, Model

def get_item(uid: str, db: Database) -> Optional[Model]:
  return db.get_by_id(uid)

def test_get_item(decoy: Decoy):
    mock_item = cast(Model, { "foo": "bar" })
    mock_db = decoy.create_decoy(spec=Database)

    # arrange stub using rehearsals
    decoy.when(mock_db.get_by_id("some-id")).then_return(mock_item)

    # call code under test
    some_result = get_item("some-id")
    other_result = get_item("other-id")

    # assert code result
    assert some_result == mock_item
    assert other_result is None
```

### Verifying interactions

If you're coming from `unittest.mock`, you're probably used to calling your code under test and _then_ verifying that your dependency was called correctly. Decoy provides similar call verification using the same "rehearsal" mechanism that the stubbing API uses.

```python
import pytest
from typing import cast, Optional
from decoy import Decoy, verify

from .logger import Logger

def log_warning(msg: str, logger: Logger) -> None:
    logger.warn(msg)

def test_log_warning(decoy: Decoy):
    logger = decoy.create_decoy(spec=Logger)

    # call code under test
    some_result = log_warning("oh no!", logger)

    # verify double called correctly with a rehearsal
    decoy.verify(logger.warn("oh no!"))
```

Asserting that calls happened after the fact can be useful, but **should only be used if the dependency is being called solely for its side-effect(s)**. Verification of interactions in this manner should be considered a last resort, because:

-   If you're calling a dependency to get data, then you can more precisely describe that relationship using [stubbing](#stubbing)
-   Side-effects are harder to understand and maintain than pure functions, so in general you should try to side-effect sparingly

Stubbing and verification of a decoy are **mutually exclusive** within a test. If you find yourself wanting to both stub and verify the same decoy, then one or more of these is true:

-   The assertions are redundant
-   The dependency is doing too much based on its input (e.g. side-effecting _and_ calculating complex data) and should be refactored

#### Verifying order of multiple calls

If your code under test must call several dependencies in order, you may pass multiple rehearsals to `verify`. Decoy will search through the list of all calls made to the given spies and look for the exact rehearsal sequence given, in order.

```python
decoy.verify(
    handler.call_first_procedure("hello"),
    handler.call_second_procedure("world"),
)
```

### Usage with async/await

Decoy supports async/await out of the box! Pass your async function or class with async methods to `spec` in `decoy.create_decoy_func` or `decoy.create_decoy`, respectively, and Decoy will figure out the rest.

When writing rehearsals on async functions and methods, remember to include the `await` with your rehearsal call:

```py
decoy.when(await mock_db.get_by_id("some-id")).then_return(mock_item)
```

### Matchers

Sometimes, when you're stubbing or verifying calls (or really when you're doing any sort of equality assertion in a test), you need to loosen a given assertion. For example, you may want to assert that a dependency is called with a string, but you don't care about the full contents of that string.

Decoy includes a set of matchers, which are simply Python classes with `__eq__` methods defined, that you can use in rehearsals and/or assertions.

```python
import pytest
from typing import cast, Optional
from decoy import Decoy, matchers

from .logger import Logger

def log_warning(msg: str, logger: Logger) -> None:
    logger.warn(msg)

def test_log_warning(decoy: Decoy):
    logger = decoy.create_decoy(spec=Logger)

    # call code under test
    some_result = log_warning(
        "Oh no, something went wrong with request ID abc123efg456",
        logger=logger
    )

    # verify double called correctly
    decoy.verify(
        logger.warn(matchers.StringMatching("request ID abc123efg456"))
    )
```

            

Raw data

            {
    "_id": null,
    "home_page": "https://mike.cousins.io/decoy/",
    "name": "decoy",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.7,<4.0",
    "maintainer_email": "",
    "keywords": "",
    "author": "Mike Cousins",
    "author_email": "mike@cousins.io",
    "download_url": "https://files.pythonhosted.org/packages/70/32/771139dc5bd53f5c1e772b3df4a32033c2c6b2c526eedf482ef659dd663d/decoy-1.3.2.tar.gz",
    "platform": "",
    "description": "<div align=\"center\">\n    <h1>Decoy</h1>\n    <img src=\"https://mike.cousins.io/decoy/img/decoy.png\" width=\"256px\">\n    <p>Opinionated, typed stubbing and verification library for Python</p>\n    <p>\n        <a href=\"https://github.com/mcous/decoy/actions\">\n            <img title=\"CI Status\" src=\"https://flat.badgen.net/github/checks/mcous/decoy/main\">\n        </a>\n        <a href=\"https://pypi.org/project/decoy/\">\n            <img title=\"PyPI Version\" src=\"https://flat.badgen.net/pypi/v/decoy\">\n        </a>\n        <a href=\"https://github.com/mcous/decoy/blob/main/LICENSE\">\n            <img title=\"License\" src=\"https://flat.badgen.net/github/license/mcous/decoy\">\n        </a>\n    </p>\n    <p>\n        <a href=\"https://mike.cousins.io/decoy/\">https://mike.cousins.io/decoy/</a>\n    </p>\n</div>\n\nThe Decoy library allows you to create, stub, and verify test double objects for your Python unit tests, so your tests are:\n\n-   Less prone to insufficient tests due to unconditional stubbing\n-   Covered by typechecking\n-   Easier to fit into the Arrange-Act-Assert pattern\n\nThe Decoy API is heavily inspired by / stolen from the excellent [testdouble.js][] and [Mockito][] projects.\n\n[testdouble.js]: https://github.com/testdouble/testdouble.js\n[mockito]: https://site.mockito.org/\n\n## Install\n\n```bash\n# pip\npip install decoy\n\n# poetry\npoetry add --dev decoy\n```\n\n## Setup\n\nYou'll want to create a test fixture to reset Decoy state between each test run. In [pytest][], you can do this by using a fixture to create a new Decoy instance for every test.\n\nThe examples below assume the following global test fixture:\n\n```python\nimport pytest\nfrom decoy import Decoy\n\n@pytest.fixture\ndef decoy() -> Decoy:\n    return Decoy()\n```\n\nWhy is this important? The `Decoy` container tracks every test double that is created during a test so that you can define assertions using fully-typed rehearsals. It's important to wipe this slate clean for every test so you don't leak memory or have any state preservation between tests.\n\n[pytest]: https://docs.pytest.org/\n\n### Mypy Setup\n\nDecoy's rehearsal syntax can be a bit confusing to [mypy][] if the mock in question is supposed to return `None`. Normally, [mypy will complain][] if you try to use a `None`-returning expression as a value, because this is almost always a mistake.\n\nIn Decoy, however, it's an intentional part of the API and _not_ a mistake. To suppress these errors, Decoy provides a mypy plugin that you should add to your configuration file:\n\n```ini\n# mypi.ini\n\n# ...\nplugins = decoy.mypy\n# ...\n```\n\n[mypy]: https://mypy.readthedocs.io/\n[mypy will complain]: https://mypy.readthedocs.io/en/stable/error_code_list.html#check-that-called-function-returns-a-value-func-returns-value\n\n## Usage\n\n### Stubbing\n\nA stub is an object used in a test that is pre-configured to return a result or raise an error if called according to a specification. In Decoy, you specify a stub's call conditions with a \"rehearsal\", which is simply a call to the stub inside of a `decoy.when` wrapper.\n\nBy pre-configuring the stub with specific rehearsals, you get the following benefits:\n\n-   Your test double will only return your mock value **if it is called correctly**\n-   You avoid separate \"set up mock return value\" and \"assert mock called correctly\" steps\n-   If you annotate your test double with an actual type, the rehearsal will fail typechecking if called incorrectly\n\n```python\nimport pytest\nfrom typing import cast, Optional\nfrom decoy import Decoy\n\nfrom .database import Database, Model\n\ndef get_item(uid: str, db: Database) -> Optional[Model]:\n  return db.get_by_id(uid)\n\ndef test_get_item(decoy: Decoy):\n    mock_item = cast(Model, { \"foo\": \"bar\" })\n    mock_db = decoy.create_decoy(spec=Database)\n\n    # arrange stub using rehearsals\n    decoy.when(mock_db.get_by_id(\"some-id\")).then_return(mock_item)\n\n    # call code under test\n    some_result = get_item(\"some-id\")\n    other_result = get_item(\"other-id\")\n\n    # assert code result\n    assert some_result == mock_item\n    assert other_result is None\n```\n\n### Verifying interactions\n\nIf you're coming from `unittest.mock`, you're probably used to calling your code under test and _then_ verifying that your dependency was called correctly. Decoy provides similar call verification using the same \"rehearsal\" mechanism that the stubbing API uses.\n\n```python\nimport pytest\nfrom typing import cast, Optional\nfrom decoy import Decoy, verify\n\nfrom .logger import Logger\n\ndef log_warning(msg: str, logger: Logger) -> None:\n    logger.warn(msg)\n\ndef test_log_warning(decoy: Decoy):\n    logger = decoy.create_decoy(spec=Logger)\n\n    # call code under test\n    some_result = log_warning(\"oh no!\", logger)\n\n    # verify double called correctly with a rehearsal\n    decoy.verify(logger.warn(\"oh no!\"))\n```\n\nAsserting that calls happened after the fact can be useful, but **should only be used if the dependency is being called solely for its side-effect(s)**. Verification of interactions in this manner should be considered a last resort, because:\n\n-   If you're calling a dependency to get data, then you can more precisely describe that relationship using [stubbing](#stubbing)\n-   Side-effects are harder to understand and maintain than pure functions, so in general you should try to side-effect sparingly\n\nStubbing and verification of a decoy are **mutually exclusive** within a test. If you find yourself wanting to both stub and verify the same decoy, then one or more of these is true:\n\n-   The assertions are redundant\n-   The dependency is doing too much based on its input (e.g. side-effecting _and_ calculating complex data) and should be refactored\n\n#### Verifying order of multiple calls\n\nIf your code under test must call several dependencies in order, you may pass multiple rehearsals to `verify`. Decoy will search through the list of all calls made to the given spies and look for the exact rehearsal sequence given, in order.\n\n```python\ndecoy.verify(\n    handler.call_first_procedure(\"hello\"),\n    handler.call_second_procedure(\"world\"),\n)\n```\n\n### Usage with async/await\n\nDecoy supports async/await out of the box! Pass your async function or class with async methods to `spec` in `decoy.create_decoy_func` or `decoy.create_decoy`, respectively, and Decoy will figure out the rest.\n\nWhen writing rehearsals on async functions and methods, remember to include the `await` with your rehearsal call:\n\n```py\ndecoy.when(await mock_db.get_by_id(\"some-id\")).then_return(mock_item)\n```\n\n### Matchers\n\nSometimes, when you're stubbing or verifying calls (or really when you're doing any sort of equality assertion in a test), you need to loosen a given assertion. For example, you may want to assert that a dependency is called with a string, but you don't care about the full contents of that string.\n\nDecoy includes a set of matchers, which are simply Python classes with `__eq__` methods defined, that you can use in rehearsals and/or assertions.\n\n```python\nimport pytest\nfrom typing import cast, Optional\nfrom decoy import Decoy, matchers\n\nfrom .logger import Logger\n\ndef log_warning(msg: str, logger: Logger) -> None:\n    logger.warn(msg)\n\ndef test_log_warning(decoy: Decoy):\n    logger = decoy.create_decoy(spec=Logger)\n\n    # call code under test\n    some_result = log_warning(\n        \"Oh no, something went wrong with request ID abc123efg456\",\n        logger=logger\n    )\n\n    # verify double called correctly\n    decoy.verify(\n        logger.warn(matchers.StringMatching(\"request ID abc123efg456\"))\n    )\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Opinionated, typed stubbing and verification library for Python",
    "version": "1.3.2",
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "d31e2f47c7a91c3d0e018826feacb5d1",
                "sha256": "f5431caaa9dbd8125dbcd500fb5caf5f48fd3739178bf40cc942b9d0cd017322"
            },
            "downloads": -1,
            "filename": "decoy-1.3.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "d31e2f47c7a91c3d0e018826feacb5d1",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7,<4.0",
            "size": 14132,
            "upload_time": "2021-05-04T19:00:31",
            "upload_time_iso_8601": "2021-05-04T19:00:31.275117Z",
            "url": "https://files.pythonhosted.org/packages/40/88/f22e0660e39cf7d8114700b86370a50bb906ce5c9e0505e635d733aac993/decoy-1.3.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "87bdc60af521977f105f2f5c63a30837",
                "sha256": "42ded96ad3d059018895eb2e660329fe31784e2a09531e0cb595c8fd8469ea8b"
            },
            "downloads": -1,
            "filename": "decoy-1.3.2.tar.gz",
            "has_sig": false,
            "md5_digest": "87bdc60af521977f105f2f5c63a30837",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7,<4.0",
            "size": 14952,
            "upload_time": "2021-05-04T19:00:33",
            "upload_time_iso_8601": "2021-05-04T19:00:33.287466Z",
            "url": "https://files.pythonhosted.org/packages/70/32/771139dc5bd53f5c1e772b3df4a32033c2c6b2c526eedf482ef659dd663d/decoy-1.3.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2021-05-04 19:00:33",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "lcname": "decoy"
}
        
Elapsed time: 0.23879s