Difficulty: Advanced
Django's test framework is built on Python's unittest module and provides additional features for testing web applications. The primary class is django.test.TestCase, which wraps each test in a database transaction and rolls it back after the test completes, ensuring tests are isolated and do not affect each other.
Django provides several test classes. SimpleTestCase is for tests that do not need database access (testing utilities, template rendering, URL resolving). TestCase is the most common - it provides database access with transaction rollback for isolation. TransactionTestCase actually commits transactions to the database, required for testing transaction-related behavior. LiveServerTestCase starts a real Django server for testing with browser automation tools like Selenium.
The test client (self.client) simulates a web browser, allowing you to make GET, POST, PUT, and DELETE requests without running a real server. It handles cookies, sessions, and redirects. The response object contains status_code, content (raw bytes), context (template context), and other attributes for assertions.
Django's TestCase provides enhanced assertions beyond unittest: assertContains (checks response content), assertNotContains, assertRedirects (checks redirect chain), assertTemplateUsed (checks which template was rendered), assertFormError (checks form validation errors), assertQuerysetEqual (compares QuerySets), and assertNumQueries (checks the number of database queries).
The setUp() method runs before each test method, creating fresh test data. setUpTestData() is a class method that runs once per test class - use it for data that is not modified by tests, as it is more efficient. tearDown() runs after each test for cleanup, though transaction rollback usually handles database cleanup automatically.
Test discovery is automatic: Django finds all files matching test*.py in each app. You can organize tests into a tests/ directory with separate files like test_models.py, test_views.py, test_forms.py. Run tests with 'python manage.py test', which creates a temporary test database, runs migrations, executes tests, and destroys the database.
from django.test import TestCase
from django.contrib.auth import get_user_model
from .models import Article, Category
User = get_user_model()
class ArticleModelTest(TestCase):
@classmethod
def setUpTestData(cls):
"""Set up data for the whole test class (runs once)."""
cls.user = User.objects.create_user(
username='testuser',
password='testpass123',
)
cls.category = Category.objects.create(name='Python')
def test_create_article(self):
article = Article.objects.create(
title='Test Article',
content='Test content',
author=self.user,
category=self.category,
)
self.assertEqual(article.title, 'Test Article')
self.assertEqual(article.author, self.user)
self.assertFalse(article.is_published)
def test_str_representation(self):
article = Article.objects.create(
title='My Article',
content='Content',
author=self.user,
)
self.assertEqual(str(article), 'My Article')
def test_slug_auto_generation(self):
article = Article.objects.create(
title='Hello World Article',
content='Content',
author=self.user,
)
self.assertEqual(article.slug, 'hello-world-article')
def test_article_ordering(self):
a1 = Article.objects.create(title='First', content='c', author=self.user)
a2 = Article.objects.create(title='Second', content='c', author=self.user)
articles = list(Article.objects.all())
self.assertEqual(articles[0], a2) # Newest first
setUpTestData creates shared data once for all tests in the class. Each test method tests one specific behavior. assertEqual, assertFalse, and other assertions verify expected outcomes.
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model
from .models import Article
User = get_user_model()
class ArticleViewTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser', password='testpass123'
)
self.article = Article.objects.create(
title='Test Article',
content='Test content',
author=self.user,
is_published=True,
)
def test_article_list_view(self):
url = reverse('blog:article-list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'blog/article_list.html')
self.assertContains(response, 'Test Article')
self.assertEqual(len(response.context['articles']), 1)
def test_article_detail_view(self):
url = reverse('blog:article-detail', kwargs={'pk': self.article.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Article')
def test_article_detail_404(self):
url = reverse('blog:article-detail', kwargs={'pk': 9999})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
def test_create_requires_login(self):
url = reverse('blog:article-create')
response = self.client.get(url)
self.assertRedirects(response, f'/accounts/login/?next={url}')
def test_create_article(self):
self.client.login(username='testuser', password='testpass123')
url = reverse('blog:article-create')
data = {'title': 'New Article', 'content': 'New content'}
response = self.client.post(url, data)
self.assertEqual(response.status_code, 302)
self.assertTrue(Article.objects.filter(title='New Article').exists())
self.client simulates HTTP requests. reverse() generates URLs from names. assertTemplateUsed checks the template. assertContains checks response content. self.client.login() authenticates for protected views.
from django.test import TestCase
from .forms import ArticleForm, RegistrationForm
class ArticleFormTest(TestCase):
def test_valid_form(self):
data = {
'title': 'Test Article',
'content': 'This is test content for the article.',
'category': self.category.pk,
}
form = ArticleForm(data=data)
self.assertTrue(form.is_valid())
def test_blank_title(self):
data = {'title': '', 'content': 'Content'}
form = ArticleForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_title_max_length(self):
data = {'title': 'x' * 201, 'content': 'Content'}
form = ArticleForm(data=data)
self.assertFalse(form.is_valid())
class RegistrationFormTest(TestCase):
def test_password_mismatch(self):
data = {
'username': 'newuser',
'email': 'new@example.com',
'password': 'securepass123',
'confirm_password': 'differentpass',
}
form = RegistrationForm(data=data)
self.assertFalse(form.is_valid())
self.assertIn('Passwords do not match', str(form.errors))
Form tests verify validation logic. Create a form with data, call is_valid(), and check the result and errors. Test both valid and invalid inputs, including edge cases like blank required fields and maximum lengths.
# Run all tests
# python manage.py test
# Run tests for a specific app
# python manage.py test blog
# Run a specific test class
# python manage.py test blog.tests.ArticleModelTest
# Run a specific test method
# python manage.py test blog.tests.ArticleModelTest.test_create_article
# Run with verbosity
# python manage.py test -v 2
# Run with parallel execution
# python manage.py test --parallel
# Keep the test database for faster re-runs
# python manage.py test --keepdb
# Test directory structure:
# blog/
# tests/
# __init__.py
# test_models.py
# test_views.py
# test_forms.py
# test_api.py
Django's test runner discovers test*.py files automatically. You can run all tests or narrow down to specific apps, classes, or methods. --parallel speeds up test execution. --keepdb reuses the test database.
TestCase, Client, setUp, assertions, SimpleTestCase, TransactionTestCase