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.
Django has two ways to give every test a clean database. TestCase runs each test inside a transaction and rolls it back at the end. TransactionTestCase lets the test commit for real, then empties every table in the database afterwards. The second is much slower, and the cost grows with the number of tables in your project, not with what the test did.
Many suites use it without needing it: a base class copied from an old project, transaction=True added to fix one test and then copied around, or a live_server fixture used where the test client would do.
Why TransactionTestCase is slow
After every TransactionTestCase test, Django runs the flush management command:
- It truncates every table of every installed app (on PostgreSQL one
TRUNCATEover all tables, on other backends a statement per table). - It sends the
post_migratesignal, which recreates content types and permissions for every model, because the flush deleted them too.
A test that inserts one row still pays for emptying 200 tables and re-inserting hundreds of permission rows.
TestCase instead opens a transaction for the whole class and a savepoint per test, and rolls back to the savepoint after each test. Nothing is committed, so there's nothing to clean up: a rollback costs about as much as the test's own writes.
These all use the flushing behaviour:
django.test.TransactionTestCaseand its subclassesLiveServerTestCaseandStaticLiveServerTestCase- pytest-django's
@pytest.mark.django_db(transaction=True), thetransactional_dbfixture, and thelive_serverfixture reset_sequences=True, which also resets every sequence and only works on transactional tests
The fix
Switch the base class:
-from django.test import TransactionTestCase
+from django.test import TestCase
-class CheckoutTests(TransactionTestCase):
+class CheckoutTests(TestCase):
With pytest-django, drop transaction=True:
[email protected]_db(transaction=True)
[email protected]_db
def test_checkout(client):
...
Run the tests. Most pass unchanged. A TestCase also lets you move shared data into setUpTestData, which only works with the transaction rollback: see setUpTestData instead of setUp.
Tests that need on_commit callbacks
The most common reason a test "needs" TransactionTestCase is transaction.on_commit(): code that sends an email or queues a Celery task after the transaction commits. Inside TestCase nothing commits, so the callbacks never run. Capture and run them instead:
from django.core import mail
from django.test import TestCase
from model_bakery import baker
from shop.models import Order
from shop.services import place_order
class PlaceOrderTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.order = baker.make(Order)
def test_sends_confirmation(self):
with self.captureOnCommitCallbacks(execute=True) as callbacks:
place_order(self.order)
self.assertEqual(len(callbacks), 1)
self.assertEqual(len(mail.outbox), 1)
execute=True runs the callbacks when the block exits, as a commit would. pytest-django has the same as a fixture, django_capture_on_commit_callbacks.
What really needs real commits
Keep TransactionTestCase (or transaction=True) for tests where a second connection must see the data, or where the commit itself is what's tested:
- live server and browser tests: the server runs in another thread with its own connection, which can't see uncommitted rows
- code that uses threads, multiple processes or several database connections, like
select_for_updateraces - statements that can't run inside a transaction, like
CREATE INDEX CONCURRENTLYon PostgreSQL
Keep the remaining ones cheap
- Don't add
reset_sequences=Trueunless a test depends on exact ids; it resets every sequence on top of the flush. Tests shouldn't hardcode ids anyway. - Both Django's runner and pytest-django run transactional tests after the others, so they don't need special ordering. Grouping them in a few modules makes them easy to run as their own job.
- Data created by data migrations is gone after the first flush.
serialized_rollback = Truerestores it, but by reloading a serialized copy of the database for every test, which is slower still. If a test needs that data, create it in the test.
Keep the tests correct
Tests that rely on commit behaviour, multiple connections or a live server still need a transactional test case. If a test fails after the switch, the failure usually names why: an on_commit callback that didn't run (capture it, as above), or another thread or connection that can't see the test's rows (that test genuinely needs TransactionTestCase). Don't make it pass by weakening its assertions.
How flakytest finds it
flakytest measures the time spent in Django's flush command, which it shows as the DB schema / flush category. When that's at least half a second and 1% of test time, it suggests Use TestCase instead of TransactionTestCase, with the number of TransactionTestCase, transaction=True and live_server tests that triggered it and how long the flushing took in total. The tests are listed, so you can go through them one by one.
Further reading
- Django: TransactionTestCase
- Django:
TestCase.captureOnCommitCallbacks() - Django: order in which tests are executed
- pytest-django: testing transactions
- pytest-django:
django_capture_on_commit_callbacks
Related guides
- 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.
- Run Celery tasks eagerly in tests: Tests that call task.delay() publish to a broker, retry against a missing one or wait on a worker. Run tasks eagerly with CELERY_TASK_ALWAYS_EAGER, or mock delay.
- 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.
- Keep tests independent of each other: Tests should pass alone, in any order and in parallel. Find order-dependent pytest tests with pytest-randomly and pytest-xdist, and fix the shared state behind them.
Updated October 18, 2026 · Suggest a change