Filter concepts by levelShowing all levels.

Django · Testing

Django Testing

The three test-case classes, the test client, pytest-django and fixtures, parameterization and factories

  • TestCase never commitsso on_commit callbacks do not fire and the test silently proves nothing
  • SimpleTestCase forbids the databasewhich turns "this should be pure" into an enforced rule
  • Test at the narrowest layer that can faila model method through the test client has five suspects instead of one
  • pytest-django blocks the database by defaultthe marker is both permission and documentation
  • Factories hide what the test is not aboutso a new required field breaks one factory, not every test file

1.SimpleTestCase, TestCase, and TransactionTestCase

The three classes differ in how they handle the database, and choosing wrongly is either slow or subtly broken. `SimpleTestCase` **forbids** database access — touching it raises, which is exactly what you want for a template filter or a pure function. `TestCase` wraps each test in a transaction and rolls it back afterwards, which makes it fast and is the right default. `TransactionTestCase` genuinely commits and then truncates the tables between tests, which is much slower but is the only one that can test anything involving real commits — `transaction.on_commit()` callbacks, `select_for_update()`, or code that expects another connection to see its writes.

Key features

  • `SimpleTestCase` raises on any database query — use it for pure functions, template filters, form validation without a model, and URL resolution
  • `TestCase` wraps each test in a transaction and rolls back: fast, isolated, and the right default for almost everything
  • Because `TestCase` never commits, `transaction.on_commit()` callbacks do not run — use `captureOnCommitCallbacks()` to execute and inspect them
  • `TransactionTestCase` commits for real and truncates tables afterwards; it is much slower and resets sequences, so hard-coded primary keys break
  • Anything needing a real commit — `select_for_update()` against a second connection, database-level triggers, cross-connection visibility — needs `TransactionTestCase`
  • `setUpTestData` runs once per class rather than per test, but the objects are shared: mutate one and re-fetch, or the change leaks into the next test

Which class, and what it costs

Which class, and what it costs
ClassDatabaseIsolation bySpeedUse for
`SimpleTestCase`**forbidden**nothing to isolatefastestpure functions, filters, URL resolution
`TestCase`transaction, rolled backrollbackfastalmost everything
`TransactionTestCase`real commitstruncate between testsslowon_commit, locking, cross-connection
`LiveServerTestCase`real commits + a live servertruncateslowestbrowser/end-to-end tests
The same on_commit test, under each class

TestCase — never commits

  • Each test runs inside a transaction that is rolled back.
  • Fast, and isolated without truncating anything.
  • on_commit callbacks never fire — the assertion silently passes.
  • A second connection cannot see the writes, so locking tests prove nothing.
  • Right for almost every test that is not about committing.

TransactionTestCase — really commits

  • Writes are committed, then tables are truncated between tests.
  • on_commit callbacks fire exactly as they do in production.
  • A second connection sees the data, so select_for_update is testable.
  • Sequences reset, so tests assuming a specific pk break.
  • Noticeably slower — reserve it for tests about commits.
  • TestCase — never commits
    • Each test runs inside a transaction that is rolled back.
    • Fast, and isolated without truncating anything.
    • on_commit callbacks never fire — the assertion silently passes.
    • A second connection cannot see the writes, so locking tests prove nothing.
    • Right for almost every test that is not about committing.
  • TransactionTestCase — really commits
    • Writes are committed, then tables are truncated between tests.
    • on_commit callbacks fire exactly as they do in production.
    • A second connection sees the data, so select_for_update is testable.
    • Sequences reset, so tests assuming a specific pk break.
    • Noticeably slower — reserve it for tests about commits.

Command

python
class MyTests(TestCase):
    def test_side_effect(self):
        with self.captureOnCommitCallbacks(execute=True):
            do_the_thing()

`SimpleTestCase` forbids the database, which turns "this should be pure" into an enforced rule. `TestCase` wraps each test in a rolled-back transaction — fast, isolated, and the right default — but because it never commits, `on_commit` callbacks do not fire and other connections cannot see your writes, so use `captureOnCommitCallbacks(execute=True)` rather than switching classes. `TransactionTestCase` really commits and truncates, and is only worth its cost when the commit itself is what you are testing.

