Testing in Depth: Mocking and Fixtures
Fixtures, parametrization, mocking and coverage: how to test code that touches the real world
assert statements over pure functions. Real systems are not pure: they call networks, read files, hit databases, and read the clock. To test that code you must isolate it from the world with test doubles (mocks), build reusable test state with fixtures, run one test over many inputs with parametrization, and then measure what your tests actually exercise with coverage. These four skills turn "I wrote a test" into "I can test anything".
- write
@pytest.fixturefunctions and inject them by parameter name, - choose the right fixture scope and clean up with
yield, - run one test across many cases with
@pytest.mark.parametrize, - replace real dependencies with
unittest.mockandpatch(), applying the "patch where it is looked up" rule, - configure return values, side effects and assertions on a mock,
- measure and read coverage, and know why 100% is not "bug-free".
1. Fixtures: setup as dependency injection
A fixture is a function decorated with @pytest.fixture that produces some object a test needs: a temporary database, a configured client, a sample record. A test requests a fixture simply by naming it as a parameter. pytest sees the name, runs the fixture, and passes its return value in. This is dependency injection: the test declares what it needs, pytest supplies it.
import pytest
@pytest.fixture
def account():
return {"balance": 100, "owner": "ada"}
def test_withdraw(account): # pytest injects the fixture by name
account["balance"] -= 30
assert account["balance"] == 70
Note what you did not write: no setup call at the top of the test, no global state. Each test that names account gets its own freshly built dict, so tests never leak state into each other. Fixtures can also request other fixtures, letting you compose small pieces into a full environment.