The test database lifecycle, and transaction behaviour per test class
coreintermediateYour tests never touch the real database. Django creates a second one named `test_` plus your real database name, runs every migration into it, runs the suite, and destroys it at the end — pass or fail. Inside that database, the class you inherit from decides how each test is cleaned up. `TestCase` wraps each test in a transaction and rolls it back, which is fast. `TransactionTestCase` lets the test really commit and then truncates every table, which is slow but is the only way to test anything that depends on a commit actually happening.
Think of it as
Think of two nested boxes. The outer box is the test *database*: created once before the suite, migrated, and dropped afterwards. The inner box is the test *transaction*: opened before one test method and thrown away after it. Almost everything you write lives in the inner box, and the inner box is why your tests do not leak rows into each other. The moment your code under test cares about the boundary of that box, the box becomes the bug. `transaction.on_commit()` callbacks never fire, because the commit never happens. `select_for_update()` cannot be shown to block, because there is no second committed transaction to block against. A real `IntegrityError` from a deferred constraint arrives at commit time, which never arrives. That is the whole decision rule: if the behaviour under test is *inside* a transaction, use `TestCase` and enjoy the speed; if the behaviour under test *is* the transaction, you have to give it up and use `TransactionTestCase`, paying a truncate per test. The third option is the one people forget — `captureOnCommitCallbacks(execute=True)` emulates a commit inside a fast `TestCase`, which covers most `on_commit` work without the slow class.
What we're doing: Show the rollback boundary biting, and the three ways past it — in the order you should try them.
- 1–4
- The rollback boundary as a feature, not a problem: this test asserts the receipt is *not* sent before the transaction commits, which is the behaviour `on_commit()` exists to give you.
- 8–9
- The middle option, and the one to reach for first. `execute=True` runs the captured callbacks as the context manager exits, "emulating a commit", inside a fast rollback-based test.
- 12
- Assert on the callbacks list too. A refactor that stops registering the callback at all still leaves `mailoutbox` empty in the wrong way otherwise.
- 17–18
- Only here is `transaction=True` earned: a second thread on a second connection has to *see* a committed row for the lock to mean anything. This test costs a truncate.
- 22–27
- `nowait=True` turns "block forever" into an immediate `OperationalError`, so the test fails in milliseconds instead of hanging the suite.
Why this works: The three tests are the whole decision rule in order of cost: assert the pre-commit behaviour if that is what you mean, emulate the commit if you need the side effect, and pay for a real transaction only when a second connection has to observe the first one.
Asserting on an `on_commit()` side effect inside a plain `TestCase`
Wrong
Better
What you see: The feature works in a browser and the test fails with an empty outbox. The usual next move is to delete the `on_commit()` wrapper "because it breaks the tests" — which reintroduces the bug of emailing a receipt for an order that then rolls back.
Why: `TestCase` never commits, so `transaction.on_commit()` never runs its callbacks. The failure is real information about the test harness, not about the code. `captureOnCommitCallbacks(execute=True)` runs those callbacks as the block exits and keeps the fast rollback cleanup, so you get the assertion without moving the whole test class to `TransactionTestCase`.
- TestCase — rollback
- BEGIN before the test, ROLLBACK after it
- Nothing is ever committed to the test database
- `on_commit()` callbacks never fire on their own
- A second connection cannot see the rows this test made
- Fast: no table truncation between tests
- TransactionTestCase — truncate
- The test really commits
- Every table is truncated afterwards
- `on_commit()` fires, locks really block
- A second connection sees the committed rows
- Slow: pay a truncate per test, so use it deliberately
Which class (or marker) to reach for, and what it costs
Together
Remember: Django builds `test_<your database>`, migrates it, and destroys it after the run — use `--keepdb` (or `pytest --reuse-db`) so you stop paying for the migrate every time. Inside it, `TestCase` rolls back and `TransactionTestCase` truncates. Stay on the fast one until the thing under test *is* the commit: reach for `captureOnCommitCallbacks(execute=True)` for `on_commit` side effects, and only pay for `transaction=True` when a second connection has to see committed rows.
See also: testing constraints unique violations and races · the test case classes · on commit and transaction timing

