Taming parametrize with pytest.param
Redowan Delowar
August 28, 2024
I love [pytest.mark.parametrize] - so much so that I sometimes shoehorn my tests to fit into
it. But the default style of writing tests with parametrize can quickly turn into an
unreadable mess as the test complexity grows. For example:
The polarify function converts Cartesian coordinates to polar coordinates. We're using
@pytest.mark.parametrize in its standard form to test different conditions.
Here, the list of nested tuples with inputs and expected values becomes hard to read as the
test suite grows larger. When the function under test has a more complex signature, I find
myself needing to do more mental gymnastics to parse the positional input and expected
values inside parametrize.
Also, how do you run a specific test case within the suite? For instance, what if you want
to run only the third case where x, y, expected = (0, 1, (1, 1.5707963267948966))?
I used to set custom test IDs like below to be able to run individual test cases within
parametrize:
This works, but mentally associating the IDs with the examples is cumbersome, and it doesn't
make things any easier to read.
TIL, [pytest.param] gives you a better syntax and more control to achieve the same. Observe:
We're setting the unique IDs inside pytest.param. Now, any test can be targeted with
pytest's -k flag like this:
This will only run the second test case on the list.
Or,
This will run the last two tests.
But the test is still somewhat hard to read. I usually refactor mine to take a kwargs
argument so that I can neatly tuck all the input and expected values associated with a test
case in a single dictionary. Notice:
Everything associated with a single test case is passed to pytest.param in a single
dictionary, eliminating the need to guess any positional arguments.
Using pytest.param also allows you to set custom test execution conditionals, which I've
started to take advantage of recently:
In the last block, pytest.param bundles test data with execution conditions. We're using
xfail to mark a test as expected to fail, while skipif skips tests based on conditions.
This keeps all the logic for handling test cases, including failures and skips, directly
alongside the test data.
[pytest.mark.parametrize]:
https://docs.pytest.org/en/7.1.x/how-to/parametrize.html#parametrize-basics
[pytest.param]:
https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest-param
Discussion in the ATmosphere