pytest-describe


Namepytest-describe JSON
Version 2.2.0 PyPI version JSON
download
home_pagehttps://github.com/pytest-dev/pytest-describe
SummaryDescribe-style plugin for pytest
upload_time2024-02-10 15:30:35
maintainerChristoph Zwerschke
docs_urlNone
authorRobin Pedersen
requires_python>=3.7
licenseMIT
keywords test unittest plugin describe
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![PyPI version](https://badge.fury.io/py/pytest-describe.svg)](https://pypi.org/project/pytest-describe/)
[![Workflow status](https://github.com/pytest-dev/pytest-describe/actions/workflows/main.yml/badge.svg)](https://github.com/pytest-dev/pytest-describe/actions)

# Describe-style plugin for pytest

**pytest-describe** is a plugin for [pytest](https://docs.pytest.org/)
that allows tests to be written in arbitrary nested describe-blocks,
similar to RSpec (Ruby) and Jasmine (JavaScript).

The main inspiration for this was
a [video](https://www.youtube.com/watch?v=JJle8L8FRy0>) by Gary Bernhardt.

## Installation

You guessed it:

```sh
pip install pytest-describe
```

## Usage

Pytest will automatically find the plugin and use it when you run pytest. 
Running pytest will show that the plugin is loaded:

```sh
$ pytest 
...
plugins: describe-2.2.0
...
```

Tests can now be written in describe-blocks.
Here is an example for testing a Wallet class:

```python 
import pytest


class Wallet:

    def __init__(self, initial_amount=0):
        self.balance = initial_amount

    def spend_cash(self, amount):
        if self.balance < amount:
            raise ValueError(f'Not enough available to spend {amount}')
        self.balance -= amount

    def add_cash(self, amount):
        self.balance += amount
        
        
def describe_wallet():
    
    def describe_start_empty():
        
        @pytest.fixture
        def wallet():
            return Wallet()

        def initial_amount(wallet):
            assert wallet.balance == 0
    
        def add_cash(wallet):
            wallet.add_cash(80)
            assert wallet.balance == 80

        def spend_cash(wallet):
            with pytest.raises(ValueError):
                wallet.spend_cash(10)

    def describe_with_starting_balance():
        
        @pytest.fixture
        def wallet():
            return Wallet(20)

        def initial_amount(wallet):
            assert wallet.balance == 20
    
        def describe_adding():
            
            def add_little_cash(wallet):
                wallet.add_cash(5)
                assert wallet.balance == 25
    
            def add_much_cash(wallet):
                wallet.add_cash(980)
                assert wallet.balance == 1000
                
        def describe_spending():
            
            def spend_cash(wallet):
                wallet.spend_cash(15)
                assert wallet.balance == 5
        
            def spend_too_much_cash(wallet):
                with pytest.raises(ValueError):
                    wallet.spend_cash(25)
```

The default prefix for describe-blocks is `describe_`, but you can configure it 
in the pytest/python configuration file via `describe_prefixes` or
via the command line option `--describe-prefixes`.

For example in your `pyproject.toml`:

```toml    
[tool.pytest.ini_options]
describe_prefixes = ["custom_prefix_"]
```

Functions prefixed with `_` in the describe-block are not collected as tests. 
This can be used to group helper functions. Otherwise, functions inside the 
describe-blocks need not follow any special naming convention.

```python
def describe_function():

    def _helper():
        return "something"

    def it_does_something():
        value = _helper()
        ...
```


## Why bother?

I've found that quite often my tests have one "dimension" more than my production
code. The production code is organized into packages, modules, classes
(sometimes), and functions. I like to organize my tests in the same way, but
tests also have different *cases* for each function. This tends to end up with
a set of tests for each module (or class), where each test has to name both a
function and a *case*. For instance:

```python
def test_my_function_with_default_arguments():
def test_my_function_with_some_other_arguments():
def test_my_function_throws_exception():
def test_my_function_handles_exception():
def test_some_other_function_returns_true():
def test_some_other_function_returns_false():
```

It's much nicer to do this:

```python
def describe_my_function():
    def with_default_arguments():
    def with_some_other_arguments():
    def it_throws_exception():
    def it_handles_exception():

def describe_some_other_function():
    def it_returns_true():
    def it_returns_false():
```

It has the additional advantage that you can have marks and fixtures that apply
locally to each group of test function.

With pytest, it's possible to organize tests in a similar way with classes.
However, I think classes are awkward. I don't think the convention of using
camel-case names for classes fit very well when testing functions in different
cases. In addition, every test function must take a "self" argument that is
never used.

The pytest-describe plugin allows organizing your tests in the nicer way shown
above using describe-blocks.

## Shared Behaviors

If you've used rspec's shared examples or test class inheritance, then you may
be familiar with the benefit of having the same tests apply to
multiple "subjects" or "suts" (system under test).

```python
from pytest import fixture
from pytest_describe import behaves_like

def a_duck():
    def it_quacks(sound):
        assert sound == "quack"

@behaves_like(a_duck)
def describe_something_that_quacks():
    @fixture
    def sound():
        return "quack"

    # the it_quacks test in this describe will pass

@behaves_like(a_duck)
def describe_something_that_barks():
    @fixture
    def sound():
        return "bark"

    # the it_quacks test in this describe will fail (as expected)
```

Fixtures defined in the block that includes the shared behavior take precedence
over fixtures defined in the shared behavior. This rule only applies to
fixtures, not to other functions (nested describe blocks and tests). Instead,
they are all collected as separate tests.

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/pytest-dev/pytest-describe",
    "name": "pytest-describe",
    "maintainer": "Christoph Zwerschke",
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": "cito@online.de",
    "keywords": "test,unittest,plugin,describe",
    "author": "Robin Pedersen",
    "author_email": "robinpeder@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/61/58/4079baf5a7937159aa59b2f696f8d61c55a6ae4df87bd9ed49e7e130df21/pytest-describe-2.2.0.tar.gz",
    "platform": "unix",
    "description": "[![PyPI version](https://badge.fury.io/py/pytest-describe.svg)](https://pypi.org/project/pytest-describe/)\n[![Workflow status](https://github.com/pytest-dev/pytest-describe/actions/workflows/main.yml/badge.svg)](https://github.com/pytest-dev/pytest-describe/actions)\n\n# Describe-style plugin for pytest\n\n**pytest-describe** is a plugin for [pytest](https://docs.pytest.org/)\nthat allows tests to be written in arbitrary nested describe-blocks,\nsimilar to RSpec (Ruby) and Jasmine (JavaScript).\n\nThe main inspiration for this was\na [video](https://www.youtube.com/watch?v=JJle8L8FRy0>) by Gary Bernhardt.\n\n## Installation\n\nYou guessed it:\n\n```sh\npip install pytest-describe\n```\n\n## Usage\n\nPytest will automatically find the plugin and use it when you run pytest. \nRunning pytest will show that the plugin is loaded:\n\n```sh\n$ pytest \n...\nplugins: describe-2.2.0\n...\n```\n\nTests can now be written in describe-blocks.\nHere is an example for testing a Wallet class:\n\n```python \nimport pytest\n\n\nclass Wallet:\n\n    def __init__(self, initial_amount=0):\n        self.balance = initial_amount\n\n    def spend_cash(self, amount):\n        if self.balance < amount:\n            raise ValueError(f'Not enough available to spend {amount}')\n        self.balance -= amount\n\n    def add_cash(self, amount):\n        self.balance += amount\n        \n        \ndef describe_wallet():\n    \n    def describe_start_empty():\n        \n        @pytest.fixture\n        def wallet():\n            return Wallet()\n\n        def initial_amount(wallet):\n            assert wallet.balance == 0\n    \n        def add_cash(wallet):\n            wallet.add_cash(80)\n            assert wallet.balance == 80\n\n        def spend_cash(wallet):\n            with pytest.raises(ValueError):\n                wallet.spend_cash(10)\n\n    def describe_with_starting_balance():\n        \n        @pytest.fixture\n        def wallet():\n            return Wallet(20)\n\n        def initial_amount(wallet):\n            assert wallet.balance == 20\n    \n        def describe_adding():\n            \n            def add_little_cash(wallet):\n                wallet.add_cash(5)\n                assert wallet.balance == 25\n    \n            def add_much_cash(wallet):\n                wallet.add_cash(980)\n                assert wallet.balance == 1000\n                \n        def describe_spending():\n            \n            def spend_cash(wallet):\n                wallet.spend_cash(15)\n                assert wallet.balance == 5\n        \n            def spend_too_much_cash(wallet):\n                with pytest.raises(ValueError):\n                    wallet.spend_cash(25)\n```\n\nThe default prefix for describe-blocks is `describe_`, but you can configure it \nin the pytest/python configuration file via `describe_prefixes` or\nvia the command line option `--describe-prefixes`.\n\nFor example in your `pyproject.toml`:\n\n```toml    \n[tool.pytest.ini_options]\ndescribe_prefixes = [\"custom_prefix_\"]\n```\n\nFunctions prefixed with `_` in the describe-block are not collected as tests. \nThis can be used to group helper functions. Otherwise, functions inside the \ndescribe-blocks need not follow any special naming convention.\n\n```python\ndef describe_function():\n\n    def _helper():\n        return \"something\"\n\n    def it_does_something():\n        value = _helper()\n        ...\n```\n\n\n## Why bother?\n\nI've found that quite often my tests have one \"dimension\" more than my production\ncode. The production code is organized into packages, modules, classes\n(sometimes), and functions. I like to organize my tests in the same way, but\ntests also have different *cases* for each function. This tends to end up with\na set of tests for each module (or class), where each test has to name both a\nfunction and a *case*. For instance:\n\n```python\ndef test_my_function_with_default_arguments():\ndef test_my_function_with_some_other_arguments():\ndef test_my_function_throws_exception():\ndef test_my_function_handles_exception():\ndef test_some_other_function_returns_true():\ndef test_some_other_function_returns_false():\n```\n\nIt's much nicer to do this:\n\n```python\ndef describe_my_function():\n    def with_default_arguments():\n    def with_some_other_arguments():\n    def it_throws_exception():\n    def it_handles_exception():\n\ndef describe_some_other_function():\n    def it_returns_true():\n    def it_returns_false():\n```\n\nIt has the additional advantage that you can have marks and fixtures that apply\nlocally to each group of test function.\n\nWith pytest, it's possible to organize tests in a similar way with classes.\nHowever, I think classes are awkward. I don't think the convention of using\ncamel-case names for classes fit very well when testing functions in different\ncases. In addition, every test function must take a \"self\" argument that is\nnever used.\n\nThe pytest-describe plugin allows organizing your tests in the nicer way shown\nabove using describe-blocks.\n\n## Shared Behaviors\n\nIf you've used rspec's shared examples or test class inheritance, then you may\nbe familiar with the benefit of having the same tests apply to\nmultiple \"subjects\" or \"suts\" (system under test).\n\n```python\nfrom pytest import fixture\nfrom pytest_describe import behaves_like\n\ndef a_duck():\n    def it_quacks(sound):\n        assert sound == \"quack\"\n\n@behaves_like(a_duck)\ndef describe_something_that_quacks():\n    @fixture\n    def sound():\n        return \"quack\"\n\n    # the it_quacks test in this describe will pass\n\n@behaves_like(a_duck)\ndef describe_something_that_barks():\n    @fixture\n    def sound():\n        return \"bark\"\n\n    # the it_quacks test in this describe will fail (as expected)\n```\n\nFixtures defined in the block that includes the shared behavior take precedence\nover fixtures defined in the shared behavior. This rule only applies to\nfixtures, not to other functions (nested describe blocks and tests). Instead,\nthey are all collected as separate tests.\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Describe-style plugin for pytest",
    "version": "2.2.0",
    "project_urls": {
        "Homepage": "https://github.com/pytest-dev/pytest-describe",
        "Source": "https://github.com/pytest-dev/pytest-describe",
        "Tracker": "https://github.com/pytest-dev/pytest-describe/issues"
    },
    "split_keywords": [
        "test",
        "unittest",
        "plugin",
        "describe"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5d952990d6e3e777be36690700b524d7daf44c9562158d1af6a8f85a6236f22c",
                "md5": "5f389f04ea46458a897be179f1b551f4",
                "sha256": "bd9e2c73acb4b9522a8400823d98f5b6a081667d3bfd7243a8598336896b544d"
            },
            "downloads": -1,
            "filename": "pytest_describe-2.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "5f389f04ea46458a897be179f1b551f4",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 6940,
            "upload_time": "2024-02-10T15:30:33",
            "upload_time_iso_8601": "2024-02-10T15:30:33.451907Z",
            "url": "https://files.pythonhosted.org/packages/5d/95/2990d6e3e777be36690700b524d7daf44c9562158d1af6a8f85a6236f22c/pytest_describe-2.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "61584079baf5a7937159aa59b2f696f8d61c55a6ae4df87bd9ed49e7e130df21",
                "md5": "a48d626f4062bcea8094059bb8daefb1",
                "sha256": "39bb05eb90f2497d9ca342ef9a0b7fa5bada7e58505aec33f66d661d631955b7"
            },
            "downloads": -1,
            "filename": "pytest-describe-2.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "a48d626f4062bcea8094059bb8daefb1",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 10907,
            "upload_time": "2024-02-10T15:30:35",
            "upload_time_iso_8601": "2024-02-10T15:30:35.224582Z",
            "url": "https://files.pythonhosted.org/packages/61/58/4079baf5a7937159aa59b2f696f8d61c55a6ae4df87bd9ed49e7e130df21/pytest-describe-2.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-02-10 15:30:35",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "pytest-dev",
    "github_project": "pytest-describe",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "pytest-describe"
}
        
Elapsed time: 0.21765s