EUROPYTHON 2026 · KRAKÓW
POSTER SESSION · EXHIBIT HALL
WED 15 JULY · 12:50–13:50
TRACK: TESTING & QUALITY

Django TDD Patterns

A Visual Field Guide
Kuldeep Pisda — Django & growth consultant · creator of django-rls · DjangoCon US tutorials 2022–24

01The Factory Pattern Taxonomy

Choosing the right factory_boy strategy for your model graph — without a test-data explosion.

Does this test need rows in the database?
NO
build() / build_batch()
In-memory, no INSERT, no pk. The fast default for pure-logic tests.
user = UserFactory.build() assert user.display_name()
YES → create() · how are the objects related?
SubFactory
This model needs a parent — the FK must exist first.
class ArticleFactory(...): author = factory.SubFactory( UserFactory)
RelatedFactory
The parent should spawn children after it is created.
class UserFactory(...): article = factory.RelatedFactory( ArticleFactory, "author")
LazyAttribute
The field is derived from sibling fields, not a relation.
email = factory.LazyAttribute( lambda o: f"{o.username}@corp.dev")
CIRCULAR FKs Reference the factory by string + break one direction with factory.SelfAttribute("..") — never two eager SubFactories.
FAKER RULE Match the provider to the domain — faker.company(), not "test1". Failure messages should read like your data.

02Permission Testing, Without Drowning in Fixtures

One parameterized test × a role–resource matrix replaces thirty hand-written test functions.

ROLE ↓
list
retrieve own
update
delete
anonymous
401
401
401
401
member
200
200
403
403
staff
200
200
200
403
owner
200
200
200
204
LAYER 1 · MODEL
Meta.permissions — test once, in isolation.
LAYER 2 · VIEW
permission_classes — test the matrix here.
LAYER 3 · OBJECT
django-guardian / row-level security.
@pytest.mark.parametrize("role, expected", [
("anonymous", 401), ("member", 403),
("staff", 200), ("owner", 200),
])
def test_invoice_update(role, expected,
client_for, invoice):
resp = client_for(role).patch(
invoice.get_absolute_url(),
{"status": "paid"})
assert resp.status_code == expected
# roles come from factory traits —
# one factory, four personas, zero fixtures

03The Mock Boundary Diagram

Where to cut the request lifecycle: too deep is brittle, too shallow is slow.

Request
test client
View
urls · DRF
Service
your business logic
Adapter / Client
✓ MOCK AT THIS SEAM
Third parties
Stripe · S3 · SMTP
MOCK THE VIEW/SERVICE →brittle — you assert your own implementation, and every refactor breaks green tests.
MOCK NOTHING →slow & flaky — real network, real rate limits, CI red on someone else's outage.
AT THE ADAPTER →responses / unittest.mock on the client you wrote around code you didn't.
“Mock at the boundary where your code meets code you don’t own.”

04The Rogues’ Gallery — Five Anti-Patterns

Seen in production codebases. Each has a one-line escape route.

№ 1
The Leaky Test
BEFORE
class PayTests(TestCase): # on_commit never fires
AFTER
class PayTests( TransactionTestCase):
Code under test uses transactions? TestCase wraps everything in one — hooks silently skip.
№ 2
Over-Mocking
BEFORE
mock_calc.assert_called_ once_with(order)
AFTER
assert order.total == Decimal("21.60")
Assert behavior, not calls. Call-count tests pin the implementation, not the contract.
№ 3
Fixture Graveyard
BEFORE
fixtures = ["users.json", "orgs.json", "plans.json"]
AFTER
org = OrgFactory( plan="pro")
JSON fixtures rot with every migration. Factories declare only what the test needs.
№ 4
Order-Dependent Tests
BEFORE
_cache = {} # module state # test_b needs test_a
AFTER
@pytest.fixture def cache(): return {}
State lives in fixtures, never modules. Enforce with pytest-randomly in CI.
№ 5
The God Factory
BEFORE
UserFactory() # creates # org, plan, team, invoice…
AFTER
class Params: with_org = factory.Trait(…)
Opt in to related data with traits. Default factory = one row, not half the schema.