The Django Testing Framework
The Django testing framework provides a robust suite of tools for verifying application behavior, ranging from simple unit tests to complex integration tests involving live servers. The framework is built upon the standard Python unittest library but extends it with specialized classes and utilities tailored for the Django environment.
The Test Case Hierarchy
The core of the testing framework is a hierarchy of TestCase classes in django.test.testcases.py, each providing a different level of isolation and functionality.
SimpleTestCase
SimpleTestCase is the base class for tests that do not require a database. It provides a wide array of Django-specific assertions, such as:
assertContainsandassertNotContains: Verify that a response contains specific text or HTML.assertTemplateUsedandassertTemplateNotUsed: Check which templates were rendered.assertRedirects: Ensure a request was redirected to the expected URL.assertJSONEqualandassertXMLEqual: Compare structured data formats.
It also automatically sets up a Client and AsyncClient instance for each test method in its _pre_setup method:
@classmethod
def _pre_setup(cls):
cls.client = cls.client_class()
cls.async_client = cls.async_client_class()
mail.outbox = []
TransactionTestCase
TransactionTestCase inherits from SimpleTestCase and adds database support. It achieves test isolation by flushing the database (truncating tables) after every test in _fixture_teardown. This is slower than using transactions but is necessary for testing code that performs its own transaction management.
TestCase
TestCase is the most commonly used class. It inherits from TransactionTestCase but uses database transactions to achieve isolation, which is significantly faster. It wraps the entire test class in a transaction in setUpClass and each individual test method in another transaction in _fixture_setup.
@classmethod
def _enter_atomics(cls):
"""Open atomic blocks for multiple databases."""
atomics = {}
for db_name in cls._databases_names():
atomic = transaction.atomic(using=db_name)
atomic._from_testcase = True
atomic.__enter__()
atomics[db_name] = atomic
return atomics
Data Setup and Isolation
To optimize data creation, TestCase provides the setUpTestData hook. Data created here is shared across all test methods in the class.
The TestData Descriptor
To prevent one test method from accidentally modifying data used by another, Django uses the TestData descriptor. When setUpTestData finishes, Django wraps any class attributes created during the process in a TestData instance. When a test method accesses the attribute, the descriptor returns a deepcopy of the original data.
class TestData:
def __get__(self, instance, owner):
if instance is None:
return self.data
memo = self.get_memo(instance)
data = deepcopy(self.data, memo)
setattr(instance, self.name, data)
return data
Simulating Requests
Django provides two primary ways to simulate HTTP requests: the Client and the RequestFactory.
Test Client
The Client (and its asynchronous counterpart AsyncClient) simulates a browser. It maintains state (like cookies and sessions) and runs requests through the full middleware chain using ClientHandler.
class Client(ClientMixin, RequestFactory):
def request(self, **request):
environ = self._base_environ(**request)
# ... signals and exception handling ...
try:
response = self.handler(environ)
finally:
# ... cleanup ...
return response
The Client also annotates the response with useful testing metadata, such as response.context and response.templates.
RequestFactory
RequestFactory provides a way to generate a WSGIRequest (or ASGIRequest via AsyncRequestFactory) that can be passed directly to a view function. This bypasses the middleware and routing layers, making it ideal for true unit tests of view logic.
Advanced Testing Utilities
LiveServerTestCase
For tests that require a real HTTP server (e.g., Selenium integration tests), LiveServerTestCase launches a LiveServerThread in the background. This thread runs a ThreadedWSGIServer that serves the application, including static and media files.
override_settings
The override_settings utility in django.test.utils allows you to temporarily modify Django settings for the duration of a test method or class. It acts as both a decorator and a context manager.
with self.settings(DEBUG=True):
# Test behavior with DEBUG enabled
pass
CaptureQueriesContext
To verify database performance, CaptureQueriesContext (used by the assertNumQueries assertion) records every SQL query executed within its scope.
def assertNumQueries(self, num, func=None, *args, using=DEFAULT_DB_ALIAS, **kwargs):
conn = connections[using]
context = _AssertNumQueriesContext(self, num, conn)
if func is None:
return context
with context:
func(*args, **kwargs)
Test Execution
The DiscoverRunner class in django.test.runner is responsible for finding and running tests. It handles:
- Discovery: Finding test files matching a pattern (default
test*.py). - Suite Building: Constructing a
TestSuitefrom the discovered tests. - Database Setup: Creating the test databases via
setup_databases. - Parallelization: If requested, it uses
ParallelTestSuiteto distribute tests across multiple processes usingmultiprocessing.Pool.
class ParallelTestSuite(unittest.TestSuite):
def run(self, result):
# ... initialization ...
with multiprocessing.Pool(processes=self.processes, ...) as pool:
test_results = pool.imap_unordered(self.run_subsuite.__func__, args)
# ... handle events and results ...
This comprehensive infrastructure ensures that tests are isolated, efficient, and capable of exercising every layer of a Django application.