# Django testing best practices

> How to write fast, reliable Django tests: TestCase vs TransactionTestCase, setUpTestData, model_bakery, force_login, test settings, assertNumQueries and parallel runs.

A good Django test suite is fast enough that you run it all the time and strict enough that a green run means the code works. Most Django suites miss one of the two for the same handful of reasons: the wrong test case class, data rebuilt for every test, slow production settings, and views tested with empty pages.

This page is the checklist. Each item links to a guide that goes deeper where speed is the main concern.

## Pick the right test case class

Django has three test case classes, and the choice decides how the database is reset between tests.

| Class | Database | Reset between tests | Use it for |
|---|---|---|---|
| `SimpleTestCase` | Queries are refused | Nothing to reset | Pure functions, forms without models, template tags, URL resolving |
| `TestCase` | Allowed | Transaction rolled back | Almost everything |
| `TransactionTestCase` | Allowed | Every table truncated | Code that needs real commits, several connections, or a live server |

`TestCase` wraps the class in a transaction and every test in a savepoint, then rolls back: resetting costs almost nothing. `TransactionTestCase` lets your code commit and then flushes every table after each test, which gets slower with every model you add. pytest-django has the same split: `@pytest.mark.django_db` behaves like `TestCase`, `@pytest.mark.django_db(transaction=True)` and the `live_server` fixture behave like `TransactionTestCase`.

