Course outline

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.

About 4 min readContent reviewed
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.

python Isolated executable example
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())
Expected output
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.

python Isolated executable example
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)
Expected output
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.

Official references