Advanced

One test, many cases

I wrote a separate function for each input I wanted to try and the file grew every time I thought of a case. Parametrising turns one test into a table of cases the runner runs for you.

What you will see

FAILED test_add.py::test_add[-1, 1, 1] ============================== 1 failed, 2 passed ==============================

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

Turn a single test into a table of cases and add a row to grow coverage.

1

Parametrise add over several cases in one function.

import pytest @pytest.mark.parametrize("a, b, want", [(2, 3, 5), (0, 0, 0), (-1, 1, 0)]) def test_add(a, b, want): assert add(a, b) == want

Before you run it, say out loud what it will print. Then open the answer.

What it prints
>>>
2

Run it. The runner reports three cases, all passing.

pytest test_add.py

Before you run it, say out loud what it will print. Then open the answer.

What it prints
collected 3 items test_add.py ... ============================== 3 passed in 0.01s ===============================
3

Add a wrong case as a new row. The runner names the failing row by its values.

pytest test_add.py

Before you run it, say out loud what it will print. Then open the answer.

What it prints
FAILED test_add.py::test_add[-1, 1, 1] ============================== 1 failed, 2 passed ==============================
The failing report names the exact tuple. You did not have to find which copy-pasted function broke, the table told you. That is the whole value of parametrising.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

The parametrize mark holds a table of cases, the runner calls the test once per row, and each run is reported by its case values.
1 Mark holds a table of cases
↓
2 Runner calls test per row
↓
3 Each run named by its values
↓
4 Failure points at one row
Why this works
The table is the spec

A parametrised test is a table of input and expected output. That table is a readable specification of the function: here is what it must return for these inputs. Growing coverage is adding a row, not writing a new function, which is why it scales.

Cases are named by value

Because the runner labels each run with its arguments, a failure tells you the exact case. This is the payoff over copy-pasted functions: the report is the broken row, and you can reproduce it by running that one case.

Examples

Pass the case in

@pytest.mark.parametrize("value", [1, 2, 3])
def test_positive(value):
    assert value > 0

The mark runs the function once per value. One function, three runs, and the runner reports each case by its value.

Input and answer together

@pytest.mark.parametrize("a, b, want", [(2, 3, 5), (0, 0, 0), (-1, 1, 0)])
def test_add(a, b, want):
    assert add(a, b) == want

Each tuple is one case: two inputs and the expected answer. The table is the list of cases, and adding a case is adding a tuple.

Read the report

test_add(2, 3, 5) PASSED
test_add(-1, 1, 1) FAILED

The runner names each run with its case values, so a failure points at the exact tuple that broke, not at a function you have to open.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Stepping with a debugger
Previous Lesson
Stepping with a debugger
Next Lesson
Choosing the hard cases
Choosing the hard cases →