Django / Reliable application workflows / Lesson 12
Testing requests and application boundaries
Match unit, client, database, and browser evidence to the promise being tested, with explicitly isolated settings and fixtures.
On this page
Lesson goals
- Begin with the observable contract
- Execute a complete database-free client test
- Control collaborators and preserve meaningful failures
Begin with the observable contract
A test supplies controlled input, performs an operation, and checks a justified outcome. Derive expected values from the requirement rather than copying the implementation’s calculation into the test. For a protected edit, status alone is insufficient: verify the resource stayed unchanged after denial and that a valid owner can still succeed. A snapshot can show markup but not prove keyboard interaction or error recovery. Choose the smallest test boundary that exercises the promised behavior, then add integration evidence where the boundary crosses another system. A large test with many unrelated assumptions can make the first failure harder to identify.
Know which Django test tool you are using
RequestFactory calls can isolate view logic without middleware. Django’s test Client drives the configured request stack in process, but it is not a real browser or reverse proxy. SimpleTestCase is appropriate for tests that should not use a database, while TestCase and TransactionTestCase provide different database and transaction behavior. Verify disposable database settings before any database-backed test; a test class name does not prove the target is safe. SQLite can support many isolated fixtures without reproducing every production engine’s locking, collation, JSON, or migration semantics. Keep these limitations explicit rather than treating every green test as evidence for the entire deployment.
Execute a complete database-free client test
The first program defines its own URL module and one plain-text view, then runs a SimpleTestCase through Python’s unit-test runner. It asserts status and body and prints only stable result summary data. Capturing the runner stream avoids a timing-dependent expected output. This exercises the configured in-process client route without opening a server or touching a database. It does not establish authentication, browser JavaScript, proxy headers, or a production server’s behavior because those are absent from the fixture. Keeping one narrow promise per test makes the result understandable and lets another reader derive why the assertions should pass.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
import io
import unittest
from django.http import HttpResponse
from django.test import SimpleTestCase, override_settings
from django.urls import path
def ping(request):
return HttpResponse("ready", content_type="text/plain")
urlpatterns = [path("ping/", ping)]
@override_settings(ROOT_URLCONF=__name__)
class PingTests(SimpleTestCase):
def test_ready(self):
response = self.client.get("/ping/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"ready")
result = unittest.TextTestRunner(stream=io.StringIO()).run(unittest.defaultTestLoader.loadTestsFromTestCase(PingTests))
print(result.testsRun, result.wasSuccessful())
1 True
The complete client fixture runs one database-free test. Its runner summary is stable and no HTTP server is started.
Opt into CSRF enforcement when testing that contract
Django’s ordinary test Client relaxes CSRF checks for convenience. A passing POST test under that default is therefore not proof that the browser’s CSRF wiring is correct. The second program explicitly uses enforce_csrf_checks=True with the middleware and verifies a synthetic tokenless write receives 403. It does not make a network request or test a real account. Add a valid protected submission case in the application’s isolated suite when checking token delivery and origin policy. Do not disable production middleware merely to match a permissive test client, and do not confuse a deliberate CSRF rejection with form-field validation.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.http import HttpResponse
from django.test import Client, override_settings
from django.urls import path
def write(request):
return HttpResponse("accepted", content_type="text/plain")
urlpatterns = [path("write/", write)]
with override_settings(ROOT_URLCONF=__name__, MIDDLEWARE=["django.middleware.csrf.CsrfViewMiddleware"], TEMPLATES=[{"BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": False}]):
response = Client(enforce_csrf_checks=True).post("/write/", {})
print(response.status_code)
403
The middleware rejects a synthetic unsafe request without a CSRF token. This is an intentional expected rejection inside a complete isolated program.
Control collaborators and preserve meaningful failures
Use synthetic fixtures and appropriate fakes for email, remote APIs, and storage when testing whether the application requests an effect. Retain focused adapter integration tests where actual transport or engine semantics matter. Do not send real mail, create external resources, or exercise configured services for ordinary feature validation. Clean up temporary files and subscriptions on both success and failure. For authorization, cover guests, permitted users, and another authenticated user; for async work, control completion order. Keep a regression case after repairing a defect, and rerun the relevant checks after a focused change instead of repeatedly broadening tests without a new concern.
Common errors and fixes
A POST test passes while browser CSRF fails
- Why it happens
- The ordinary test client bypassed the relevant check.
- How to fix it
- Use explicit CSRF enforcement for that contract.
- Prevent it next time
- Separate tokenless rejection from valid protected submission.
A permission test passes despite a partial write
- Why it happens
- It checked only the final response code.
- How to fix it
- Assert unchanged state after denial.
- Prevent it next time
- Pair a negative case with an allowed case.
Tests depend on a live service and fail unpredictably
- Why it happens
- The external collaborator is uncontrolled.
- How to fix it
- Use a bounded fake or approved isolated adapter fixture.
- Prevent it next time
- Make each test’s dependency boundary explicit.
Check your understanding
1. What should an ownership test assert beyond a denied response?
Show worked solution
Reload the protected record and compare its original values and owner, then test a legitimate owner operation separately. This catches write-before-deny defects and prevents an always-deny implementation from satisfying the whole suite. The stored invariant is part of the result, not an implementation detail.
2. Why cannot an in-process Client test prove reverse-proxy HTTPS configuration?
Show worked solution
It does not send a real request through that proxy or verify how trusted headers and TLS termination are delivered. Keep unit/client tests for application behavior, then validate the reviewed deployment boundary with suitable isolated integration evidence. Do not claim the omitted layer was exercised.
Key takeaways
- Choose the test boundary before interpreting success.
- CSRF enforcement must be explicit in relevant client tests.
- Isolate resources and verify actual outcomes.