Fixtures and factories
Every test needs data to work with — a patient to list, an appointment to check. How you create that data
matters more than it seems: done badly, your tests become a wall of repetitive object-creation that breaks
whenever a model changes; done well, test data is a single readable line. This lesson compares the two
approaches — fixtures and factories — and makes the case for factories (with factory_boy) as the better
default.
The problem: repetitive, fragile setup
Without a strategy, every test creates objects by hand:
patient = Patient.objects.create(name="Asha", phone="09812345678", city="Pune")
doctor = Doctor.objects.create(name="Rao", specialisation="Cardiology")
appointment = Appointment.objects.create(patient=patient, doctor=doctor,
scheduled_for=timezone.now(), fee=Decimal("500"))
Two problems grow with the suite. First, it is repetitive — the same object creation copied across
dozens of tests. Second, it is fragile: add a required field to Patient and every test that creates
one breaks, because each must now supply the new field. When most of a test is boilerplate setup, the actual
thing being tested is buried, and a model change means a mass edit.
Approach 1: fixtures (JSON/YAML data files)
Django's built-in answer is fixtures — serialised data files you load into the test database:
// patients/fixtures/sample_patients.json
[
{"model": "patients.patient", "pk": 1,
"fields": {"name": "Asha", "phone": "09812345678", "city": "Pune"}}
]
class PatientTests(TestCase):
fixtures = ["sample_patients.json"] # loaded before each test
Fixtures work, but they age poorly. The file is separate from the test, so you cannot see what data a test runs against without opening another file; they are verbose and hard to maintain (hand-editing JSON with primary keys and foreign keys); and, worst, they are brittle — add a required field and every fixture file must be updated by hand. Fixtures have their place (a fixed set of reference data, like a list of test types), but as the general way to set up test data they scale badly. Most experienced Django teams have moved away from them.
Approach 2: factories (factory_boy) — the better default
A factory is a Python object that knows how to build a model instance with sensible defaults, so a test
asks for "a patient" and gets a valid one without spelling out every field. The standard library is
factory_boy:
import factory
from .models import Patient, Doctor
class PatientFactory(factory.django.DjangoModelFactory):
class Meta:
model = Patient
name = factory.Sequence(lambda n: f"Patient {n}") # "Patient 0", "Patient 1", ...
phone = factory.Sequence(lambda n: f"098100000{n:02d}")
city = "Pune"
class DoctorFactory(factory.django.DjangoModelFactory):
class Meta:
model = Doctor
name = factory.Sequence(lambda n: f"Doc{n}")
specialisation = "General"
Verified: these factories build valid objects, and the model tests using them pass. Now test setup is one line, and you override only what the test cares about:
PatientFactory() # a valid patient with default everything
PatientFactory(name="Asha") # a patient named Asha, rest defaulted
PatientFactory(city="Mumbai") # override just the city
PatientFactory.create_batch(5) # five patients at once
This is the key advantage: a test states only the data relevant to it, and the factory fills in the
rest. test_booked_returns_only_booked does not care about patients' names or phones — it just needs
appointments in certain statuses, so it uses PatientFactory() and focuses on the status. The test reads as
its intent, not its setup.
Why factories win: the field-added test
The decisive advantage is maintenance. Add a new required field to Patient — say date_of_birth becomes
required. With hand-created objects or fixtures, every test and every fixture that makes a patient
breaks and must be edited. With a factory, you add one line to PatientFactory
(date_of_birth = factory.Faker("date_of_birth")) and every test keeps working, because they all get their
patients from the factory. The factory is the single place that knows how to build a valid object, so a
model change is a one-line factory update, not a suite-wide edit. This is the same "define it once" principle
as custom managers, applied to test data.
Factory features you will use
factory_boy handles the awkward parts of realistic test data:
factory.Sequence— a unique value per instance (Patient 0,Patient 1), so unique fields do not collide across a batch.factory.Faker("name")/Faker("email")— realistic fake data (names, addresses, phone numbers) instead of"test test".factory.SubFactory(DoctorFactory)— automatically create a related object, soAppointmentFactory()builds its own patient and doctor without you wiring them up.create_batch(n)— make many at once for list/pagination tests.
class AppointmentFactory(factory.django.DjangoModelFactory):
class Meta:
model = Appointment
patient = factory.SubFactory(PatientFactory) # builds a patient automatically
doctor = factory.SubFactory(DoctorFactory)
scheduled_for = factory.LazyFunction(timezone.now)
Now AppointmentFactory() produces a fully-valid appointment — patient, doctor, time and all — in one call.
SubFactory handling relationships is what makes factories genuinely powerful for a connected data model
like a clinic's.
Check your work
The problem. Hand-creating objects in every test is repetitive and fragile — a new required field breaks every test that builds that model.
Fixtures. JSON/YAML data files loaded via fixtures = [...]; separate from the test, verbose, and
brittle (a model change means hand-editing every file). Fine for fixed reference data, poor as the general
approach.
Factories (factory_boy). A factory builds a valid instance with defaults; a test overrides only what
it cares about (PatientFactory(name="Asha")). Verified: factories build valid objects and the tests pass.
Why factories win. The factory is the single place that knows how to build a valid object — a new required field is a one-line factory change, not a suite-wide edit.
Key features. Sequence (unique values), Faker (realistic data), SubFactory (auto-create related
objects), create_batch(n).
Practice
- Write a
PatientFactorywithSequencefor name and phone; create one withPatientFactory()and confirm it is valid. Override the city withPatientFactory(city="Mumbai"). - Rewrite a test's hand-built setup to use the factory and note how much shorter and clearer it becomes.
- Add a new required field to the model; observe hand-built setups breaking, then add one line to the factory and confirm the factory-based tests still pass.
- Add a
SubFactorysoAppointmentFactory()builds its own patient and doctor; create an appointment in one line. - Use
create_batch(20)to test a paginated list view. - Compare a JSON fixture and a factory for the same data; articulate why the factory is easier to maintain.
Official documentation
- factory_boy documentation — Factories,
Sequence,SubFactory,Faker. - Django — Fixtures — The built-in fixtures approach, for comparison.
- factory_boy — Using with Django —
DjangoModelFactory.
Next: fat models, thin views — where business logic belongs.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship