In Django tests, how does seeding data with TestCase.fixtures compare with building it through model factories, and which do teams prefer?
answer
- loaddata behind the attribute
- raw saves skip your save()
- JSON drifts from the models
- data defined where it is used
basics
~20 sTestCase.fixtures loads serialized files through loaddata, once per class, with raw saves that skip custom save() logic and drift as models change. Model factories build objects in Python per test, so most teams prefer factories and keep fixtures for small static reference data.
solid answer
~50 sListing file names in a test case's `fixtures` attribute makes Django run `loaddata` for them: `TestCase` loads them once per class inside its class-level transaction, `TransactionTestCase` before every test. The objects are saved raw, so a model's overridden `save()` is bypassed and signal handlers receive `raw=True`, and the files hard-code primary keys and every field value. As models evolve, the JSON goes stale, and a reader cannot see what a test depends on without opening a file. **Model factories**, functions or a factory library that call `Model.objects.create()` with sensible defaults and let the test override just the fields it cares about, keep data next to the assertion, run real model code and survive schema changes. Most teams therefore use factories, usually inside `setUpTestData`, and keep fixtures for small, stable reference data such as countries or plan tiers.
code
python · 14 linesfrom django.test import TestCase
from billing.tests.factories import make_customer, make_invoice
class OverdueTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.customer = make_customer()
make_invoice(cls.customer, amount_cents=5_000, is_overdue=True)
make_invoice(cls.customer, amount_cents=2_000)
def test_overdue_total(self):
self.assertEqual(self.customer.overdue_total_cents(), 5_000)go deeper
Know that a TestCase's fixtures attribute loads named data files and that factories create objects in code.
Explain when fixtures load (per class for TestCase, per test for TransactionTestCase), the raw-save behaviour, and why factories resist schema drift.
Choose a data strategy for a large suite: factories in setUpTestData, fixtures only for reference data, and awareness of the per-test reload cost.
Set conventions that keep test data maintainable across teams: shared factory modules, rules for reference fixtures, and review expectations.
## Two ways to put rows in the test database A fresh test database contains only what migrations created. Tests then need data, and Django codebases typically use one of two approaches: **fixtures** declared on the test case, or **factories** called from test code. ## `TestCase.fixtures` ```python from django.test import TestCase class InvoiceTotalsTests(TestCase): fixtures = ["customers.json", "invoices.json"] def test_overdue_total(self): ... ``` The mechanics: 1. For each database the class uses, Django calls **`loaddata`** with the listed names, found in each app's `fixtures/` directory or `FIXTURE_DIRS`. 2. **`TestCase`** loads them **once per class** in `setUpClass`, inside the class-wide transaction, so every test sees them and each test's changes are rolled back. 3. **`TransactionTestCase`** loads them **before every test**, because it empties tables after each one. Large fixtures on many transactional tests are a classic slowdown. 4. Objects are deserialized and saved **raw**: `Model.save_base(raw=True)` is used, so an overridden `save()` never runs, and `pre_save`/`post_save` handlers are sent `raw=True`. ## Model factories A factory is a callable that builds a valid object with defaults and accepts overrides: ```python from itertools import count from billing.models import Customer, Invoice _seq = count(1) def make_customer(**overrides): n = next(_seq) fields = {"name": f"Customer {n}", "email": f"c{n}@example.com"} fields.update(overrides) return Customer.objects.create(**fields) def make_invoice(customer=None, **overrides): fields = {"customer": customer or make_customer(), "amount_cents": 10_000} fields.update(overrides) return Invoice.objects.create(**fields) ``` Third-party libraries such as factory_boy generalise this pattern; the idea is the same whether you hand-roll it or use one. ## Comparison | Concern | `fixtures` (JSON/YAML/XML) | Factories | |---|---|---| | Where the data is visible | a separate file | in the test, as overrides | | Custom `save()` and defaults | bypassed by raw saves | executed | | After a model change | files edited by hand or regenerated | usually only the factory changes | | Primary keys | hard-coded in the file | assigned by the database | | Load cost on `TestCase` | once per class | per call, unless in `setUpTestData` | | Good fit | static reference data | almost everything else | ## Why teams drift toward factories - **Readability.** `make_invoice(amount_cents=0)` states the one fact the test relies on; a 400-line JSON file hides it. - **Schema drift.** A new non-nullable field breaks every fixture file that lacks it; a factory needs one default. - **Real behaviour.** Slugs filled in `save()`, denormalised totals and other model logic run, so the test data looks like production data. - **Speed control.** Creating objects in `setUpTestData` runs once per class inside the class transaction, which is as cheap as a fixture and far more precise. ## Where fixtures still earn their place - Small, stable **reference tables** (currencies, plan tiers) that many tests share and that rarely change. - **Reproducing a reported bug** from a dump of a few real rows. - Cases where the raw-save behaviour is exactly what you want, for example loading data without triggering side effects. If fixtures are used, keep them small, prefer natural keys over hard-coded primary keys where models support them, and regenerate them from a known state with `dumpdata` rather than hand-editing. ## Fixtures and the other test case classes - **`SimpleTestCase`** cannot use fixtures at all: it declares no databases, and database queries are refused. - **Multiple databases:** fixtures are loaded into every alias the class declares in `databases`, unless the fixture names a specific database. - **Transactions unavailable:** on a backend without transaction or savepoint support, `TestCase` falls back to loading fixtures per test, like `TransactionTestCase`. ## A migration path for an old suite Teams inheriting a fixture-heavy suite rarely rewrite it at once. A practical order is: 1. Write factories for the most-used models first. 2. Use them in every new test. 3. Convert old tests when they are touched for other reasons. 4. Shrink the shared fixtures to reference data only, and delete files that no test loads any more. The payoff shows up the next time a model gains a required field: one factory default changes instead of dozens of JSON files, and failing tests point at real behaviour rather than at stale data.
- Why can a fixture-loaded object differ from one created with Model.objects.create() for the same field values?Fixtures are saved raw, so an overridden `save()` that fills a slug, normalises an email or computes a total never runs, and signal receivers get `raw=True` and often skip their work. `objects.create()` goes through `save()` and full signal handling, so derived fields are populated as in production.
- Why do large fixtures hurt TransactionTestCase much more than TestCase?`TestCase` loads its fixtures once per class inside a class-level transaction and rolls each test back to that point. `TransactionTestCase` empties the tables after every test, so it has to run `loaddata` again before each one; a big fixture multiplied by hundreds of tests dominates the run.
saying these in an interview costs you the question
- Believing fixtures are loaded once per run and shared by every test class
- Thinking loaddata calls the model's overridden save() method
- Saying factories always make tests slower than fixtures
- Claiming fixture files update automatically when models change
- Hard-coding primary keys in fixtures and asserting on them across apps