Testing Django Apps

Django Intermediate/Advanced — Testing Django Apps
Django Intermediate/Advanced
Course 2 · Chapter 3 · Testing Django Apps

🧪 Testing Django Apps

The TypeScript course's Testing chapter argued that types aren't tests — the same is true here: Django's ORM and forms catch shape mistakes, but only a real test proves the actual behavior is correct. Django ships its own test runner, built on Python's unittest, with one standout feature most other stacks make you configure yourself: every test's database changes vanish automatically when the test ends.

TestCase Basics

A Django test is a class extending django.test.TestCase, with test methods starting with test_ — everything from unittest (assertEqual, assertTrue, ...) is available directly:

# catalog/tests.py from django.test import TestCase from .models import Book, Author class BookModelTests(TestCase): def setUp(self): self.author = Author.objects.create(name="Ursula K. Le Guin") def test_book_str_returns_title(self): book = Book.objects.create(title="The Dispossessed", author=self.author, price=12.99) self.assertEqual(str(book), "The Dispossessed")
python manage.py test catalog # Creating test database for alias 'default'... # . # Ran 1 test in 0.012s # OK

🔄 Automatic Per-Test Rollback

The single biggest difference from a typical FastAPI/pytest testing setup: TestCase wraps every test method in a database transaction and rolls it back the instant the test finishes — no manual cleanup, no shared state leaking between tests:

Manual Cleanup vs Automatic Rollback

A Typical Manual Setup

Create test data in a fixture/setup step, run the test, then explicitly delete or reset the rows afterward — often in a teardown function you have to remember to write and keep correct.

Django's TestCase

Every test_* method runs inside its own transaction; Django rolls it back automatically when the method returns — objects created in setUp() or the test itself are gone before the next test starts, with zero cleanup code.

setUp()

Runs before every test method in the class — the natural place for data every test in the class needs, since it's re-created fresh (and rolled back) per test.

Tests Never Pollute Each Other

Because each test's changes roll back, test order never matters and one test's leftover data can never silently affect another's assertions.

The Test Client

self.client simulates real HTTP requests against your views — no running server required:

class BookViewTests(TestCase): def setUp(self): self.author = Author.objects.create(name="Ursula K. Le Guin") self.book = Book.objects.create(title="The Dispossessed", author=self.author, price=12.99) def test_book_list_returns_200(self): response = self.client.get("/books/") self.assertEqual(response.status_code, 200) def test_book_list_shows_book_title(self): response = self.client.get("/books/") self.assertContains(response, "The Dispossessed") self.assertTemplateUsed(response, "catalog/book_list.html") def test_book_list_context_contains_books(self): response = self.client.get("/books/") self.assertIn(self.book, response.context["books"])

response.context

Direct access to the exact context dict passed to render() — a genuinely Django-specific testing convenience, letting you assert on the data a view produced, not just the rendered HTML string.

assertTemplateUsed

Confirms which template actually rendered the response — catches a view that returns the right data through the wrong template.

Fixtures

A fixture is a JSON (or YAML) file of pre-defined data, loadable into the test database — useful for larger, shared datasets, though many Django projects prefer building data directly in setUp() (as this chapter has done) for anything test-specific and easy to read at a glance:

// catalog/fixtures/sample_books.json [ { "model": "catalog.author", "pk": 1, "fields": { "name": "Ursula K. Le Guin" } }, { "model": "catalog.book", "pk": 1, "fields": { "title": "The Dispossessed", "author": 1, "price": "12.99" } } ]
class BookFixtureTests(TestCase): fixtures = ["sample_books.json"] def test_fixture_loaded(self): self.assertEqual(Book.objects.count(), 1)

Testing Forms Directly

A form (or a DRF serializer, from the last chapter) can be tested without a client or a real request at all:

from .forms import BookForm class BookFormTests(TestCase): def test_negative_price_is_invalid(self): form = BookForm(data={"title": "Test", "price": -5, "published_date": None}) self.assertFalse(form.is_valid()) self.assertIn("price", form.errors)

💻 Coding Challenges

Challenge 1: Write a Model Test

Write a TestCase for the Author model that creates an author in setUp() and asserts str(author) returns the author's name.

Goal: Practice the basic setUp() + test_* method structure.

→ Solution

Challenge 2: Write a View Test With the Test Client

Write a test that uses self.client to GET a book detail view, asserting the response status is 200, the correct template was used, and the response contains the book's title.

Goal: Practice self.client.get(), assertTemplateUsed, and assertContains together.

→ Solution

Challenge 3: Prove the Rollback Behavior

Write two test methods in the same TestCase class: the first creates a Book and asserts Book.objects.count() == 1; the second (with no setUp() creating any books) asserts Book.objects.count() == 0. Explain why the second test passes even though it runs after the first.

Goal: Practice reasoning about — and directly observing — the per-test transaction rollback.

→ Solution

⚠️ Gotcha: The Rollback Only Covers the Database

The automatic per-test cleanup is specifically a database transaction rollback — it does nothing for Django's cache framework, module-level variables, or any other in-memory state a test might touch. A test that warms the cache (covered in the next-but-one chapter) or mutates a global will leak that state into the next test regardless of TestCase's rollback. Also watch for accidentally subclassing plain unittest.TestCase instead of django.test.TestCase — the plain version has no database transaction wrapping at all, and a test that touches the ORM under it will leave real, un-rolled-back rows behind for every test that runs afterward.

🎯 What's Next

With a real test suite in place, the next chapter looks at code that runs around every request rather than inside a specific view: Middleware & Signals — Django's request/response middleware chain and the signal-dispatch system for decoupled event handling.