Pytest’s parametrize feature is powerful but it looks scary. I hope this step-by-step explanation helps people use it more.
Writing tests can be difficult and repetitive. Pytest has a feature called parametrize that can make it reduce duplication, but it can be hard to understand if you are new to the testing world. It’s not as complicated as it seems.
Let’s say you have a function called add_nums()
that adds up a list of
numbers, and you want to write tests for it. Your tests might look like
this:
def test_123():
assert add_nums([1, 2, 3]) == 6
def test_negatives():
assert add_nums([1, 2, -3]) == 0
def test_empty():
assert add_nums([]) == 0
This is great: you’ve tested some behaviors of your add_nums()
function. But it’s getting tedious to write out more test cases. The names of the
function have to be different from each other, and they don’t mean anything, so
it’s extra work for no benefit. The test functions all have the same structure,
so you’re repeating uninteresting details. You want to add more cases but it
feels like there’s friction that you want to avoid.
If we look at these functions, they are very similar. In any software, when we have functions that are similar in structure, but differ in some details, we can refactor them to be one function with parameters for the differences. We can do the same for our test functions.
Here the functions all have the same structure: call add_nums()
and
assert what the return value should be. The differences are the list we pass to
add_nums()
and the value we expect it to return. So we can turn those
into two parameters in our refactored function:
def test_add_nums(nums, expected_total):
assert add_nums(nums) == expected_total
Unfortunately, tests aren’t run like regular functions. We write the test
functions, but we don’t call them ourselves. That’s the reason the names of the
test functions don’t matter. The test runner (pytest) finds functions named
test_*
and calls them for us. When they have no parameters, pytest can
call them directly. But now that our test function has two parameters, we have
to give pytest instructions about how to call it.
To do that, we use the @pytest.mark.parametrize
decorator. Using it
looks like this:
import pytest
@pytest.mark.parametrize(
"nums, expected_total",
[
([1, 2, 3], 6),
([1, 2, -3], 0),
([], 0),
]
)
def test_add_nums(nums, expected_total):
assert add_nums(nums) == expected_total
There’s a lot going on here, so let’s take it step by step.
If you haven’t seen a decorator before, it starts with @
and is like a
prologue to a function definition. It can affect how the function is defined or
provide information about the function.
The parametrize decorator is itself a function call that takes two arguments.
The first is a string (“nums, expected_total”) that names the two arguments to
the test function. Here the decorator is instructing pytest, “when you call
test_add_nums
, you will need to provide values for its nums and
expected_total parameters
.”
The second argument to parametrize
is a list of the values to supply
as the arguments. Each element of the list will become one call to our test
function. In this example, the list has three tuples, so pytest will call our
test function three times. Since we have two parameters to provide, each
element of the list is a tuple of two values.
The first tuple is ([1, 2, 3], 6)
, so the first time pytest calls
test_add_nums, it will call it as test_add_nums([1, 2, 3], 6). All together,
pytest will call us three times, like this:
test_add_nums([1, 2, 3], 6)
test_add_nums([1, 2, -3], 0)
test_add_nums([], 0)
This will all happen automatically. With our original test functions, when we ran pytest, it showed the results as three passing tests because we had three separate test functions. Now even though we only have one function, it still shows as three passing tests! Each set of values is considered a separate test that can pass or fail independently. This is the main advantage of using parametrize instead of writing three separate assert lines in the body of a simple test function.
What have we gained?
- We don’t have to write three separate functions with different names.
- We don’t have to repeat the same details in each function (
assert
,add_nums()
,==
). - The differences between the tests (the actual data) are written succinctly all in one place.
- Adding another test case is as simple as adding another line of data to the decorator.
Comments
Add a comment: