We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Testing with pytest and Hypothesis step 16 of 19
Build a Typed Factory Fixture
A factory fixture returns a function that a test calls to make records, each
with its own values. Here you write that function in plain Python, without
pytest. Its type is the MakeUser callback protocol from the starter, so
mypy checks every call to it.
make = user_factory()
original = ["staff"]
first = make(name="Ada", tags=original)
second = make(name="Lin", age=24)
first.tags.append("reviewer")
assert first.id == 1
assert second.id == 2
assert second.tags == []
assert original == ["staff"]
assert user_factory()(name="New").id == 1
What to write
Write user_factory() -> MakeUser. It returns a function that takes only
keyword arguments and returns a User:
name: strhas no default, so every call must pass it.age: intdefaults to30.tags: Sequence[str]defaults to the empty tuple().
Follow these rules:
- The first user from a factory gets the ID
1. Each later call to the same factory adds one. - Each factory counts on its own. A new factory starts at
1again, so IDs are unique within one factory, not across factories. - Keep the name, the age and the tags as given. Keep the tags in order, duplicates included. Do not validate names or ages.
- Give every user a new list of tags, even when the caller passes a list. Changing one user's tags must not change another user's tags or the caller's sequence. Changing the caller's list later must not change a user.
Why a protocol, and why a copy
class MakeUser(Protocol):
def __call__(
self, *, name: str, age: int = 30, tags: Sequence[str] = ()
) -> User: ...
Callable[..., User] would check only the result. This protocol keeps the
keyword names, so mypy rejects nmae="Ada", a call with no name, and a
str passed as age. Submit runs mypy, so the function you return must
accept every call the protocol allows. mypy checks that age and tags
have defaults, but not what the defaults are. The tests check the values, so
use 30 and ().
User is a frozen dataclass, so user.name = "Bo" raises
FrozenInstanceError. Freezing stops you from assigning a new value to a
field, but not from changing the list a field holds. user.tags.append(...)
still works, which is why each user needs its own list.
Edge cases
- An age of
0is fine, because you do not validate ages. - A string is a sequence of strings, so a direct call with
tags="ab"gives the tags["a", "b"]. The driver always passes lists.
What the driver reports
solve makes one factory and runs calls in order. It returns one
dictionary per call, with the keys id, name, age and tags. A call
that leaves out age or tags gets your defaults. An empty calls list
gives an empty result.
After each call, the driver copies the user's tags into that call's row. If
mutate is true, it then appends "MUTATED" to the user's own list before
the next call. The driver copied the row first, so that row never shows the
marker. If two users share one list, a later row does.
The driver cannot catch every sharing bug, so test the other cases yourself. Make two users from one input list, change that list after the calls, and make two separate factories.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Python reference solution
Sign in to track your attempts toward unlocking the reference solution.