Testing views and forms
Model tests check your logic in isolation; view tests check that the whole request/response cycle works — the right URL calls the right view, which queries, renders, redirects, and enforces permissions correctly. Django's test client makes this straightforward: it makes requests to your views in-process and hands back the response to assert on. This lesson tests Nidaan's views, with the same weighting as before: the failures and the access rules matter most.
The test client: requests without a server
TestCase gives every test a self.client — a test client that calls your views directly (no running
server, no network) and returns the response:
from django.test import TestCase
from .models import Patient
class PatientViewTests(TestCase):
def test_list_shows_patients(self):
Patient.objects.create(name="Asha", phone="09812345678", city="Pune")
response = self.client.get("/patients/?city=Pune")
self.assertEqual(response.status_code, 200) # verified: 200
self.assertIn(b"Asha", response.content) # the patient appears in the HTML
Verified behaviour: the list view returns 200 and the patient's name appears in the rendered content. The
client handles the full cycle — URL resolution, the view, template rendering — so a passing test means the
whole path works, not just the view function. response.status_code and response.content are your primary
assertions; self.client.get(url) and self.client.post(url, data) are the two calls you make most.
Testing the POST-redirect-GET flow
The create view's whole contract — validate, save, redirect on success; re-render with errors on failure — is exactly what a view test should pin down:
def test_create_valid_redirects_and_saves(self):
response = self.client.post("/patients/new/",
{"name": "Chetan", "phone": "09812345000", "city": "Mumbai"})
self.assertEqual(response.status_code, 302) # verified: redirect
self.assertTrue(Patient.objects.filter(name="Chetan").exists()) # verified: created
def test_create_invalid_rerenders_and_does_not_save(self):
response = self.client.post("/patients/new/",
{"name": "Bad", "phone": "12", "city": "Pune"})
self.assertEqual(response.status_code, 200) # verified: re-render (not redirect)
self.assertFalse(Patient.objects.filter(name="Bad").exists()) # verified: nothing created
Verified: a valid POST returns 302 and creates the patient; an invalid POST returns 200 (re-render)
and creates nothing. These two tests capture the entire POST-redirect-GET contract — success redirects,
failure re-renders, and bad data is never saved. The status code alone distinguishes the two paths (302 vs
200), and the .exists() check confirms the database effect. This is the highest-value view test: it fails
if someone breaks the validation, the redirect, or the save.
Testing authentication and permissions
Access rules are security, so test both the allowed and the denied paths. To act as a logged-in user, use
self.client.force_login:
from django.contrib.auth.models import User
class ProtectedViewTests(TestCase):
def test_dashboard_requires_login(self):
response = self.client.get("/dashboard/")
self.assertEqual(response.status_code, 302) # redirected to login
self.assertIn("/accounts/login/", response.url)
def test_logged_in_user_sees_dashboard(self):
user = User.objects.create_user("staff", password="pw-123456")
self.client.force_login(user) # log in without a password round-trip
response = self.client.get("/dashboard/")
self.assertEqual(response.status_code, 200)
force_login(user) authenticates a user for the test without going through the login form — the clean way
to test a view as a particular user. Test the denied path (anonymous → redirect/403) as carefully as
the allowed one: "an unauthorised user is blocked" is precisely the assertion that catches a permission you
accidentally removed. For object-level rules, create two users and confirm one cannot see the other's
records — a direct test of the privacy the object-level-permissions lesson demanded.
Following redirects and checking messages
Sometimes you want to test what the user ends up seeing after a redirect (including the success message).
follow=True makes the client follow the redirect to the final page:
def test_create_shows_success_message(self):
response = self.client.post("/patients/new/",
{"name": "Divya", "phone": "09812345111", "city": "Pune"},
follow=True) # follow the redirect to the list
self.assertEqual(response.status_code, 200)
self.assertIn(b"Added Divya.", response.content) # the message crossed the redirect
Verified: with follow=True, the final page is the list (200) and the "Added Divya." message appears — the
POST-redirect-GET plus messages flow, tested end to end. Without follow, you assert the 302 and its target;
with follow, you assert the destination page's content. Use follow=True when the thing you care about is
the result page, not the redirect itself.
What to assert on a view — and what not to
Keep view tests focused on behaviour, not exact markup:
- Do assert: the status code (200/302/403/404), the database effect (created/updated/deleted), a redirect target, and that key content or an error message is present.
- Do not assert: exact HTML structure, whitespace, or CSS classes — those change with design and make
tests brittle without protecting behaviour. Checking a patient's name appears is robust; asserting the
exact
<table>markup around it is not.
A view test should fail when the view's behaviour is wrong (wrong status, missing data, broken redirect, absent permission check) — not when a designer adjusts the template. Test what the view does, not how the HTML happens to look.
Check your work
The test client. self.client.get/post calls views in-process (no server) through the full cycle;
assert on response.status_code and response.content. Verified: list returns 200 with the patient shown.
POST-redirect-GET. Valid POST → 302 + created (verified); invalid POST → 200 re-render + not created (verified). Two tests capture the whole contract.
Auth and permissions. force_login(user) acts as a user without the login form; test the denied path
(anonymous → redirect/403) as carefully as the allowed; two users for object-level rules.
Following redirects. follow=True lands on the final page to assert its content — verified: the
"Added Divya." message appears after the redirect.
What to assert. Status, database effect, redirect target, key content/errors — not exact HTML/CSS, which is brittle. Test behaviour, not markup.
Practice
- Test the list view returns 200 and shows a created patient's name in
response.content. - Test the create view: valid POST → 302 and the record exists; invalid POST → 200 and it does not. Reproduce the verified statuses.
- Add
@login_requiredto a view and test both the anonymous redirect (302 to login) and theforce_loginsuccess (200). - Create two users and an object owned by one; test that the other cannot access it (404/403) — an object-level access test.
- Use
follow=Trueto assert a success message appears on the destination page after a create. - Write one brittle test that asserts exact HTML and one robust test that asserts the key content; change the template's markup and see which breaks unnecessarily.
Official documentation
- Django — The test client —
get/post,follow,force_login, response attributes. - Django — Testing responses —
assertRedirects,assertContainsand friends. - Django —
force_login— Authenticating in tests.
Next: fixtures and factories.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship