Django có sẵn 3 lớp test khác nhau ở cách xử lý DB và mức độ isolation.
TestCase wrap mỗi test trong transaction.atomic() rồi rollback khi xong — DB sạch giữa các test, rất nhanh, là lựa chọn mặc định. TransactionTestCase không wrap transaction nên mỗi test commit/rollback thật — chậm hơn, nhưng bắt buộc khi test logic dùng transaction.on_commit, select_for_update, hoặc nhiều DB connection. SimpleTestCase không cho phép DB access — chỉ test pure logic (template, util, form không save). Client là fake HTTP client để gọi view như request thật, không cần chạy server.
from django.test import TestCase, Client
from django.urls import reverse
class PostViewTest(TestCase):
@classmethod
def setUpTestData(cls): # chạy 1 lần cho cả class
cls.user = User.objects.create_user(email='a@b.com', password='x')
cls.post = Post.objects.create(title='Hi', author=cls.user)
def setUp(self): # chạy mỗi test
self.client = Client()
self.client.force_login(self.user)
def test_detail_page(self):
url = reverse('blog:post-detail', kwargs={'slug': self.post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Hi')Project lớn hay dùng pytest-django thay vì stdlib test — fixture mạnh hơn, có @pytest.mark.django_db và client fixture sẵn.
Một bẫy nhỏ: setUpTestData chạy 1 lần per class (nhanh) nhưng object trả về shared giữa các test — test nào mutate object thì phải đổi sang setUp. Và signal post_save vẫn fire trong test; nếu signal gọi email/API thật thì mock bằng @override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') hoặc unittest.mock.patch.
Django ships three test base classes that differ in DB handling and isolation:
TestCase— wraps each test intransaction.atomic()then rolls back on exit → clean DB between tests, very fast. Default choice.TransactionTestCase— no transaction wrap → each test really commits/rolls back → slower, but required when testingtransaction.on_commit,select_for_update, or multiple DB connections.SimpleTestCase— no DB access allowed; only for pure logic (templates, utils, forms without saving).Client— fake HTTP client that calls views as if they were real requests, no server needed.
from django.test import TestCase, Client
from django.urls import reverse
class PostViewTest(TestCase):
@classmethod
def setUpTestData(cls): # runs once per class
cls.user = User.objects.create_user(email='a@b.com', password='x')
cls.post = Post.objects.create(title='Hi', author=cls.user)
def setUp(self): # runs per test
self.client = Client()
self.client.force_login(self.user)
def test_detail_page(self):
url = reverse('blog:post-detail', kwargs={'slug': self.post.slug})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Hi')Larger projects use pytest-django — more powerful fixtures, @pytest.mark.django_db, ready client fixture.
Pitfall: setUpTestData runs once per class (fast) but the returned objects are shared between tests — if a test mutates them, use setUp instead. And post_save signals still fire in tests; if they call email/external APIs, mock with @override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') or unittest.mock.patch.