RizTech Academy logo
RizTech Academy
Testing and QualityLesson 3 of 430 min

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

  1. Test the list view returns 200 and shows a created patient's name in response.content.
  2. Test the create view: valid POST → 302 and the record exists; invalid POST → 200 and it does not. Reproduce the verified statuses.
  3. Add @login_required to a view and test both the anonymous redirect (302 to login) and the force_login success (200).
  4. Create two users and an object owned by one; test that the other cannot access it (404/403) — an object-level access test.
  5. Use follow=True to assert a success message appears on the destination page after a create.
  6. 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

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