Default to `TestCase` and justify every exception. [Use TestCase instead of TransactionTestCase](https://flakytest.dev/guides/testcase-vs-transactiontestcase/) covers the migration, including the common reason people reach for `TransactionTestCase` in the first place: `transaction.on_commit` callbacks (see below).

## Build data once with setUpTestData

`setUp` runs before every test. `setUpTestData` runs once per class, inside the class-level transaction, and every test still starts from the same rows because each test rolls back to a savepoint:

```python
from django.test import TestCase
from model_bakery import baker

from shop.models import Order, Product


class OrderTotalTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.customer = baker.make("auth.User")
        cls.product = baker.make(Product, price=10)
        cls.order = baker.make(Order, customer=cls.customer)

    def test_empty_order_total(self):
        self.assertEqual(self.order.total(), 0)

    def test_total_with_lines(self):
        self.order.lines.create(product=self.product, quantity=3)
        self.assertEqual(self.order.total(), 30)
```

Since Django 3.2, attributes assigned in `setUpTestData` are deep-copied for each test, so a test that changes `self.order` in memory doesn't affect the next one. See [setUpTestData instead of setUp](https://flakytest.dev/guides/setuptestdata-vs-setup/) for the details and [setUpTestData deep copies](https://flakytest.dev/guides/setuptestdata-deepcopy/) for when that copy gets expensive.

## Create data with factories

JSON fixture files rot: every new required field breaks them, and nobody can tell which test needs which row. Create data in the test with a factory instead, and give it only the fields the test is about.

- **model_bakery** fills required fields automatically: `baker.make(Order, status="paid")`. Use `baker.prepare()` for unsaved instances and `baker.make(Order, _quantity=50, _bulk_create=True)` for many rows.
- **factory_boy** gives you explicit, reusable defaults with `DjangoModelFactory`, `SubFactory` and traits. Use `build()` when the test doesn't need the row in the database.

Never hardcode primary keys (`Order.objects.get(pk=1)`, `"/orders/1/"`). Sequences aren't reset between tests and differ between databases and parallel workers, so use `order.pk` and `reverse()`. If factories show up in your profile, read [faster test factories](https://flakytest.dev/guides/faster-test-factories/).

## Log in with force_login

`client.login()` runs the full authentication flow, including password hashing. Unless the test is about logging in, attach the user directly:

```python
def setUp(self):
    self.client.force_login(self.customer)
```

Pair it with a fast password hasher in test settings. [Use a fast password hasher in tests](https://flakytest.dev/guides/fast-password-hasher-in-tests/) explains why this is often the biggest single speed-up in a Django suite.

## Use test settings

Keep a settings module for tests (`DJANGO_SETTINGS_MODULE=myproject.settings.test`, or `--ds` / `DJANGO_SETTINGS_MODULE` in your pytest config) that changes what production needs and tests don't:

```python
# myproject/settings/test.py
from .base import *  # noqa: F403

PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]

CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}

STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.InMemoryStorage"},
    "staticfiles": {"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"},
}

# With app.config_from_object("django.conf:settings", namespace="CELERY")
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
```

You don't need to configure email. Django's test runner and pytest-django both switch `EMAIL_BACKEND` to the locmem backend, so sent messages land in `django.core.mail.outbox` (or pytest-django's `mailoutbox` fixture) and the outbox is emptied before each test. They also force `DEBUG = False`.

Each of these has its own guide: [in-memory cache](https://flakytest.dev/guides/in-memory-cache-in-tests/), [Celery eager mode](https://flakytest.dev/guides/celery-eager-in-tests/), and [debug toolbar, DEBUG and static storage](https://flakytest.dev/guides/django-test-settings/). Clear the locmem cache between tests (`cache.clear()` in `setUp` or an autouse fixture) so cached values don't leak from one test into another.

## Test views with real data

A view test against an empty database proves very little. The template's `{% for %}` loop never runs, so a broken `{% url %}` tag, a missing attribute or an N+1 query inside the loop only shows up in production. Create the rows the page lists, then assert on what the user would see:

```python
from django.test import TestCase
from django.urls import reverse
from model_bakery import baker

from shop.models import Order


class OrderListViewTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.customer = baker.make("auth.User")
        cls.orders = baker.make(Order, customer=cls.customer, _quantity=3)

    def setUp(self):
        self.client.force_login(self.customer)

    def test_lists_the_customers_orders(self):
        response = self.client.get(reverse("order-list"))

        self.assertEqual(response.status_code, 200)
        for order in self.orders:
            self.assertContains(response, reverse("order-detail", args=[order.pk]))
```

Prefer the test client over calling view functions with a `RequestFactory` for these tests: the client runs URL routing, middleware and template rendering, which is where the bugs hide.

## Lock in query counts with assertNumQueries

Once a view has real data, pin how many queries it makes. Create more rows than the page needs and assert the count, so a missing `select_related()` fails the test instead of slowing down production:

```python
def test_order_list_query_count(self):
    baker.make(Order, customer=self.customer, _quantity=20)

    with self.assertNumQueries(4):
        self.client.get(reverse("order-list"))
```

The number is whatever the view needs today (session, user, orders, a prefetch); what matters is that it stays the same with 3 rows or 20. In pytest, use pytest-django's `django_assert_num_queries` fixture. [Find and fix N+1 queries](https://flakytest.dev/guides/n-plus-one-queries/) shows how to find the offenders.

## Run on_commit callbacks in TestCase

`TestCase` never commits, so `transaction.on_commit()` callbacks (send the email, enqueue the task) never fire. That's not a reason to switch to `TransactionTestCase`. Capture and run them:

```python
from django.core import mail


def test_signup_sends_welcome_email(self):
    with self.captureOnCommitCallbacks(execute=True) as callbacks:
        self.client.post(reverse("signup"), {"email": "new@example.com"})

    self.assertEqual(len(callbacks), 1)
    self.assertEqual(len(mail.outbox), 1)
```

pytest-django has the same as the `django_capture_on_commit_callbacks` fixture.

## Override settings per test, not globally

Change a setting for one test or class with `@override_settings(...)`, or `with self.settings(...):` inside a test. Both restore the original value afterwards and send the `setting_changed` signal so Django's caches of settings are reset. pytest-django's `settings` fixture does the same. Never assign to `django.conf.settings` directly in a test: the change outlives the test and makes the next one depend on the order tests run in (see [test isolation](https://flakytest.dev/guides/test-isolation/)).

## Control time with time-machine

Code that depends on "now" (expiry dates, trial periods, reports for "this month") needs a fixed clock. time-machine works as a class decorator, a method decorator or a context manager:

```python
import datetime as dt

import time_machine
from django.test import TestCase


@time_machine.travel(dt.datetime(2026, 3, 1, 9, 0, tzinfo=dt.UTC), tick=False)
class TrialExpiryTests(TestCase):
    def test_trial_expires_after_14_days(self): ...
```

It's much cheaper to start than freezegun: see [replace freezegun with time-machine](https://flakytest.dev/guides/freezegun-to-time-machine/). Never use `time.sleep()` to wait for time to pass ([tests are sleeping](https://flakytest.dev/guides/sleep-in-tests/)).

## Never touch the network

A test that calls a real API is slow and fails whenever that API, DNS or the CI network has a bad minute. Mock HTTP at the transport (responses, respx, pytest-httpx) and block everything else so a forgotten mock fails loudly. [Mock and block real network calls](https://flakytest.dev/guides/block-network-in-tests/) has the setup, including an autouse fixture that rejects any host but localhost.

## Reuse the database and run in parallel

Two flags change how long you wait more than anything in the test code:

- Keep the test database between runs: `pytest --reuse-db` with pytest-django, `manage.py test --keepdb` with Django's runner. CI should still build it from scratch to catch broken migrations. See [stop re-running migrations](https://flakytest.dev/guides/reuse-test-database/).
- Run on every core: `pytest -n auto` with pytest-xdist, or `manage.py test --parallel auto`. Each worker gets its own test database. See [run tests in parallel with pytest-xdist](https://flakytest.dev/guides/pytest-xdist/), and [run migrations once](https://flakytest.dev/guides/run-migrations-once/) when every worker migrating its own database adds up.

Parallel runs are also a free isolation check: a test that only passes after another test has run will start failing.

## Measure, then fix

Guessing which of these matters for your suite wastes time. `flakytest run` profiles the suite and ranks the fixes by the seconds per run they'd save: password hashing, `setUp` work that belongs in `setUpTestData`, `TransactionTestCase` flushes, migrations, N+1 queries. After a change, `flakytest compare` shows what got faster and flags tests whose outcome changed or that stopped running. See the [docs](https://flakytest.dev/docs/) for details.

## Further reading

- [Django: testing tools](https://docs.djangoproject.com/en/stable/topics/testing/tools/)
- [Django: speeding up the tests](https://docs.djangoproject.com/en/stable/topics/testing/overview/#speeding-up-the-tests)
- [pytest-django: database access](https://pytest-django.readthedocs.io/en/latest/database.html)
- [model_bakery documentation](https://model-bakery.readthedocs.io/)
- [time-machine](https://github.com/adamchainz/time-machine)
