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 oron_commitcallbacks (TestCase.captureOnCommitCallbacksoften 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.