Testing: Test Client & Views

The previous lesson tested Django code directly — calling model methods and manager methods and checking what came back. That's valuable, but it never actually goes through a URL, a view function, or a template. This lesson covers Django's test client, a tool built into every TestCase that simulates real HTTP requests against TaskFlow's views — GET requests, POST requests, even login sessions — all without starting an actual web server.

The Test Client: self.client

Every test class inheriting from django.test.TestCase automatically has access to self.client, an object that can issue requests to your project's URLs exactly like a real browser would, then hand back a response object you can make assertions about:

from django.test import TestCase

class TaskListViewTests(TestCase):
    def test_task_list_returns_200(self):
        response = self.client.get('/tasks/')
        self.assertEqual(response.status_code, 200)

response.status_code is the same HTTP status code a real browser would receive — 200 meaning the request succeeded and a page was returned. No server process was started to produce this; Django routes the request through its own URL resolver and view logic internally, in-process.

Checking the Response Actually Contains What It Should

A 200 status code only proves a page loaded — not that it loaded the right content. assertContains() checks that a specific piece of text actually appears somewhere in the rendered response body:

    def test_task_list_shows_existing_task(self):
        Task.objects.create(title='Write report')
        response = self.client.get('/tasks/')
        self.assertContains(response, 'Write report')

This is a meaningfully stronger check than the status code alone — it confirms the task actually made it from the database, through the view, through the template, into the HTML the client would see.

Testing a View That Requires Login

User Authentication covered protecting a view with @login_required. Testing that protection properly means checking two things: an anonymous request gets redirected away, and a logged-in request goes through normally.

from django.contrib.auth.models import User

class TaskCreateViewAuthTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username='sonu', password='testpass123')

    def test_anonymous_user_is_redirected_to_login(self):
        response = self.client.get('/tasks/new/')
        self.assertEqual(response.status_code, 302)
        self.assertIn('/login/', response.url)

    def test_logged_in_user_can_access_create_page(self):
        self.client.login(username='sonu', password='testpass123')
        response = self.client.get('/tasks/new/')
        self.assertEqual(response.status_code, 200)

302 is an HTTP redirect — exactly what @login_required sends an anonymous visitor towards the login page. self.client.login() simulates actually signing in as that user for the rest of the test; every subsequent request from self.client in that test method now carries an authenticated session, so the same view that redirected before now returns 200.

Testing a POST Request That Creates a Task

Forms matter just as much as read-only pages. This test submits TaskFlow's task-creation form exactly like a browser would, then checks the database directly to confirm the Task was actually created — not just that the response looked successful:

    def test_post_creates_new_task(self):
        self.client.login(username='sonu', password='testpass123')
        self.client.post('/tasks/new/', {
            'title': 'Prepare client demo',
            'priority': 'high',
        })
        self.assertTrue(
            Task.objects.filter(title='Prepare client demo').exists()
        )

self.client.post() takes the URL and a dictionary standing in for submitted form fields — exactly what a browser would send from an HTML form with those name attributes. The assertion afterward doesn't inspect the response at all; it queries the (transaction-scoped) test database directly, which is the most reliable way to confirm a write actually happened.

Testing the DRF API with APITestCase

The API endpoints built across the DRF lessons — including the ViewSets from DRF: Views & ViewSets — deserve the same treatment. Django REST Framework ships its own APITestCase, a thin extension of Django's TestCase with a client that understands JSON responses more conveniently:

from rest_framework.test import APITestCase

class TaskApiTests(APITestCase):
    def test_task_list_endpoint_returns_expected_shape(self):
        Task.objects.create(title='Write report')
        response = self.client.get('/api/tasks/')

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.data[0]['title'], 'Write report')

response.data is a DRF convenience — the already-parsed JSON body, so there's no need to manually decode it. Checking a field like title confirms not just that the endpoint responded, but that the serializer shaped the data the way callers of the API actually expect.

An Honest Note on Coverage

It's tempting to chase 100% test coverage as a number to hit, but that's rarely where testing effort is best spent. Focus first on the paths where a bug is genuinely costly: anything gated by authentication, anything that writes or deletes data, anything involving permissions or ownership checks. A thoroughly tested login flow and task-creation form is worth far more than untested edge cases padded out just to raise a coverage percentage.

Note: Every test shown here is correct, runnable Django/DRF code — but this website's own project doesn't run a live test suite or server, so these requests and responses aren't executed on this page.
Next up: Caching looks at speeding up TaskFlow's slower views and queries once tests give you the confidence to change things without breaking them.