Testing Django
Reviewed & published by Brayan K
Testing in Django means writing automated checks that confirm your application behaves correctly — using the built-in TestCase class, which spins up a temporary database, lets you create test data, and provides a test Client to simulate requests so you can assert that models, views, and forms work as expected.
Part of the free Django course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll write your first test cases, exercise views through the test Client, run the suite with manage.py, and learn how Django isolates every test inside its own throwaway database.
What You'll Learn in This Lesson
- How django.test.TestCase builds on Python's unittest
- Using setUp() to create reusable test data
- Assertions like assertEqual, assertTrue, and assertContains
- Testing views with the self.client test Client
- Checking status codes and assertTemplateUsed
- Running the suite with python manage.py test and testing forms
1 TestCase, setUp(), and Assertions
Django's testing tools live in django.test. The workhorse is django.test.TestCase, which subclasses Python's built-in unittest.TestCase. That inheritance means every familiar assertion — assertEqual, assertTrue, assertFalse — is available, plus web-specific ones like assertContains.
A test class groups related checks. Each method whose name starts with test_ is run automatically. The special setUp method runs before every test method, so it's the perfect place to create the data each test needs.
- setUp — runs before each test to build fresh fixtures.
- assertEqual(a, b) — passes when a equals b.
- assertTrue(x) — passes when x is truthy.
- assertContains(response, text) — passes when a response body includes text.
Here is what a real Django model test looks like. It creates a Post in setUp and checks that the model's string representation returns its title:
# blog/tests.py
from django.test import TestCase
from blog.models import Post
class PostModelTest(TestCase):
def setUp(self):
# Runs before EVERY test method below
self.post = Post.objects.create(title="Hello", body="First post")
def test_str_returns_title(self):
self.assertEqual(str(self.post), "Hello")
def test_body_is_saved(self):
self.assertTrue(self.post.body)
self.assertEqual(self.post.body, "First post")The example below runs the same API using Python's real unittest module — which Django's TestCase is built on — so you can watch setUp and the assertions execute right here in the editor.
# Django's TestCase inherits from Python's unittest.TestCase, so the
# assertion API is identical. We model a tiny "Post" with plain Python
# so this runs without a database.
import io
import sys
import unittest
class Post:
def __init__(self, title, body):
self.title = title
self.body = body
def __str__(self):
return self.title
class PostModelTest(unittest.TestCase):
def setUp(self):
# setUp runs before EACH test_ method, just like in Django
self.post = Post(title="Hello", body="First post")
def test_str_returns_title(self):
self.assertEqual(str(self.post), "Hello")
def test_body_is_saved(self):
self.assertTrue(self.post.body)
self.assertEqual(self.post.body, "First post")
# A manual runner so the output is easy to read here
# unittest writes its report to STDERR, not stdout, so nothing shows up here
# unless you redirect it. Capturing it also lets us drop the one line that is
# not the same every run — "Ran N tests in 0.000s" is a stopwatch reading, and
# on a busy machine it reads 0.006s instead.
report = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
result = unittest.TextTestRunner(stream=report, verbosity=2).run(suite)
for line in report.getvalue().splitlines():
if not line.startswith("Ran "):
print(line)
print(f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errored")
# ✅ Expected output:
# test_body_is_saved (__main__.PostModelTest.test_body_is_saved) ... ok
# test_str_returns_title (__main__.PostModelTest.test_str_returns_title) ... ok
#
# ----------------------------------------------------------------------
#
# OK
# 2 run, 0 failed, 0 errored2 Testing Views with the Test Client
Every Django TestCase gives you a ready-made test Client at self.client. It simulates a browser without starting a real server: you call self.client.get(url) and get back a response object you can inspect — its status_code, its rendered content, and which template produced it.
Always build the URL with reverse() instead of hard-coding a path, so your tests keep working even if you rename a route. Useful response checks include:
- response.status_code — 200 means OK, 404 means not found.
- assertContains(response, text) — the page body includes text.
- assertTemplateUsed(response, name) — the right template rendered.
# blog/tests.py
from django.test import TestCase
from django.urls import reverse
from blog.models import Post
class PostListViewTest(TestCase):
def setUp(self):
Post.objects.create(title="Hello", body="First post")
def test_list_view_works(self):
url = reverse("post_list") # build URL by name
response = self.client.get(url) # simulate a GET request
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hello")
self.assertTemplateUsed(response, "blog/post_list.html")The runnable version below models a minimal "client" and "view" in plain Python so you can see how a response's status code and body are asserted — exactly the pattern Django uses.
# Mimic Django's test Client + a view, in plain Python, so the
# response/status_code/assertContains pattern runs here.
import io
import sys
import unittest
class FakeResponse:
def __init__(self, status_code, content):
self.status_code = status_code
self.content = content
def post_list_view(request_path):
# Pretend this view rendered a template listing one post
if request_path == "/posts/":
return FakeResponse(200, "<h1>Hello</h1>")
return FakeResponse(404, "Not Found")
class FakeClient:
def get(self, path):
return post_list_view(path)
class PostListViewTest(unittest.TestCase):
def setUp(self):
self.client = FakeClient()
def test_list_view_works(self):
response = self.client.get("/posts/")
self.assertEqual(response.status_code, 200)
# assertContains checks the body — here we do it by hand
self.assertIn("Hello", response.content)
def test_missing_page_is_404(self):
response = self.client.get("/nope/")
self.assertEqual(response.status_code, 404)
# unittest writes its report to STDERR, not stdout, so nothing shows up here
# unless you redirect it. Capturing it also lets us drop the one line that is
# not the same every run — "Ran N tests in 0.000s" is a stopwatch reading, and
# on a busy machine it reads 0.006s instead.
report = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
result = unittest.TextTestRunner(stream=report, verbosity=2).run(suite)
for line in report.getvalue().splitlines():
if not line.startswith("Ran "):
print(line)
print(f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errored")
# ✅ Expected output:
# test_list_view_works (__main__.PostListViewTest.test_list_view_works) ... ok
# test_missing_page_is_404 (__main__.PostListViewTest.test_missing_page_is_404) ... ok
#
# ----------------------------------------------------------------------
#
# OK
# 2 run, 0 failed, 0 errored3 Running Tests and Testing Forms
You run your whole suite with one command. Django discovers every file named tests.py (or any module inside a tests/ package), creates a fresh test database, runs the tests, prints the results, and tears the database down again.
# Run every test in the project
python manage.py test
# Run only the blog app's tests
python manage.py test blog
# Run a single test method (fast feedback while coding)
python manage.py test blog.tests.PostModelTest.test_str_returns_title
# Typical output:
# Creating test database for alias 'default'...
# ...
# ----------------------------------------------------------------------
# Ran 3 tests in 0.012s
#
# OK
# Destroying test database for alias 'default'...Forms are great to test because they hold validation rules. You build a form with a data dictionary and assert whether it is valid. Here is a real Django form test:
# blog/tests.py
from django.test import TestCase
from blog.forms import PostForm
class PostFormTest(TestCase):
def test_valid_data_passes(self):
form = PostForm(data={"title": "Hi", "body": "Some text"})
self.assertTrue(form.is_valid())
def test_blank_title_fails(self):
form = PostForm(data={"title": "", "body": "Some text"})
self.assertFalse(form.is_valid())
self.assertIn("title", form.errors)The runnable example below recreates that validation logic in plain Python so you can see is_valid being asserted both ways:
# A tiny form with validation, tested the Django way (using unittest).
import io
import sys
import unittest
class PostForm:
def __init__(self, data):
self.data = data
self.errors = {}
def is_valid(self):
self.errors = {}
if not self.data.get("title"):
self.errors["title"] = "This field is required."
if not self.data.get("body"):
self.errors["body"] = "This field is required."
return len(self.errors) == 0
class PostFormTest(unittest.TestCase):
def test_valid_data_passes(self):
form = PostForm(data={"title": "Hi", "body": "Some text"})
self.assertTrue(form.is_valid())
def test_blank_title_fails(self):
form = PostForm(data={"title": "", "body": "Some text"})
self.assertFalse(form.is_valid())
self.assertIn("title", form.errors)
# unittest writes its report to STDERR, not stdout, so nothing shows up here
# unless you redirect it. Capturing it also lets us drop the one line that is
# not the same every run — "Ran N tests in 0.000s" is a stopwatch reading, and
# on a busy machine it reads 0.006s instead.
report = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
result = unittest.TextTestRunner(stream=report, verbosity=2).run(suite)
for line in report.getvalue().splitlines():
if not line.startswith("Ran "):
print(line)
print(f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errored")
# ✅ Expected output:
# test_blank_title_fails (__main__.PostFormTest.test_blank_title_fails) ... ok
# test_valid_data_passes (__main__.PostFormTest.test_valid_data_passes) ... ok
#
# ----------------------------------------------------------------------
#
# OK
# 2 run, 0 failed, 0 errored🎯 Your Turn: Finish the Assertion
Fill in the blank with the assertion method that checks two values are equal, so the test passes.
# 🎯 YOUR TURN: replace ___ with the assertion method that
# checks that two values are EQUAL (hint: assertEqual)
import io
import sys
import unittest
def double(n):
return n * 2
class DoubleTest(unittest.TestCase):
def test_double_of_three(self):
result = double(3)
# Replace ___ below with the correct assertion method name
self.___(result, 6)
# unittest writes its report to STDERR, not stdout, so nothing shows up here
# unless you redirect it. Capturing it also lets us drop the one line that is
# not the same every run — "Ran N tests in 0.000s" is a stopwatch reading, and
# on a busy machine it reads 0.006s instead.
report = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
result = unittest.TextTestRunner(stream=report, verbosity=2).run(suite)
for line in report.getvalue().splitlines():
if not line.startswith("Ran "):
print(line)
print(f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errored")
# EXPECTED: the test passes (OK) once you use assertEqualCommon Errors (And How to Fix Them)
❌ My test never runs (it's silently skipped)
Test methods must start with the prefix test_. A method named check_title is invisible to the test runner.
✅ Fix: rename it to test_check_title so Django discovers and runs it.
❌ Assuming my dev data is there during tests
Tests run against a brand-new, empty test database — not your real one. Objects you created in development do not exist.
✅ Fix: create everything a test needs inside setUp or the test method itself.
❌ NoReverseMatch when building a URL
You hard-coded a path or passed the wrong name to reverse(), so the URL can't be resolved.
✅ Fix: use reverse("the_named_url") with the exact name from your URLconf.
📋 Quick Reference
| Tool | What It Does |
|---|---|
| TestCase | Base class for tests; wraps each test in a rolled-back transaction |
| setUp | Runs before every test method to build fresh test data |
| assertEqual(a, b) | Fails the test unless a equals b |
| self.client | Test Client that simulates requests like self.client.get(url) |
| python manage.py test | Builds the test database, runs the suite, then destroys it |
🏆 Mini Challenge: Test a Discount Calculator
Write a test class with setUp and two test_ methods that verify a discount function. Use assertEqual and assertTrue just like a Django TestCase would.
# 🏆 Mini Challenge: test a discount calculator the Django way.
import io
import sys
import unittest
def apply_discount(price, percent):
return round(price - (price * percent / 100), 2)
class DiscountTest(unittest.TestCase):
def setUp(self):
# Shared fixture for every test below
self.base_price = 100.0
def test_ten_percent_off(self):
result = apply_discount(self.base_price, 10)
self.assertEqual(result, 90.0)
def test_discount_is_cheaper(self):
result = apply_discount(self.base_price, 25)
self.assertTrue(result < self.base_price)
# unittest writes its report to STDERR, not stdout, so nothing shows up here
# unless you redirect it. Capturing it also lets us drop the one line that is
# not the same every run — "Ran N tests in 0.000s" is a stopwatch reading, and
# on a busy machine it reads 0.006s instead.
report = io.StringIO()
suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
result = unittest.TextTestRunner(stream=report, verbosity=2).run(suite)
for line in report.getvalue().splitlines():
if not line.startswith("Ran "):
print(line)
print(f"{result.testsRun} run, {len(result.failures)} failed, {len(result.errors)} errored")
# EXPECTED: both tests pass — output ends with "OK"❓ Frequently Asked Questions
Lesson complete — your code now has a safety net!
You can write tests with TestCase and setUp, assert with assertEqual, assertTrue, and assertContains, exercise views through self.client, run everything with python manage.py test against an isolated test database, and validate forms with is_valid().
Practice quiz
Which class does Django's django.test.TestCase build on?
- pytest.TestCase
- django.db.Model
- Python's unittest.TestCase
- django.views.View
Answer: Python's unittest.TestCase. Django's TestCase subclasses Python's unittest.TestCase, so all its assertions are available.
When does the setUp method run?
- Before every test method in the class
- Once before the whole class
- Only after a test fails
- Once at the end of the run
Answer: Before every test method in the class. setUp runs before each test_ method, making it the place to build fresh fixtures.
Which prefix must a method name have for the test runner to discover it?
- check_
- test_
- assert_
- run_
Answer: test_. Only methods whose names start with test_ are run automatically.
What does self.client provide in a Django TestCase?
- A database connection
- A reference to the current request
- A logging handler
- A test Client that simulates requests without a running server
Answer: A test Client that simulates requests without a running server. self.client is a test Client you use like self.client.get(url) to exercise views.
Which assertion checks that a response body contains some text?
- assertEqual(response, text)
- assertContains(response, text)
- assertBody(response, text)
- assertIn(response, text)
Answer: assertContains(response, text). assertContains(response, text) passes when the rendered response includes that text.
Why use reverse() instead of hard-coding a path in a test?
- The test keeps working even if you rename the route
- It runs the test faster
- reverse() creates the test database
- Hard-coded paths are not allowed by Python
Answer: The test keeps working even if you rename the route. Building the URL by name with reverse() keeps tests working when paths change.
What does running python manage.py test do to the database?
- It runs tests against your production database
- It modifies your development data in place
- It creates a fresh test database, runs tests, then destroys it
- It requires you to create the test database by hand
Answer: It creates a fresh test database, runs tests, then destroys it. Django builds a throwaway test database, runs the suite, and tears it down afterward.
Which assertion verifies the expected template rendered a response?
- assertEqual(response.template, name)
- assertContains(response, name)
- assertTemplate(name)
- assertTemplateUsed(response, name)
Answer: assertTemplateUsed(response, name). assertTemplateUsed(response, name) confirms the named template produced the response.
How do you run only one test method from the command line?
- Pass its dotted path, e.g. python manage.py test app.tests.MyTest.test_one
- Pass --only test_one
- You cannot — the whole suite always runs
- Set RUN_SINGLE=True in settings
Answer: Pass its dotted path, e.g. python manage.py test app.tests.MyTest.test_one. A dotted path like app.tests.ClassName.test_method runs just that one test.
A test you wrote is silently skipped. What is the most likely cause?
- assertEqual is misspelled
- setUp raised an exception
- The method name does not start with test_
- The test database was not created
Answer: The method name does not start with test_. Methods that do not start with test_ are invisible to the runner and never execute.
Continue this course
- Previous: Middleware
- Next: Migrations Deep Dive — Understand migration files, data migrations, and the migration graph
- Quick reference: Django cheat sheet
Frequently asked questions
Why should I write tests for a Django project?
Tests give you a safety net. As your project grows, a small change in one view or model can quietly break something elsewhere. Automated tests catch those regressions instantly so you find bugs before your users do. They also document how your code is supposed to behave, make refactoring far less scary, and let teams ship faster because everyone trusts that the test suite will flag mistakes.
What is the difference between TestCase and unittest.TestCase?
Django's django.test.TestCase subclasses Python's built-in unittest.TestCase, so you get all the familiar assertion methods like assertEqual and assertTrue. On top of that, Django's version wraps each test in a database transaction that is rolled back afterward, gives you a self.client test Client to simulate requests, and adds web-specific assertions such as assertContains and assertTemplateUsed. Use Django's TestCase whenever your test touches the database, the ORM, views, or templates.
Does running tests touch my real database?
No. When you run python manage.py test, Django creates a brand-new, empty test database, runs your tests against it, and then destroys it when the run finishes. Your development and production data are never touched. Each test method also runs inside a transaction that is rolled back, so tests stay isolated from one another and cannot leave stray data behind.
How do I run just one test instead of the whole suite?
Pass a dotted path to python manage.py test. For example, python manage.py test blog runs every test in the blog app, python manage.py test blog.tests.PostModelTest runs one test class, and python manage.py test blog.tests.PostModelTest.test_str_returns_title runs a single method. Narrowing the run like this keeps feedback fast while you focus on one piece of code.