We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Tests That Earn Their Keep step 15 of 19
A typed factory fixture
A test suite excluded from type checking lets you rename a parameter, change a return type, or delete a field, and still see green — because the tests were never checked against the signatures they call. The suite becomes a second, unversioned copy of your API that drifts quietly out of date.
So type the tests. The single hardest thing to type in a real suite is the factory fixture, and this is it.
What to write
def user_factory() -> MakeUser
It returns a callable that builds User records:
-
keyword-only,
namerequired,agedefaulting to30,tagsdefaulting to empty, -
idstarting at1and increasing by one per call, per factory — the monotonic ids a real suite needs so two users are never accidentally equal, -
and each
Usergets its owntagslist.
solve is provided; it builds one factory, replays a list of calls, and —
when mutate is set — appends "MUTATED" to each returned tags list
before making the next call.
That last part is the assertion that matters. A factory holding one default
list and handing the same object to every caller passes every test anyone
writes until the day one test mutates what it was given, and then a
completely different test starts failing. Build the list fresh each time, or
copy what the caller passed. frozen=True on the dataclass does not save you
— it freezes the binding, not the list behind it.
Why Callable[..., User] is not good enough
The obvious annotation is Callable[..., User]. It type-checks
make(name="x"), and it equally type-checks make(nmae="x", age="old", colour=7), because ... means “any parameters at all”. You have written
down the return type and thrown away everything a caller could get wrong.
Callable has no syntax for keyword arguments or defaults. A callback
Protocol does:
class MakeUser(Protocol):
def __call__(
self, *, name: str, age: int = 30, tags: Sequence[str] = ()
) -> User: ...
Now make(nmae="x") is an error at the call site, in the test file, before
anything runs. This is the standard shape for any fixture that returns a
factory, and it is why --disallow-any-generics is in --strict: a bare
Callable is exactly the hole it is there to close.
The rest of the typed-pytest vocabulary
Same discipline, in the fixtures you already use — tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture,
request: pytest.FixtureRequest, and capsys: pytest.CaptureFixture[str],
where the parameter is mandatory because --disallow-any-generics rejects a
bare CaptureFixture. A yield fixture is annotated Iterator[T], never T
— the most common --strict error in a test suite that has just been turned
on. And with pytest.raises(MyError) as exc_info: gives you
ExceptionInfo[MyError], so exc_info.value.code is checked rather than
guessed.
What you must not do is add a per-module mypy override to make the tests green. It removes the benefit permanently, and nobody ever takes it back out.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.