08 factories: making several
A plain fixture gives you one thing. Sometimes a test
needs three, or needs one with particular settings. So the fixture returns a
function instead of a value. That is a factory fixture.
@pytest.fixture
def make_user():
def _make(name, admin=False):
return {"name": name, "admin": admin}
return _make # the FUNCTION, not a user
def test_two_users(make_user):
boss = make_user("ada", admin=True)
other = make_user("sam")
assert boss["admin"] and not other["admin"]
Reach for a factory the moment you would otherwise write
admin_user, expired_user,
user_with_no_email as separate fixtures. Those pile up fast, and
a reader has to open another file to find out what each one is.
A factory can also keep a list of what it made and clean up after the
test, using yield the same way the fixture above does.
09 fixtures you get for free
| tmp_path | an empty folder, new for every test, cleaned up for you |
| capsys | captures anything printed, so you can assert on it |
| monkeypatch | change something temporarily and have it put back |
| caplog | captures log messages |
def test_writes_a_file(tmp_path):
out = tmp_path / "report.txt"
save_report(out)
assert out.read_text().startswith("REPORT")
Never write to a fixed path like /tmp/out.txt
in a test. Two tests running at once will fight over it.
10 parametrize: same test, many inputs
@pytest.mark.parametrize("number,word", [
(0, "zero"),
(1, "one"),
(2, "many"),
])
def test_naming(number, word):
assert name(number) == word
$ pytest -v
test_naming[0-zero] PASSED
test_naming[1-one] PASSED
test_naming[2-many] FAILED
Three separate tests, so one failing does not hide
the others. A for loop inside a single test stops at the first
problem and reports one result.
11 glossary
| assertion | a line that says what must be true. assert x == y |
| test | a function whose name starts with test |
| fixture | named setup a test asks for by argument name |
| factory | a fixture returning a function, so a test can make several |
| teardown | cleanup after a test, written after yield |
| conftest.py | shared fixtures for a folder, no import needed |
| parametrize | run one test many times with different inputs |
| pytest.raises | assert that something DOES fail: with pytest.raises(ValueError): |
| skip | this test does not apply here, do not run it |
| flaky | passes and fails without the code changing. always a bug |
| coverage | which lines ran. NOT whether they were checked |
12 glossary: fakes and patching
Words for "a stand-in used instead of the real thing".
Collectively they are test doubles.
| stub | always gives the same canned answer |
| fake | a real but simplified version, e.g. a dict instead of a database |
| mock | a spy that also checks it was called correctly |
| monkeypatch | temporarily replace something, put back automatically |
| injection | passing the collaborator in, instead of the code fetching it |
Prefer a fake. A stub that always returns success
cannot tell you what happens on failure, because it was never pretending to
be anything.
If a test is painful to write, that is usually the code talking. Code
that fetches its own database or network connection is hard to stand in for;
code that is handed one is easy.
13 habits worth starting with
- Name the behaviour, not the function.
test_rejects_empty_name beats test_create_user_2.
- One idea per test. Four unrelated asserts report the first
failure and hide the rest.
- Write the failing test first when fixing a bug. If it does not
fail before your fix, it is not testing the fix.
- Check the unhappy path. Empty input, missing file, wrong type.
That is where the bugs are.
14 when something looks wrong
| test not running | check the name, then --collect-only |
| fixture not found | spelling, or it is not in conftest.py |
| passes alone, fails together | something is shared between tests |
| passes here, fails on CI | a path, a clock, or an installed package |
| no output shown | pytest hides prints on pass. use -s |
Stuck on a failure? pytest --lf -x --tb=short
gets you the shortest useful loop: last failure only, stop at it, short
traceback.