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

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.

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 errored

2 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:

# 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 errored

3 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 assertEqual

Common 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

ToolWhat It Does
TestCaseBase class for tests; wraps each test in a rolled-back transaction
setUpRuns before every test method to build fresh test data
assertEqual(a, b)Fails the test unless a equals b
self.clientTest Client that simulates requests like self.client.get(url)
python manage.py testBuilds 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

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.