SkillByAIOpen interactive version →

Lesson 21 / 26

Testing Django Apps

TestCase, the test client and query counts.

Fast, isolated database tests

Django's TestCase wraps each test in a transaction that is rolled back, with setUpTestData creating shared fixtures once per class. The built-in test client performs requests against your URLs without a server, and assertNumQueries guards performance. manage.py test creates a separate test database automatically. Many teams use pytest with pytest-django for fixtures and nicer output.

Verified and hardened

Both frameworks make testing straightforward; Django adds strong security defaults and checks.

Figure 7.1 — Tests, passwords and deploy checks.

The catalog tests

I ran this with Django 6.1.1 and Python 3.12 in a demo project (shopsite with a catalog app) using SQLite. Three tests: filtering by price and stock, a 404 for a missing product, and a query-count guard.

from decimal import Decimal

from django.test import TestCase
from django.urls import reverse

from .models import Category, Product


class ProductApiTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        office = Category.objects.create(name="Office")
        Product.objects.create(category=office, name="Pen", price=Decimal("899"))
        Product.objects.create(category=office, name="Notebook", price=Decimal("120"))
        Product.objects.create(category=office, name="Lamp", price=Decimal("1499"), in_stock=False)

    def test_list_filters_by_price_and_stock(self):
        r = self.client.get(reverse("product-list"), {"max_price": "1000"})
        self.assertEqual(r.status_code, 200)
        self.assertEqual([p["name"] for p in r.json()["results"]], ["Notebook", "Pen"])

    def test_missing_product_is_404(self):
        self.assertEqual(self.client.get(reverse("product-detail", args=[999])).status_code, 404)

    def test_list_uses_two_queries_or_less(self):
        with self.assertNumQueries(1):
            self.client.get(reverse("product-list"))

Running the tests, run

I ran this with Django 6.1.1 and Python 3.12 in a demo project (shopsite with a catalog app) using SQLite. All three tests pass (timing replaced with (time)); the list view needs exactly one query thanks to select_related.

python manage.py test catalog -v 2

Output:

test_list_filters_by_price_and_stock (catalog.tests.ProductApiTests.test_list_filters_by_price_and_stock) ... ok
test_list_uses_two_queries_or_less (catalog.tests.ProductApiTests.test_list_uses_two_queries_or_less) ... ok
test_missing_product_is_404 (catalog.tests.ProductApiTests.test_missing_product_is_404) ... ok
Ran 3 tests in (time)
OK

Quick check: What does Django's TestCase do after each test?

  • Deletes the database file permanently
  • Rolls back the database changes
  • Commits changes to production
  • Restarts Python
Answer

Rolls back the database changes — Isolated tests.