Testing Django apps

Django · Practical · 14 min

Django apps change constantly, and its test tools make full request-to-database tests fast and isolated.

python manage.py test finds tests in files named test*.py, creates a separate test database, runs them and destroys it.

Choose a base class:

  • SimpleTestCase: no database access
  • `TestCase`: wraps each test in a transaction that is rolled back, so tests are isolated and fast. The usual choice.
  • TransactionTestCase: really commits, for testing transaction behavior or on_commit callbacks (TestCase.captureOnCommitCallbacks often suffices)
  • LiveServerTestCase: runs a real server for browser tests with Playwright or Selenium

setUpTestData(cls) creates data once per class, much faster than setUp per test; each test still sees it fresh thanks to the rollback.

self.client is a test Client that makes requests through the whole stack (middleware, URLs, views, templates) without a server:

  • self.client.get(reverse("blog:list"), {"q": "x"}), .post(url, data)
  • self.client.force_login(user) skips the login form
  • assertions: assertEqual(resp.status_code, 200), assertContains(resp, "Hello"), assertRedirects, assertTemplateUsed, assertFormError
  • resp.context["posts"] gives the template context

self.assertNumQueries(n) catches N+1 regressions. @override_settings(...) changes settings for a test. RequestFactory builds requests to call a view directly, for unit tests.

Many teams use pytest-django, which runs the same Django machinery with pytest fixtures and plain assert.

Example

from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from blog.models import Author, Post

class PostViewsTests(TestCase):
    @classmethod
    def setUpTestData(cls):                       # runs once per class
        cls.user = User.objects.create_user("ann", password="pw")
        cls.author = Author.objects.create(name="Ann", user=cls.user)
        Post.objects.create(title="Hello", slug="hello", author=cls.author, status="pub")

    def test_list_shows_published_posts(self):
        with self.assertNumQueries(2):            # count + page (author via select_related)
            resp = self.client.get(reverse("blog:list"))
        self.assertContains(resp, "Hello")
        self.assertTemplateUsed(resp, "blog/post_list.html")
    def test_create_requires_login(self):
        resp = self.client.get(reverse("blog:create"))
        self.assertRedirects(resp, f"{reverse('login')}?next={reverse('blog:create')}")
    def test_logged_in_user_can_create(self):
        self.client.force_login(self.user)
        resp = self.client.post(reverse("blog:create"), {"title": "New", "slug": "new", "body": "x"})
        self.assertEqual(resp.status_code, 302)
        self.assertTrue(Post.objects.filter(slug="new", author=self.author).exists())

Remember

  • TestCase isolates each test in a rolled-back transaction
  • setUpTestData creates shared data once per class
  • self.client exercises the full stack; force_login skips login forms
  • assertNumQueries catches query regressions

Watch out for

  • Creating heavy fixtures in setUp for every test instead of setUpTestData.

Check yourself

  1. How does django.test.TestCase keep tests from affecting each other's data?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/testing/overview/