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.