Fixtures & parametrize
Share setup with fixtures and cover many cases with parametrize while you TDD a module.
Fixtures and parametrize
Why this matters
Every test for an inventory module needs the same starting state: a stock dict, maybe a temp file or a DB connection. Copy that setup into each test and one change to the shape breaks twenty tests at once. pytest fixtures give you one named source for that setup, and parametrize lets a single test body cover a whole table of cases so a failure points at the exact row that broke. Interviewers watch for this. Writing five near-identical test_ functions signals you do not know the tooling.
Fixtures: named, injected setup
A fixture is a function decorated with @pytest.fixture that builds a value your tests need. Any test that names the fixture as a parameter receives the returned value. pytest matches by name and injects it.
import pytest
@pytest.fixture
def base_stock():
return {"apple": 5, "pear": 2}
def test_restock_adds_item(base_stock): # pytest passes base_stock in
result = restock(base_stock, {"plum": 3})
assert result["plum"] == 3
By default a fixture has function scope: pytest calls it fresh for every test, so base_stock is a brand-new dict each time and tests cannot leak state into one another. A fixture that uses yield instead of return runs the code after yield as teardown once the test finishes.
- Fixture setupeverything above the yield runs
- yieldhands the value to the test as its argument
- Test bodyruns with that value
- Teardowneverything below the yield, even if the test FAILED
Scope decides how often that whole cycle repeats:
| scope= | Set up once per | Reach for it when |
|---|---|---|
| function (default) | every test | the value is mutable and tests must not leak into each other |
| class | test class | a group of tests shares expensive but read-only setup |
| module | test file | one connection or server serves every test in the file |
| session | whole test run | the setup is very expensive and genuinely immutable |
Parametrize: one body, many cases
@pytest.mark.parametrize takes a string of parameter names and a list of value rows. pytest runs the test once per row and reports each as its own case.
@pytest.mark.parametrize("stock, additions, expected", [
({"a": 1}, {"a": 1}, {"a": 2}), # shared key sums
({}, {"x": 4}, {"x": 4}), # new key added
])
def test_restock(stock, additions, expected):
assert restock(stock, additions) == expected
Two rows means two independent results, so a failure names the row instead of just "test_restock failed".
The demo below shows the restock you will build. It copies stock with dict(stock), then adds each quantity onto result.get(item, 0), returning a new dict. This sandbox has no pytest installed, so the practice tests express the same ideas directly: a helper builds the base stock and a list of case tuples is looped over. The concepts (shared setup, a table of cases) are identical; only the injection machinery differs.
Pitfall: shared mutable fixtures
A fixture that returns a mutable object at function scope is safe, but widen the scope and that object is shared:
@pytest.fixture(scope="module")
def base_stock():
return {"apple": 5}
Now every test in the module gets the same dict. If restock mutates its input (for example result = stock instead of result = dict(stock), which makes result and stock the same object), one test's change bleeds into the next, and tests pass or fail depending on order. The Practice suite checks exactly this: return a new dict and never touch stock.
Interview nuance: parametrize is not a loop inside one test. A for loop stops at the first failing assert and hides every case after it. parametrize generates N separate tests, so all N run, each gets its own id in the report, and you can xfail or skip a single case with pytest.param. That independence, together with function-scope fixture isolation, is what makes a suite deterministic no matter what order it runs in.
def restock(stock, additions):
result = dict(stock)
for item, qty in additions.items():
result[item] = result.get(item, 0) + qty
return result
print(restock({"apple": 5}, {"apple": 5, "plum": 3})) # {'apple': 10, 'plum': 3}Apply
Your turn
The task this lesson builds to.
Warm-up (one file): implement restock(stock, additions). Return a new dict merging
additions into stock, summing quantities for shared items.
restock({"apple": 5}, {"apple": 5, "plum": 3}) is {"apple": 10, "plum": 3}.
3 hints and 3 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Make the fixture-backed, parametrized suite pass: implement restock(stock, additions) in
inventory/store.py to merge additions into a new copy of stock (summing shared
quantities, never mutating the input). Some tests are hidden.
3 hints and 2 automated checks are waiting in the workspace.