Chapter 22 · The test framework
22.5 One test, in order: SimpleTestCase
How SimpleTestCase runs one test, Django’s hooks SimpleTestCase and SimpleTestCase around those of unittest; what SimpleTestCase does for a class; and SimpleTestCase and SimpleTestCase.
unittest gives a test case two hooks around each test, unittest and unittest, and both are the project’s to override. So SimpleTestCase, the base of every Django test case class, does its own work around each test from __call__, the method unittest calls to run one: its docstring says this is so that a project’s test case need not call super().setUp().
The hooks of a class
The three classes run the same hooks in the same order (Emptying the tables after each test: TransactionTestCase, A transaction around each test: TestCase). Each class does what its parent does, and what its column adds or replaces:
_pre_setup to _post_teardown repeat for each test. TestCase’s column holds where its databases have transactions and savepoints.| hook | SimpleTestCase |
Transaction |
TestCase |
|---|---|---|---|
setUpClass |
SimpleTestCase the class’s overrides entered; the database guard set |
Transaction if not a TestCase, _pre_setup for the first test |
TestCase an atomic block on each of its databases; the fixtures; Test; its objects made TestData |
_pre_setup |
SimpleTestCase new clients; the outbox emptied |
Transaction as the class asks, Transaction applied, sequences reset, serialized rows and fixtures loaded; the query log cleared |
TestCase in place of the reset and the loading, an atomic block inside the class’s: a savepoint |
setUp, the method, tearDown, the cleanups |
SimpleTestCase the project’s; an async def method run by async_to_sync |
Transaction nothing |
TestCase nothing |
_post_teardown |
SimpleTestCase nothing |
Transaction the flush command on each of its databases; connections closed; available_apps undone |
TestCase in place of the flush, deferred constraints checked, the savepoint rolled back; connections left open |
tearDownClass |
SimpleTestCase nothing |
Transaction if not a TestCase, available_apps undone if still applied |
TestCase the class’s atomic blocks rolled back; connections closed |
| the class cleanups | SimpleTestCase the database guard removed; the overrides left |
Transaction nothing |
TestCase nothing |
Running one test: _setup_and_call
unittest’s suite calls a test with the result that collects outcomes, and __call__ passes that to SimpleTestCase:
- A test whose class or method is marked skipped, as
unittest.skipmarks it, gets neither of Django’s hooks. - A coroutine test method is replaced on the instance by
async_to_syncof it, which runs it on an event loop in another thread (Sync and async in one chain);unittestand.TestCase .setUp unittestare not converted..TestCase .tearDown _pre_setupis called, unless the class’sSimpleTestCasesays._pre_setup_ran_eagerly TransactionTestCasehas done it; then the flag is cleared. If it raises, the exception is added to the result as an error, and nothing more is done..setUpClass unittestruns the test: the result’s.TestCase .__call__ unittest;.TestResult .startTest setUp; the method andtearDownonly ifsetUpsucceeded; the cleanups in any case._post_teardownis called, whatever the outcome. An exception there is added as an error too, and a test that passed is then reported both as a success and as an error.
A module that prints each hook as it runs:
# harbour/office/tests/test_hooks.py
import threading
from django.test import SimpleTestCase, TestCase
class Hooks:
@classmethod
def setUpClass(cls):
print(cls.__name__, "setUpClass")
super().setUpClass()
@classmethod
def _pre_setup(cls):
print(" _pre_setup")
super()._pre_setup()
def setUp(self):
print(" setUp")
self.addCleanup(print, " cleanup")
def tearDown(self):
print(" tearDown")
def _post_teardown(self):
super()._post_teardown()
print(" _post_teardown")
class ShipPageHookTests(Hooks, TestCase):
async def test_page(self):
print(" test_page, on", threading.current_thread().name)
class ShipNumberTests(TestCase):
reset_sequences = True
def test_first_ship(self):
print(" test_first_ship")
class BerthHookTests(Hooks, SimpleTestCase):
def setUp(self):
super().setUp()
raise RuntimeError("no berth free")
def test_berth(self):
print(" test_berth")
At verbosity 0, the second error’s traceback left out:
$ python manage.py test harbour.office.tests.test_hooks -v 0
System check identified no issues (0 silenced).
ShipPageHookTests setUpClass
_pre_setup
setUp
test_page, on ThreadPoolExecutor-1_0
tearDown
cleanup
_post_teardown
BerthHookTests setUpClass
_pre_setup
setUp
cleanup
_post_teardown
======================================================================
ERROR: test_first_ship (harbour.office.tests.test_hooks.ShipNumberTests.test_first_ship)
----------------------------------------------------------------------
TypeError: reset_sequences cannot be used on TestCase instances
======================================================================
ERROR: test_berth (harbour.office.tests.test_hooks.BerthHookTests.test_berth)
----------------------------------------------------------------------
...
RuntimeError: no berth free
----------------------------------------------------------------------
Ran 2 tests in 0.002s
FAILED (errors=2)
Where setUp raised, the cleanup and _post_teardown still ran. ShipNumberTests’ method never ran: on a database with transactions, TestCase, which _pre_setup calls, refuses TransactionTestCase. The run counts two tests of three: counting is startTest’s, which _setup_and_call never reached.
SimpleTestCase runs the same steps but raises what they would add to a result, and runs the test by unittest, so a failing test never reaches _post_teardown. SimpleTestCase makes a test case picklable, as a failed subtest sent from a parallel run’s worker needs (Running in parallel: ParallelTestSuite).
Before each test: _pre_setup
SimpleTestCase makes a test client and an asynchronous one, of the classes SimpleTestCase and SimpleTestCase name, and empties the local-memory email backend’s outbox. Each test so begins with no mail and a client with no cookies, and so no session, but a TransactionTestCase’s first test finds the outbox and the client as setUpClass left them, having run _pre_setup for it. It is a class method, and keeps the clients on the class, as SimpleTestCase and SimpleTestCase.
A class: SimpleTestCase.setUpClass
SimpleTestCase calls unittest’s, then does two things. If override_settings or modify_settings decorated the class, it enters a new one made from their options, by unittest (Settings for one test: override_settings and TestContextDecorator). Then it sets the guard that refuses the class every database it does not name, and registers the guard’s removal with unittest (Which databases a test uses: databases), where the override’s exit was registered too. Class cleanups run after tearDownClass, the last registered first, and, unlike tearDownClass, also when setUpClass raised after registering them. A project’s setUpClass that does not call the parent’s loses all of this, as the documentation warns.
Inside a test, SimpleTestCase and SimpleTestCase return an override_settings and a modify_settings.
Assertions on exceptions and form fields
SimpleTestCase and SimpleTestCase are unittest and unittest with a check that the expected text is in the string of what was caught, as a substring, not a pattern. Given a callable and its arguments, the assertion calls it; given none, it is a context manager. This test passes, its parentheses taken as they stand:
# harbour/office/tests/test_urls.py
from django.test import SimpleTestCase
from django.urls import NoReverseMatch, reverse
class ShipUrlErrorTests(SimpleTestCase):
def test_detail_needs_a_number(self):
with self.assertRaisesMessage(NoReverseMatch, "1 pattern(s) tried"):
reverse("ship-detail", args=["Endeavour"])
SimpleTestCase makes a form field as given and with required=False. Each valid input must clean to its value in both, each invalid one raise a ValidationError with the messages given, and each of empty_values be refused by the first with its "required" message and cleaned by the second to the empty value given, "" by default; a subclass of django must accept min_length=2 and max_length=20.