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 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:
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 for the details and setUpTestData deep copies 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"). Usebaker.prepare()for unsaved instances andbaker.make(Order, _quantity=50, _bulk_create=True)for many rows. - factory_boy gives you explicit, reusable defaults with
DjangoModelFactory,SubFactoryand traits. Usebuild()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.
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:
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 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:
# 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, Celery eager mode, and debug toolbar, DEBUG and static storage. 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:
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:
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 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:
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": "[email protected]"})
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).
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:
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. Never use time.sleep() to wait for time to pass (tests are sleeping).
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 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-dbwith pytest-django,manage.py test --keepdbwith Django's runner. CI should still build it from scratch to catch broken migrations. See stop re-running migrations. - Run on every core:
pytest -n autowith pytest-xdist, ormanage.py test --parallel auto. Each worker gets its own test database. See run tests in parallel with pytest-xdist, and 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 for details.
Further reading
- Django: testing tools
- Django: speeding up the tests
- pytest-django: database access
- model_bakery documentation
- time-machine
Related guides
- Use TestCase instead of TransactionTestCase: TransactionTestCase and transaction=True empty every table after each test. Django's TestCase rolls back a transaction instead. When to use which, and how to switch.
- Create shared test data in setUpTestData instead of setUp: Django runs setUp before every test, so its rows are inserted again each time. setUpTestData creates them once per TestCase class, and every test still starts clean.
- Django test settings for speed (debug toolbar, DEBUG, static storage): Development settings slow Django tests: keep django-debug-toolbar out, don't force DEBUG = True, and use StaticFilesStorage instead of manifest or WhiteNoise storage.
- Use a fast password hasher in tests: Django hashes passwords with hundreds of thousands of PBKDF2 iterations, and every create_user and client.login in a test pays for it. Switch to a fast hasher in test settings and use force_login.
Updated October 18, 2026 · Suggest a change