2.The test client, and testing requests, URLs, templates, models and forms

`self.client` is a fake browser: it builds a request, runs it through the full middleware and URL stack, and gives you the response — without a socket, a server, or a real HTTP connection. That means `client.get("/orders/")` exercises routing, middleware, permissions, the view and the template together. The response carries more than a body: `status_code`, `context` (what the template was rendered with), `templates` (which ones were used), and `redirect_chain` when you pass `follow=True`. Around that sit the narrower tests — resolve a URL with `reverse()` rather than hard-coding a path, assert a model method's return value directly, and test a form by instantiating it with data and checking `is_valid()` and `form.errors`.

Key features

  • `self.client` runs the full middleware and URL stack in-process — no socket, no live server, and no network
  • `response.context` is the context the template was rendered with; asserting on it is far more stable than asserting on HTML
  • `response.templates` lists what rendered, and `assertTemplateUsed(response, "…")` is the readable form
  • `follow=True` populates `redirect_chain`; `assertRedirects(response, url)` checks the target *and* that the target itself returns 200
  • Use `reverse("order-detail", args=[pk])` in tests, never a literal path — a route change should break one line, not forty tests
  • `client.force_login(user)` skips the password check and the login view, which is what you want unless the login flow itself is the subject

Test at the narrowest layer that can fail

Test at the narrowest layer that can fail
What you are testingTest it withNot with
A model method or propertya direct callthe test client
Form validation rules`Form(data=...)` + `is_valid()` + `errors`a POST through a view
A URL pattern resolves`reverse()` / `resolve()`a literal path in every test
A view chooses the right data`response.context`assertions on rendered HTML
Which template was used`assertTemplateUsed`searching the body for a string
Anonymous access is redirectedthe test clienta unit test of the decorator
What each kind of test actually exercises

client.get(reverse("order-list"))

the whole stack below, in one call — use it for behaviour that only exists assembled

URL resolution

reverse() and resolve() test this directly, and keep every other test free of literal paths

Middleware and permissions

only reachable through the client — redirects, 403s, and session handling live here

The view

assert on response.context: what the view CHOSE, not how the template drew it

Template rendering

assertTemplateUsed checks the decision; asserting on HTML couples the test to markup

Forms

Form(data=…).is_valid() and form.errors — a direct call, no request needed

Models

a plain function call. Routing this through the client adds four suspects to an arithmetic bug.

  1. client.get(reverse("order-list")) — the whole stack below, in one call — use it for behaviour that only exists assembled
  2. URL resolution — reverse() and resolve() test this directly, and keep every other test free of literal paths
  3. Middleware and permissions — only reachable through the client — redirects, 403s, and session handling live here
  4. The view — assert on response.context: what the view CHOSE, not how the template drew it
  5. Template rendering — assertTemplateUsed checks the decision; asserting on HTML couples the test to markup
  6. Forms — Form(data=…).is_valid() and form.errors — a direct call, no request needed
  7. Models — a plain function call. Routing this through the client adds four suspects to an arithmetic bug.

Command

python
response = self.client.get(reverse("order-detail", args=[order.pk]))
self.assertTemplateUsed(response, "orders/detail.html")
self.assertEqual(response.context["order"], order)

The test client runs the whole middleware and URL stack in-process, so use it for behaviour that only exists once the pieces are assembled — redirects, permissions, which template rendered. Test models and forms with direct calls, where a failure has one suspect. Always `reverse()` rather than literal paths, and assert on `response.context` rather than on HTML, so a template change fails a template test and nothing else.

3.pytest, pytest-django, fixtures, scopes, and database access

