Testing Django Apps
TestCase, the test client, and a complete test covering views, auth, and a DRF endpoint.
Why Django's testing tools exist
Django's test framework builds on Python's standard unittest, adding Django-specific conveniences: a test database created and destroyed automatically for each test run, a TestCase base class that wraps each test in a transaction it rolls back afterward (so tests never see each other's data), and a Client that simulates real HTTP requests against your app without needing a running server.
TestCase and the test database
# blog/tests.py
from django.test import TestCase
from .models import Author, Post
class PostModelTests(TestCase):
def setUp(self):
self.author = Author.objects.create(name="Ada Lovelace", email="ada@example.com")
def test_post_str_returns_title(self):
post = Post.objects.create(title="Hello, Django", body="...", author=self.author)
self.assertEqual(str(post), "Hello, Django")
def test_default_published_is_false(self):
post = Post.objects.create(title="Draft", body="...", author=self.author)
self.assertFalse(post.published)
python3 manage.py test
manage.py test creates a fresh test database (by default, test_<your db name>), runs every discovered TestCase, and destroys it afterward — your real development database is never touched. Each individual test method inside a TestCase runs inside its own database transaction that's rolled back once the test finishes, so setUp()'s self.author never leaks into the next test.
The test client: a complete view test
Client simulates a browser making real requests — GET, POST, following redirects — against your URL configuration, letting you test a whole view (routing, permissions, template, database effects) in one assertion-driven test:
# blog/tests.py
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Author, Post
class PostListViewTests(TestCase):
def setUp(self):
self.author = Author.objects.create(name="Ada Lovelace", email="ada@example.com")
Post.objects.create(title="Published Post", body="...", author=self.author, published=True)
Post.objects.create(title="Draft Post", body="...", author=self.author, published=False)
def test_post_list_shows_only_published_posts(self):
response = self.client.get(reverse("post_list"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Published Post")
self.assertNotContains(response, "Draft Post")
class PostCreateViewTests(TestCase):
def setUp(self):
self.user = User.objects.create_user(username="ada", password="s3cret-pass")
def test_anonymous_user_is_redirected_to_login(self):
response = self.client.get(reverse("post_create"))
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login/", response.url)
def test_logged_in_user_can_create_a_post(self):
self.client.login(username="ada", password="s3cret-pass")
response = self.client.post(reverse("post_create"), {
"title": "New Post",
"body": "Some content.",
"published": True,
})
self.assertEqual(response.status_code, 302) # redirected after a successful create
self.assertTrue(Post.objects.filter(title="New Post").exists())
self.client is a Client instance every TestCase gets automatically. reverse("post_list") resolves a named URL the same way {% url %} does in a template, so a test never hardcodes a literal path that could drift out of sync with urls.py. self.client.login(...) authenticates the test client exactly like a real login form would, letting test_logged_in_user_can_create_a_post exercise the @login_required path.
Testing a DRF endpoint
DRF ships its own APIClient (a thin Client subclass) that also understands things like sending JSON bodies and API authentication directly:
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth.models import User
class PostAPITests(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username="ada", password="s3cret-pass")
def test_creating_a_post_requires_authentication(self):
response = self.client.post("/api/posts/", {"title": "New", "body": "..."})
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_authenticated_user_can_create_a_post(self):
self.client.force_authenticate(user=self.user)
response = self.client.post("/api/posts/", {"title": "New", "body": "...", "author": self.user.id})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
force_authenticate() is a DRF testing shortcut that skips the real authentication flow entirely (no real token or session needed) and simply attaches the given user to the request — the standard way to test an authenticated endpoint without the overhead of generating and passing a real token in every test.
Common mistakes
- Using
unittest.TestCasedirectly instead of Django'sdjango.test.TestCase— the plain version has no automatic per-test transaction rollback or test database wiring, so data from one test can leak into the next. - Hardcoding literal URLs (
"/blog/posts/1/") in tests instead ofreverse("post_detail", args=[1])— a later change tourls.pysilently breaks every test relying on the old path. - Forgetting that
self.client.get()/.post()runs through the entire request/response cycle, including middleware and permission checks — an unexpected302or403in a test is usually a real, correctly-enforced restriction, not a testing quirk.
Interview questions
Q: What does Django's TestCase do differently from plain unittest.TestCase?
It wraps each test method in a database transaction that's automatically rolled back once the test finishes, so tests never see data left behind by another test, and it manages creating and destroying a dedicated test database around the whole test run. Plain unittest.TestCase has no awareness of Django's database at all — you'd have to manage isolation and cleanup yourself.
Q: What does the Django test Client let you verify that calling a view function directly can't?
It sends a real, full HTTP request through Django's actual URL routing, middleware stack, and permission checks — not just the view function's return value in isolation. That means a test using self.client.get(reverse(...)) also verifies the URL is wired up correctly, that @login_required (or any other middleware) behaves as expected, and that the rendered template actually contains the expected content — a broader, more realistic check than calling the view function directly with a fake request object.