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.
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
TestCasewraps each test in a transaction rolled back afterwards: fast and isolated.setUpTestDatacreates data once per class instead of per test.self.clientsends requests through the full stack;assertNumQueriespins the number of SQL queries.python manage.py testcreates a test database, runs tests and destroys it.
The code
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
setUpfor 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
TestCaseisolates tests with transactions;setUpTestDatashares fixtures cheaply.- Test through the client and assert content.
assertNumQueriesguards 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.