pytest replaces `TestCase` subclasses with plain functions and `assert`, and **pytest-django** adds the Django-specific pieces. The most important is that **database access is blocked by default** — a test that queries without asking raises, so touching the database is always a deliberate choice. You ask with `@pytest.mark.django_db` or by requesting the `db` fixture. The plain form wraps each test in a transaction and rolls back, exactly like Django's `TestCase`; `@pytest.mark.django_db(transaction=True)` mirrors `TransactionTestCase`, flushing between tests. Fixtures replace `setUp`: a function decorated with `@pytest.fixture` that other tests request by name, with a `scope` controlling how often it is rebuilt.

Key features

  • pytest-django blocks database access by default — a query without `django_db` or the `db` fixture raises rather than silently working
  • `@pytest.mark.django_db` is a rolled-back transaction per test, matching Django's `TestCase`
  • `@pytest.mark.django_db(transaction=True)` mirrors `TransactionTestCase`: real commits, flushed between tests, and slower
  • The `db` fixture is function-scoped, so a `session`-scoped fixture cannot request it — and anything it created would not be rolled back
  • For session-wide seed data, override `django_db_setup` and load inside `with django_db_blocker.unblock():`
  • Useful built-ins: `client`, `admin_client`, `rf` (a `RequestFactory`), `settings` (auto-restored), and `django_assert_num_queries`

Fixture scopes, and what belongs in each

Fixture scopes, and what belongs in each
ScopeRebuiltMay touch the database?Good for
`function` (default)every testyes — via `db`model instances, anything mutated
`class`once per classno (`db` is function-scoped)shared setup for a group of pure tests
`module`once per filenoa parsed file, a compiled template
`session`once per runonly via `django_db_blocker`a fake HTTP server, an expensive computation
From a blocked database to a test that says what it needs

1 · Blocked by default

A query with no marker and no db fixture raises. Reaching the database is always a visible, deliberate choice.

2 · Ask for it explicitly

The marker wraps the test in a transaction and rolls it back — the same isolation Django's TestCase provides.

3 · Declare dependencies as fixtures

A test names what it needs and pytest builds the graph. Fixtures can request other fixtures, so setup composes instead of repeating.

4 · Escalate only when the commit matters

transaction=True gives real commits and flushes between tests — needed for on_commit and locking, and noticeably slower.

  1. 1 · Blocked by default — A query with no marker and no db fixture raises. Reaching the database is always a visible, deliberate choice.
  2. 2 · Ask for it explicitly — The marker wraps the test in a transaction and rolls it back — the same isolation Django's TestCase provides.
  3. 3 · Declare dependencies as fixtures — A test names what it needs and pytest builds the graph. Fixtures can request other fixtures, so setup composes instead of repeating.
  4. 4 · Escalate only when the commit matters — transaction=True gives real commits and flushes between tests — needed for on_commit and locking, and noticeably slower.

Command

python
@pytest.mark.django_db
def test_thing(client, settings):
    settings.FEATURE_X = True          # restored automatically after the test
    assert client.get("/").status_code == 200

pytest-django blocks the database by default, so `@pytest.mark.django_db` (or the `db` fixture) is both permission and documentation. The plain form is a rolled-back transaction like `TestCase`; `transaction=True` mirrors `TransactionTestCase` and costs real time, so use it only when the commit is the subject. `db` is function-scoped, so no session-scoped fixture may request it — reserve session scope for expensive read-only work with no database, and seed a whole run through `django_db_setup` plus `django_db_blocker.unblock()`.

4.Parameterization, marks, async tests, and factories

`@pytest.mark.parametrize` runs one test body against many inputs, and each case is reported separately — so a failure names the exact input rather than "the test failed". **Marks** are labels: `@pytest.mark.slow` plus `-m "not slow"` splits a suite, and `django_db` is itself a mark. **Async tests** need `pytest-asyncio` (`@pytest.mark.asyncio`) and, in Django, `@pytest.mark.django_db` with async-safe database access — the ordinary ORM inside an async test raises `SynchronousOnlyOperation` exactly as it does in production. **Factories** (`factory_boy`) replace hand-built fixtures: `OrderFactory(status="paid")` builds a valid object with every required field filled and only the ones you care about specified.

