# 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:

1. It truncates every table of every installed app (on PostgreSQL one `TRUNCATE` over all tables, on other backends a statement per table).
2. It sends the `post_migrate` signal, 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.TransactionTestCase` and its subclasses `LiveServerTestCase` and `StaticLiveServerTestCase`
- pytest-django's `@pytest.mark.django_db(transaction=True)`, the `transactional_db` fixture, and the `live_server` fixture
- `reset_sequences=True`, which also resets every sequence and only works on transactional tests

## The fix

Switch the base class:

```diff
-from django.test import TransactionTestCase
+from django.test import TestCase


-class CheckoutTests(TransactionTestCase):
+class CheckoutTests(TestCase):
```

With pytest-django, drop `transaction=True`:

```diff
-@pytest.mark.django_db(transaction=True)
+@pytest.mark.django_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](https://flakytest.dev/guides/setuptestdata-vs-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:

```python
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_update` races
- statements that can't run inside a transaction, like `CREATE INDEX CONCURRENTLY` on PostgreSQL

### Keep the remaining ones cheap

- Don't add `reset_sequences=True` unless 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 = True` restores 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](https://docs.djangoproject.com/en/stable/topics/testing/tools/#transactiontestcase)
- [Django: `TestCase.captureOnCommitCallbacks()`](https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TestCase.captureOnCommitCallbacks)
- [Django: order in which tests are executed](https://docs.djangoproject.com/en/stable/topics/testing/overview/#order-in-which-tests-are-executed)
- [pytest-django: testing transactions](https://pytest-django.readthedocs.io/en/latest/database.html#testing-transactions)
- [pytest-django: `django_capture_on_commit_callbacks`](https://pytest-django.readthedocs.io/en/latest/helpers.html#django-capture-on-commit-callbacks)
