# Dictionary-based test parametrization for `pytest`
The decorator `@d_parametrize` (defined in `pytest_dparam`) offers an arguably more readable alternative to `@pytest.mark.parametrize`.
Test cases are configured through a dictionary so that:
- The name for each test case *precedes* its definition (the list of values to be used),
- The name for each argument/value is repeated in the test case definition.
Additionally, test cases covering the same topic can be bundled under the same name.
## Install
```
pip install pytest-dparam
```
## Syntax
`d_parametrize` takes as its input a dictionary, whose entries are the different test cases or sets of test cases (named through the dictionary keys).
Every test case is defined with a dictionary of value assignments.
For example:
```python
from pytest_dparam import d_parametrize
def square(x: int) -> int:
return x * x
@d_parametrize(
{
"trivial_case": {"input": 1, "expected": 1}, # test_square[trivial_case]
"negative_trivial_case": [
{"input": -1, "expected": 1}, # test_square[negative_trivial_case]
],
"positive_integers": [
{"input": 2, "expected": 4}, # test_square[positive_integers_0]
{"input": 3, "expected": 9}, # test_square[positive_integers_1]
],
"negative_integers": [
{"input": -2, "expected": 4}, # test_square[negative_integers_0]
{"input": -3, "expected": 9}, # test_square[negative_integers_1]
],
}
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
The keys are the names given to each test case or set of test cases.
### Defining an isolated test case
Each test case is described by a dictionary where the keys are the name of the arguments to be defined
(which would be the first argument of `pytest.mark.parametrize`),
and the values are the values to be given to said arguments.
For example, to ensure that our `square` function returns `1` for both `1` and `-1`:
```python
@d_parametrize(
{
"trivial_case": {"input": 1, "expected": 1},
"negative_trivial_case": {"input": -1, "expected": 1},
}
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
⚠ **All test cases must include the same keys in the same order.**
Otherwise, exception `pytest_dparam.InvalidParametrizedArgument` will be raised.
### Bundling cases together
It can be useful to provide several test cases to cover similar situations, calling for a same name.
In such a case, a list of test-case-describing dictionaries (or actually, any iterable of such dictionaries) can be provided instead of a single dictionary.
For example, if we want to test `square` with different negative numbers just to be sure:
```python
@d_parametrize(
{
# ...
"negative_integers": [
{"input": -2, "expected": 4},
{"input": -3, "expected": 9},
],
}
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
The name given to the set of test cases will be used for each included test case, with a numbered suffix (e.g., `negative_integers_0` and `negative_integers_1` in the previous example).
### Pseudo-bundling isolated cases for readability
For readability, you might appreciate having one line for the test case name, followed by a single-line test case description.
However, a code formatter such as Black can get in the way.
Or you might appreciate using lists all the time for consistency.
In any case, you can put an isolated test cases within a list. If it is alone in the list, its name will not be affected:
```python
@d_parametrize(
{
"trivial_case": [
{"input": 1, "expected": 1},
],
"negative_trivial_case": [
{"input": -1, "expected": 1},
],
}
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
### With test classes, mocks, etc.
The test function might be required to use additional arguments to the parametrized ones, such as a reference to *self* when in a test class, or `monkeypatch: pytest.MonkeyPatch` for mocking.
As when using `pytest.mark.parametrize`, those are simply ignored when using `d_parametrize`:
```python
class Test_class:
@d_parametrize(
{
"trivial_case": [
{"input": 1, "expected": 1},
],
# ...
}
)
def test_fun(input: int, expected: int, monkeypatch: pytest.MonkeyPatch):
# ...
assert actual == expected
```
## Under the hood
`d_parametrize(…)` actually just calls `pytest.mark.parametrize(…)` with the proper arguments,
based on the provided parametrization-describing dictionary,
and after asserting that the included test cases are compatible and valid.
Ultimately:
```python
@d_parametrize(
{
"trivial_case": {"input": 1, "expected": 1}, # test_square[trivial_case]
"negative_trivial_case": [
{"input": -1, "expected": 1}, # test_square[negative_trivial_case]
],
"positive_integers": [
{"input": 2, "expected": 4}, # test_square[positive_integers_0]
{"input": 3, "expected": 9}, # test_square[positive_integers_1]
],
"negative_integers": [
{"input": -2, "expected": 4}, # test_square[negative_integers_0]
{"input": -3, "expected": 9}, # test_square[negative_integers_1]
],
}
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
is literally equivalent to:
```python
@pytest.mark.parametrize(
("input", "expected"),
[
pytest.mark.parametrize( 1, 1, id="trivial_case"),
pytest.mark.parametrize(-1, 1, id="negative_trivial_case"),
pytest.mark.parametrize( 2, 4, id="positive_integers_"),
pytest.mark.parametrize( 3, 9, id="positive_integers_"),
pytest.mark.parametrize(-2, 4, id="negative_integers_"),
pytest.mark.parametrize(-3, 9, id="negative_integers_"),
]
)
def test_square(input: int, expected: int):
assert square(input) == expected
```
Raw data
{
"_id": null,
"home_page": null,
"name": "pytest-dparam",
"maintainer": null,
"docs_url": null,
"requires_python": null,
"maintainer_email": null,
"keywords": "pytest, tests, testing, parametrize, parametrized",
"author": null,
"author_email": "Bunker D <contact@bunkerd.fr>",
"download_url": "https://files.pythonhosted.org/packages/a3/e3/f864222dab87c72b1304bb065df9de42cdb6962df50eb42190eb6ea8ad0f/pytest_dparam-1.0.0.tar.gz",
"platform": null,
"description": "# Dictionary-based test parametrization for `pytest`\r\n\r\nThe decorator `@d_parametrize` (defined in `pytest_dparam`) offers an arguably more readable alternative to `@pytest.mark.parametrize`.\r\n\r\nTest cases are configured through a dictionary so that:\r\n- The name for each test case *precedes* its definition (the list of values to be used),\r\n- The name for each argument/value is repeated in the test case definition.\r\n\r\nAdditionally, test cases covering the same topic can be bundled under the same name.\r\n\r\n## Install\r\n\r\n```\r\npip install pytest-dparam\r\n```\r\n\r\n## Syntax\r\n\r\n`d_parametrize` takes as its input a dictionary, whose entries are the different test cases or sets of test cases (named through the dictionary keys).\r\n\r\nEvery test case is defined with a dictionary of value assignments.\r\n\r\nFor example:\r\n\r\n```python\r\nfrom pytest_dparam import d_parametrize\r\n\r\ndef square(x: int) -> int:\r\n return x * x\r\n\r\n@d_parametrize(\r\n {\r\n \"trivial_case\": {\"input\": 1, \"expected\": 1}, # test_square[trivial_case]\r\n \"negative_trivial_case\": [\r\n {\"input\": -1, \"expected\": 1}, # test_square[negative_trivial_case]\r\n ],\r\n \"positive_integers\": [\r\n {\"input\": 2, \"expected\": 4}, # test_square[positive_integers_0]\r\n {\"input\": 3, \"expected\": 9}, # test_square[positive_integers_1]\r\n ],\r\n \"negative_integers\": [\r\n {\"input\": -2, \"expected\": 4}, # test_square[negative_integers_0]\r\n {\"input\": -3, \"expected\": 9}, # test_square[negative_integers_1]\r\n ],\r\n }\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\n\r\nThe keys are the names given to each test case or set of test cases.\r\n\r\n### Defining an isolated test case\r\n\r\nEach test case is described by a dictionary where the keys are the name of the arguments to be defined\r\n(which would be the first argument of `pytest.mark.parametrize`),\r\nand the values are the values to be given to said arguments.\r\n\r\nFor example, to ensure that our `square` function returns `1` for both `1` and `-1`:\r\n\r\n```python\r\n@d_parametrize(\r\n {\r\n \"trivial_case\": {\"input\": 1, \"expected\": 1},\r\n \"negative_trivial_case\": {\"input\": -1, \"expected\": 1},\r\n }\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\n\r\n\u26a0 **All test cases must include the same keys in the same order.**\r\nOtherwise, exception `pytest_dparam.InvalidParametrizedArgument` will be raised.\r\n\r\n### Bundling cases together\r\n\r\nIt can be useful to provide several test cases to cover similar situations, calling for a same name.\r\nIn such a case, a list of test-case-describing dictionaries (or actually, any iterable of such dictionaries) can be provided instead of a single dictionary.\r\n\r\nFor example, if we want to test `square` with different negative numbers just to be sure:\r\n```python\r\n@d_parametrize(\r\n {\r\n # ...\r\n \"negative_integers\": [\r\n {\"input\": -2, \"expected\": 4},\r\n {\"input\": -3, \"expected\": 9},\r\n ],\r\n }\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\n\r\nThe name given to the set of test cases will be used for each included test case, with a numbered suffix (e.g., `negative_integers_0` and `negative_integers_1` in the previous example).\r\n\r\n### Pseudo-bundling isolated cases for readability\r\n\r\nFor readability, you might appreciate having one line for the test case name, followed by a single-line test case description.\r\nHowever, a code formatter such as Black can get in the way.\r\n\r\nOr you might appreciate using lists all the time for consistency.\r\n\r\nIn any case, you can put an isolated test cases within a list. If it is alone in the list, its name will not be affected:\r\n```python\r\n@d_parametrize(\r\n {\r\n \"trivial_case\": [\r\n {\"input\": 1, \"expected\": 1},\r\n ],\r\n \"negative_trivial_case\": [\r\n {\"input\": -1, \"expected\": 1},\r\n ],\r\n }\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\n\r\n### With test classes, mocks, etc.\r\n\r\nThe test function might be required to use additional arguments to the parametrized ones, such as a reference to *self* when in a test class, or `monkeypatch: pytest.MonkeyPatch` for mocking.\r\nAs when using `pytest.mark.parametrize`, those are simply ignored when using `d_parametrize`:\r\n```python\r\nclass Test_class:\r\n @d_parametrize(\r\n {\r\n \"trivial_case\": [\r\n {\"input\": 1, \"expected\": 1},\r\n ],\r\n # ...\r\n }\r\n )\r\n def test_fun(input: int, expected: int, monkeypatch: pytest.MonkeyPatch):\r\n # ...\r\n assert actual == expected\r\n```\r\n\r\n\r\n## Under the hood\r\n\r\n`d_parametrize(\u2026)` actually just calls `pytest.mark.parametrize(\u2026)` with the proper arguments,\r\nbased on the provided parametrization-describing dictionary,\r\nand after asserting that the included test cases are compatible and valid.\r\n\r\nUltimately:\r\n```python\r\n@d_parametrize(\r\n {\r\n \"trivial_case\": {\"input\": 1, \"expected\": 1}, # test_square[trivial_case]\r\n \"negative_trivial_case\": [\r\n {\"input\": -1, \"expected\": 1}, # test_square[negative_trivial_case]\r\n ],\r\n \"positive_integers\": [\r\n {\"input\": 2, \"expected\": 4}, # test_square[positive_integers_0]\r\n {\"input\": 3, \"expected\": 9}, # test_square[positive_integers_1]\r\n ],\r\n \"negative_integers\": [\r\n {\"input\": -2, \"expected\": 4}, # test_square[negative_integers_0]\r\n {\"input\": -3, \"expected\": 9}, # test_square[negative_integers_1]\r\n ],\r\n }\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\nis literally equivalent to:\r\n```python\r\n@pytest.mark.parametrize(\r\n (\"input\", \"expected\"),\r\n [\r\n pytest.mark.parametrize( 1, 1, id=\"trivial_case\"),\r\n pytest.mark.parametrize(-1, 1, id=\"negative_trivial_case\"),\r\n pytest.mark.parametrize( 2, 4, id=\"positive_integers_\"),\r\n pytest.mark.parametrize( 3, 9, id=\"positive_integers_\"),\r\n pytest.mark.parametrize(-2, 4, id=\"negative_integers_\"),\r\n pytest.mark.parametrize(-3, 9, id=\"negative_integers_\"),\r\n ]\r\n)\r\ndef test_square(input: int, expected: int):\r\n assert square(input) == expected\r\n```\r\n",
"bugtrack_url": null,
"license": null,
"summary": "A more readable alternative to @pytest.mark.parametrize.",
"version": "1.0.0",
"project_urls": {
"Documentation": "https://github.com/Bunker-D/pytest_dparam/blob/main/README.md",
"Homepage": "https://github.com/Bunker-D/pytest_dparam",
"Repository": "https://github.com/Bunker-D/pytest_dparam"
},
"split_keywords": [
"pytest",
" tests",
" testing",
" parametrize",
" parametrized"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "bb8edd161d90f672a5a2becb5f1f369588ebc431661b4438210e72a3dff131a0",
"md5": "6d0c914fd3a140e11ce1ac25659f1c0e",
"sha256": "ef0695e091caa5053e925bf09e7995611fca75d5a94b5e9d36c7f1b538703899"
},
"downloads": -1,
"filename": "pytest_dparam-1.0.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "6d0c914fd3a140e11ce1ac25659f1c0e",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 4763,
"upload_time": "2024-08-27T21:06:31",
"upload_time_iso_8601": "2024-08-27T21:06:31.829797Z",
"url": "https://files.pythonhosted.org/packages/bb/8e/dd161d90f672a5a2becb5f1f369588ebc431661b4438210e72a3dff131a0/pytest_dparam-1.0.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "a3e3f864222dab87c72b1304bb065df9de42cdb6962df50eb42190eb6ea8ad0f",
"md5": "3cd1b985bac4c365b05edeeb6d8f9483",
"sha256": "7ae08dc26c3ddb94301aebba0b8e876423efcb027cfd6c81f7748059f00e794b"
},
"downloads": -1,
"filename": "pytest_dparam-1.0.0.tar.gz",
"has_sig": false,
"md5_digest": "3cd1b985bac4c365b05edeeb6d8f9483",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 5772,
"upload_time": "2024-08-27T21:06:32",
"upload_time_iso_8601": "2024-08-27T21:06:32.940978Z",
"url": "https://files.pythonhosted.org/packages/a3/e3/f864222dab87c72b1304bb065df9de42cdb6962df50eb42190eb6ea8ad0f/pytest_dparam-1.0.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-27 21:06:32",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Bunker-D",
"github_project": "pytest_dparam",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "pytest-dparam"
}