Key features

  • `@pytest.mark.parametrize("a,b", [(1, 2), (3, 4)])` reports each case separately, so a failure names the input
  • Stacked `parametrize` decorators multiply — two lists of three become nine tests, which is easy to do by accident
  • Register custom marks in `pytest.ini` under `markers =`, or `--strict-markers` turns a typo into an error instead of a silently skipped filter
  • Async tests need `pytest-asyncio`; inside one, the plain ORM raises `SynchronousOnlyOperation` just as it does in production
  • `factory_boy` fills every required field, so adding a non-nullable column breaks one factory rather than every test
  • Use `SubFactory` for relations and `Faker` for realistic values — defaults that are all `"test"` and `0` hide the bugs real data finds

Four tools, four kinds of repetition removed

Four tools, four kinds of repetition removed
ToolRemovesFailure mode it prevents
`parametrize`near-identical test bodies"the test failed" without saying for which input
Marksrunning everything all the timea slow suite nobody runs before pushing
`pytest-asyncio`manual event-loop plumbingasync code that is only ever tested synchronously
Factoriessetup that buries the relevant detaila new required field breaking every test file
The same three cases, written twice

Hand-built setup, repeated

  • Three near-identical tests; the difference is one word each.
  • Every required field is spelled out, burying the relevant one.
  • Adding a non-nullable column to Customer breaks all three.
  • A failure says "test_paid_is_visible failed" and nothing more.
  • The reader has to diff the tests to see what varies.

Factory plus parametrize

  • One body, one table — the table IS the list of cases considered.
  • The factory fills everything irrelevant, so only status appears.
  • A new required field is one change, in the factory.
  • A failure reports test_visibility[cancelled-410]: the exact case.
  • Adding a fourth case is one line.
  • Hand-built setup, repeated
    • Three near-identical tests; the difference is one word each.
    • Every required field is spelled out, burying the relevant one.
    • Adding a non-nullable column to Customer breaks all three.
    • A failure says "test_paid_is_visible failed" and nothing more.
    • The reader has to diff the tests to see what varies.
  • Factory plus parametrize
    • One body, one table — the table IS the list of cases considered.
    • The factory fills everything irrelevant, so only status appears.
    • A new required field is one change, in the factory.
    • A failure reports test_visibility[cancelled-410]: the exact case.
    • Adding a fourth case is one line.

Command

python
class OrderFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Order

    customer = factory.SubFactory(CustomerFactory)
    total = factory.Faker("pydecimal", left_digits=3, right_digits=2, positive=True)

`parametrize` turns near-identical tests into one body plus a table, and the table documents the boundaries you considered — each case is reported by name, so a failure identifies the input. Register custom marks so `--strict-markers` catches a typo rather than silently skipping. Async tests need `pytest-asyncio`, and the ORM inside one raises `SynchronousOnlyOperation` exactly as in production. Factories fill every irrelevant field so a test shows only what matters — but keep the defaults realistic, or they hide the bugs real data would find.

Remember

  • captureOnCommitCallbacks(execute=True) tests on_commit behaviour without paying for TransactionTestCase.
  • Assert on response.context rather than on rendered HTML, and use reverse() rather than literal paths — otherwise a template or routing change fails forty unrelated tests.
  • The db fixture is function-scoped, so a session-scoped fixture cannot request it; seed a whole run via django_db_setup and django_db_blocker.unblock().
  • The ORM inside an async test raises SynchronousOnlyOperation exactly as it does in production — wrap it in sync_to_async.

Pro tip

  • django_assert_num_queries is the cheapest performance guard a Django suite can have — it turns an N+1 regression into a failing test.
  • Keep factory defaults realistic: values that are all "test" and 0 pass every assertion and hide the bugs real data would surface.

Next up

  • Mocking
  • Django API Testing
  • Database Testing

Sources

  • Django testing tools — docs.djangoproject.com/en/5.2/topics/testing/tools/
  • pytest-django — pytest-django.readthedocs.io
  • Every claim checked against Django 5.2 LTS and pytest-django 4.x docs · checked 2026-09-04

Spotted a mistake on this page? Tell us.