Chapter 22 · The test framework
22.6 Emptying the tables after each test: TransactionTestCase
TransactionTestCase is the test case class whose tests write to the test database as any code does, committing for real, and which puts the database back the long way: after each test TransactionTestCase runs the flush command, and before the next TransactionTestCase loads the class’s fixtures and, where the class sets serialized_rollback, the rows that migrations wrote.
A rollback cannot undo what a test committed, and some tests must commit: code that commits or rolls back, to see the effect, or a check that code runs inside a transaction, as select_for_update requires, which Django’s documentation says cannot be made in a TestCase. Such a test runs in autocommit, and the way back is to delete what it wrote: a flush after every test, where a TestCase on a database with transactions rolls back (A transaction around each test: TestCase).
The class’s work is in Django’s two hooks around a test. Each has a database half, TransactionTestCase and TransactionTestCase, which TestCase replaces; the rest it inherits. Both work on the class’s databases: the aliases of databases in the order of DATABASES, less any whose "TEST" settings name a "MIRROR" (Test databases: BaseDatabaseCreation; TransactionTestCase). The class also declares TransactionTestCase and TransactionTestCase (Counting queries: assertNumQueries and CaptureQueriesContext).
Before each test: _pre_setup
TransactionTestCase is a class method, and works in this order:
- The parent’s,
SimpleTestCase: a new client, the outbox emptied (One test, in order:._pre_setup SimpleTestCase). - Where the class sets
TransactionTestCase,.available_apps Appsnarrows the app registry to them;.set_available_apps setting_changedis sent forINSTALLED_APPS, and its receivers drop what they built from the registry (Overriding settings); andemit_post_migrate_signalsendspost_migrateon each of the class’s databases, to those applications alone. _fixture_setup. If it raises, the narrowing is undone andsetting_changedsent again before the exception goes on.- Its databases’ query logs are emptied, so that, the comment says, they are less likely to overflow and break
assertNumQueries.
The first test’s set-up runs in setUpClass
TransactionTestCase calls the parent’s and then, for a class that is not a TestCase, calls _pre_setup and sets SimpleTestCase on the class; SimpleTestCase finds the flag at the first test that is not skipped, clears it, and does not call _pre_setup again. Rows that a class’s own setUpClass writes are committed: in a run, the first test found such a row and the second, after the flush, did not.
TransactionTestCase undoes a narrowing that no test undid, as when every test was skipped, counted in TransactionTestCase.
The rows a test begins with: _fixture_setup
TransactionTestCase works on each of the class’s databases in turn, and what it writes is committed:
- With
TransactionTestCase,.reset_sequences TransactionTestCasesets each table’s sequence back to its start, on a backend whose._reset_sequences BaseDatabaseFeaturesis true (Introspection: reading the schema back)..supports_sequence_reset - With
TransactionTestCase, and where the runner kept the database’s rows as text after making it,.serialized_rollback BaseDatabaseCreationwrites them back, the narrowing to.deserialize_db_from_string available_appslifted while it does. - With
TransactionTestCase, loaddata loads them (Fixtures: dumpdata and loaddata)..fixtures
After each test: _post_teardown and the flush
TransactionTestCase calls _fixture_teardown, then closes each connection made in this thread, whatever its alias, and in a finally undoes the narrowing to available_apps. The comment’s reason for the closing: some cursors send statements as they are made, a time zone setting among them, and a test that rolls back loses their effect. A TestCase does not close them where its databases have transactions (TestCase), and SQLite’s backend ignores the close of a database in memory.
_fixture_teardown calls flush on each of the class’s databases, with reset_sequences=False, with allow_cascade=True only under available_apps, and with inhibit_post_migrate=True under available_apps, or under serialized_rollback where the runner kept the rows, the comment saying the latter is so as not to recreate the serialized rows. Flush empties the models’ tables in one transaction (Introspection: reading the schema back), and then, unless inhibited, sends post_migrate on the database for each application with a models module, its comment saying this lets each respond as if the database had been migrated from scratch: contenttypes and auth make their rows again (The migrate command, showmigrations and sqlmigrate; Command.handle).
A class of the example’s, whose fixture, the office application’s fixtures/valletta.json, holds the port Valletta with the key 1; its first test commits a ship and its second finds none:
# harbour/office/tests/test_commits.py
from django.db import transaction
from django.test import TransactionTestCase
from harbour.fleet.models import Port, Ship
class ShipCommitTests(TransactionTestCase):
fixtures = ["valletta"]
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.valletta = Port.objects.get(name="Valletta")
def test_commit(self):
with transaction.atomic():
Ship.objects.create(name="Endeavour", tonnage=366, home=self.valletta)
self.assertEqual(Ship.objects.count(), 1)
def test_empty_again(self):
self.assertEqual(Ship.objects.count(), 0)
self.assertEqual(Port.objects.get().name, "Valletta")
Both pass. With each hook printed as entered and each statement under it, lines left out at each ...:
test_commit
SQL BEGIN
SQL INSERT INTO "fleet_ship" ("name", "tonnage", "home_id", "captain_id") VALUES ('Endeavour', 366, 1, NULL) RETURNING "fleet_ship"."id"
SQL COMMIT
...
TransactionTestCase._post_teardown
TransactionTestCase._fixture_teardown
...
SQL DELETE FROM "django_content_type";
...
SQL COMMIT
...
TransactionTestCase._pre_setup
TransactionTestCase._fixture_setup
...
SQL INSERT INTO "fleet_port" ("id", "name", "country") VALUES (1, 'Valletta', 'Malta') RETURNING "fleet_port"."id"
...
test_empty_again
...
The statements of post_migrate’s receivers follow the "COMMIT". The deletes come in the order of a set of table names, which changed from one run to the next.
Fewer applications: available_apps
available_apps is private, Django’s documentation says, and spares Django’s own test suite the post_migrate after each flush, whose cost grows with the number of models. Under it, whatever asks the registry sees only the applications named: the flush empties their tables and, where the backend cascades, those that refer to them, and sends no post_migrate; and loaddata looks for fixtures in their directories and no other application’s.
In a run with available_apps = ["harbour, the port a test made was gone at the next, and the user it made, nora, was not. The post_migrate before each test wrote nothing: the receivers of contenttypes and auth return when the registry has no ContentType or no Permission.
Rows that migrations wrote: serialized_rollback
The flush takes every row, and post_migrate puts back only what its receivers write, under new keys, the sequences not being reset: in a run, Ship’s content type had the key 9 in a class’s first test and 23 in its second. A class with serialized_rollback = True has the rows the runner serialized after migrating written back before each test, keys and all, and its flush sends no post_migrate: run alone, such a class saw 9 in both tests (Test databases: BaseDatabaseCreation).
Classes with and without it do not mix. Run after a class without it, a class with serialized_rollback failed in setUpClass with "UNIQUE constraint failed: django_content_type: the first content type written back, key 1, met the one post_migrate had made for the same model under another key. With --reverse, the first test of the class without it found no content type, the other’s last flush having sent no post_migrate. The documentation says the like of available_apps: no harm, provided every TransactionTestCase of a suite sets it, or none does.