Django from Basics to Production · APIs with Django REST Framework, and Production

Testing Django Apps

Write fast tests with TestCase, test views through the client, assert query counts, and keep test data in setUpTestData.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A test case for a product list endpoint: test data created once in setUpTestData, a response test, a permission test and a query-count test that fails if someone introduces an N+1 query. The tests run with Django's test runner on a fresh test database.

The idea

  • TestCase wraps each test in a transaction rolled back afterwards: fast and isolated.
  • setUpTestData creates data once per class instead of per test.
  • self.client sends requests through the full stack; assertNumQueries pins the number of SQL queries.
  • python manage.py test creates a test database, runs tests and destroys it.

The code

TestCase with client and query-count assertions
import sys, types
import django
from django.conf import settings

sys.modules["shop"] = types.ModuleType("shop")       # lets this single file act as the "shop" app
sys.modules["shop"].__file__ = __file__
settings.configure(
    SECRET_KEY="dev-only", DEBUG=False, ALLOWED_HOSTS=["testserver"], ROOT_URLCONF=__name__,
    DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
    INSTALLED_APPS=["django.contrib.contenttypes", "django.contrib.auth", "rest_framework", "shop"],
)
django.setup()

import unittest
from django.db import connection, models
from django.http import JsonResponse
from django.test import TestCase
from django.test.utils import get_runner
from django.urls import path

class Category(models.Model):
    name = models.CharField(max_length=30)
    class Meta: app_label = "shop"

class Product(models.Model):
    name = models.CharField(max_length=50)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    class Meta: app_label = "shop"

def products(request):
    rows = Product.objects.select_related("category").order_by("id")
    return JsonResponse({"items": [f"{p.name} ({p.category.name})" for p in rows]})

urlpatterns = [path("products/", products)]

class ProductListTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        kitchen = Category.objects.create(name="kitchen")
        for name in ["Mug", "Teapot", "Kettle"]:
            Product.objects.create(name=name, category=kitchen)

    def test_lists_products_with_category(self):
        r = self.client.get("/products/")
        self.assertEqual(r.status_code, 200)
        self.assertEqual(r.json()["items"][0], "Mug (kitchen)")

    def test_one_query_regardless_of_rows(self):
        with self.assertNumQueries(1):
            self.client.get("/products/")

runner = get_runner(settings)(verbosity=0)
old = runner.setup_databases()                       # what manage.py test does: a fresh test database
with connection.schema_editor() as editor:           # (a project's migrations would create these tables)
    editor.create_model(Category); editor.create_model(Product)
result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(unittest.defaultTestLoader.loadTestsFromTestCase(ProductListTests))
runner.teardown_databases(old)

Output

test_lists_products_with_category (__main__.ProductListTests.test_lists_products_with_category) ... ok
test_one_query_regardless_of_rows (__main__.ProductListTests.test_one_query_regardless_of_rows) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.002s

OK

Run this on your computer — see “Get Started” for setup.

Common mistakes

  • Creating test data in setUp for every test when it never changes (slow suites).
  • Tests that only check status codes, not the response content.
  • No query-count tests, so N+1 regressions reach production unnoticed.

Key points

  • TestCase isolates tests with transactions; setUpTestData shares fixtures cheaply.
  • Test through the client and assert content.
  • assertNumQueries guards performance.

Further reading: Testing in Django · Testing tools

Practice questions

What does Django's TestCase do between tests?

Answer

It wraps each test in a transaction and rolls it back, so tests don't see each other's changes and run fast.

When should you use setUpTestData instead of setUp?

Answer

For data that tests read but don't need fresh copies of; it's created once per class rather than before every test.

What does assertNumQueries protect against?

Answer

Unexpected extra queries, especially N+1 regressions introduced by changes to views, serializers or templates.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?