Community Python Snippet
The pytest Fixture Builder I Cannot Live Without
A four-stage tour of the fixture-builder pattern: a callable factory that mints test models with sensible defaults, layered overrides, deterministic IDs, and per-test isolation. The shape I paste into every conftest.py.
The pytest Fixture Builder I Cannot Live Without
A four-stage tour of the fixture-builder pattern: a callable factory that mints test models with sensible defaults, layered overrides, deterministic IDs, and per-test isolation. The shape I paste into every conftest.py.
By @yunatorres
April 30, 2026
·
Updated May 18, 2026
687 views
2
4.3 (14)
The smallest version of the pattern is a function that returns a fresh dict every call, with sensible defaults that only matter when the test does not care about them. The id counter is module-level so two calls in the same test produce different ids without anyone having to remember to pass them. I have seen plenty of test suites use class-based factories from factory_boy for the same job; for unit tests I prefer the plain function because there is no metaclass, no Meta, and no debugger surprise. When the override is just name='Ada', you read the test and know exactly what is being exercised.
Rebinding the counter and the created list to fixture-local closures is the move that gives per-test isolation without any teardown plumbing. In a real pytest setup you write @pytest.fixture over user_factory_fixture and yield make_user; the fixture is recreated for every test that depends on it because the default scope is function. The make.created list attached to the function is a tiny but useful trick: assertion code can do assert len(make.created) == 3 without inventing a tracking variable. Stage-one's module-level counter would have leaked test ids across tests, which was the bug I wrote stage two to fix.
Real test data has shape: a user belongs to an org, an order belongs to a user, a payment belongs to an order. Naming each factory and letting them call each other gives you the equivalent of factory_boy's SubFactory, but explicit. The make_admin shortcut is the part I always end up wanting: it is just make_user(role='admin', ...) but using it in a test reads as the_admin = factories['admin'](), which signals intent. Auto-assigning a fresh org when the caller does not pass one keeps trivial tests trivial; passing org=org keeps related-data tests honest about which records share a parent.
Snapshot tests blow up the moment a generated id rolls forward. The fix is a freeze=True flag that pins the id and timestamp generators to constants, which lets snapshots compare equal across runs. I keep both modes available rather than always-frozen because non-snapshot tests benefit from unique ids: when an assertion looks at len({u['id'] for u in users}) you want it to fail loudly if your code accidentally reuses an id. The full builder, four stages later, is what I paste into a conftest.py and never think about again until a junior asks why our tests do not flake on dates.
