RizTech Academy logo
RizTech Academy
Testing and QualityLesson 4 of 425 min

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, so AppointmentFactory() 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

  1. Write a PatientFactory with Sequence for name and phone; create one with PatientFactory() and confirm it is valid. Override the city with PatientFactory(city="Mumbai").
  2. Rewrite a test's hand-built setup to use the factory and note how much shorter and clearer it becomes.
  3. 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.
  4. Add a SubFactory so AppointmentFactory() builds its own patient and doctor; create an appointment in one line.
  5. Use create_batch(20) to test a paginated list view.
  6. Compare a JSON fixture and a factory for the same data; articulate why the factory is easier to maintain.

Official documentation

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