Django's testing framework is built on Python's standard unittest module but provides additional functionality specifically designed for web applications. Understanding Django's testing architecture and capabilities is essential for writing effective tests that ensure your application's reliability and maintainability.
Django automatically discovers tests in your applications:
# Django looks for tests in these locations:
# myapp/tests.py
# myapp/tests/__init__.py
# myapp/tests/test_*.py
# Example test structure
myproject/
├── myapp/
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ ├── test_forms.py
│ │ └── test_utils.py
│ ├── models.py
│ ├── views.py
│ └── forms.py
Django creates a separate test database for each test run:
# settings.py - Test database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'myproject_db',
'TEST': {
'NAME': 'test_myproject_db', # Test database name
'CHARSET': 'utf8',
'COLLATION': 'utf8_general_ci',
},
'USER': 'db_user',
'PASSWORD': 'db_password',
'HOST': 'localhost',
'PORT': '5432',
}
}
# For faster tests, use in-memory SQLite
if 'test' in sys.argv:
DATABASES['default'] = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:'
}
Django provides several TestCase classes for different testing needs:
The most commonly used test class with full Django features:
from django.test import TestCase
from django.contrib.auth.models import User
from myapp.models import BlogPost
class BlogPostTestCase(TestCase):
"""Test BlogPost model functionality"""
def setUp(self):
"""Set up test data before each test method"""
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.post = BlogPost.objects.create(
title='Test Post',
content='This is a test post content.',
author=self.user
)
def tearDown(self):
"""Clean up after each test method (optional)"""
# Usually not needed as Django handles cleanup
pass
def test_post_creation(self):
"""Test that blog post is created correctly"""
self.assertEqual(self.post.title, 'Test Post')
self.assertEqual(self.post.author, self.user)
self.assertIsNotNone(self.post.created_at)
def test_post_str_representation(self):
"""Test string representation of blog post"""
self.assertEqual(str(self.post), 'Test Post')
def test_post_absolute_url(self):
"""Test get_absolute_url method"""
expected_url = f'/blog/{self.post.slug}/'
self.assertEqual(self.post.get_absolute_url(), expected_url)
# Key features of django.test.TestCase:
# - Wraps each test in a database transaction
# - Rolls back transactions after each test
# - Provides Django-specific assertion methods
# - Includes test client for HTTP requests
# - Supports fixtures and factory methods
For tests that need to test database transactions:
from django.test import TransactionTestCase
from django.db import transaction
from myapp.models import Account
class TransactionTestCase(TransactionTestCase):
"""Test database transactions"""
def test_atomic_transaction(self):
"""Test atomic transaction behavior"""
# Create initial account
account = Account.objects.create(
name='Test Account',
balance=100.00
)
# Test successful transaction
with transaction.atomic():
account.balance -= 50.00
account.save()
# Create transaction record
Transaction.objects.create(
account=account,
amount=-50.00,
description='Test withdrawal'
)
# Verify transaction completed
account.refresh_from_db()
self.assertEqual(account.balance, 50.00)
def test_transaction_rollback(self):
"""Test transaction rollback on error"""
account = Account.objects.create(
name='Test Account',
balance=100.00
)
# Test failed transaction
with self.assertRaises(ValueError):
with transaction.atomic():
account.balance -= 150.00 # Overdraft
account.save()
if account.balance < 0:
raise ValueError("Insufficient funds")
# Verify rollback occurred
account.refresh_from_db()
self.assertEqual(account.balance, 100.00)
# Use TransactionTestCase when:
# - Testing transaction.atomic() behavior
# - Testing database-level constraints
# - Testing custom transaction management
# - Working with multiple databases
For pure unit tests without Django database features:
import unittest
from myapp.utils import calculate_tax, format_currency
class UtilityFunctionTests(unittest.TestCase):
"""Test utility functions without Django dependencies"""
def test_calculate_tax(self):
"""Test tax calculation function"""
# Test standard rate
self.assertEqual(calculate_tax(100.00, 0.08), 8.00)
# Test zero amount
self.assertEqual(calculate_tax(0.00, 0.08), 0.00)
# Test zero rate
self.assertEqual(calculate_tax(100.00, 0.00), 0.00)
def test_format_currency(self):
"""Test currency formatting function"""
self.assertEqual(format_currency(123.45), '$123.45')
self.assertEqual(format_currency(0), '$0.00')
self.assertEqual(format_currency(1000), '$1,000.00')
def test_format_currency_negative(self):
"""Test negative currency formatting"""
self.assertEqual(format_currency(-50.25), '-$50.25')
# Use unittest.TestCase for:
# - Pure Python functions
# - Utility classes without Django dependencies
# - Mathematical calculations
# - String processing functions
Django's test client simulates HTTP requests without running a web server:
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
class ViewTestCase(TestCase):
"""Test views using Django test client"""
def setUp(self):
"""Set up test client and user"""
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def test_home_page_get(self):
"""Test GET request to home page"""
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Welcome')
self.assertTemplateUsed(response, 'home.html')
def test_login_post(self):
"""Test POST request to login"""
response = self.client.post('/login/', {
'username': 'testuser',
'password': 'testpass123'
})
self.assertEqual(response.status_code, 302) # Redirect after login
self.assertRedirects(response, '/dashboard/')
def test_authenticated_view(self):
"""Test view that requires authentication"""
# Test without authentication
response = self.client.get('/profile/')
self.assertEqual(response.status_code, 302) # Redirect to login
# Test with authentication
self.client.login(username='testuser', password='testpass123')
response = self.client.get('/profile/')
self.assertEqual(response.status_code, 200)
def test_ajax_request(self):
"""Test AJAX request"""
response = self.client.post(
'/api/data/',
{'key': 'value'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest'
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
def test_file_upload(self):
"""Test file upload"""
with open('test_file.txt', 'w') as f:
f.write('Test file content')
with open('test_file.txt', 'rb') as f:
response = self.client.post('/upload/', {
'file': f,
'description': 'Test upload'
})
self.assertEqual(response.status_code, 201)
# Test client methods:
# - client.get(path, data, follow, secure, **extra)
# - client.post(path, data, content_type, follow, secure, **extra)
# - client.put(path, data, content_type, **extra)
# - client.patch(path, data, content_type, **extra)
# - client.delete(path, **extra)
# - client.head(path, **extra)
# - client.options(path, **extra)
# - client.trace(path, **extra)
Django provides additional assertion methods for web testing:
from django.test import TestCase
class AssertionExampleTestCase(TestCase):
"""Examples of Django-specific assertions"""
def test_response_assertions(self):
"""Test response-related assertions"""
response = self.client.get('/')
# Status code assertions
self.assertEqual(response.status_code, 200)
self.assertNotEqual(response.status_code, 404)
# Content assertions
self.assertContains(response, 'Welcome')
self.assertNotContains(response, 'Error')
self.assertContains(response, 'Welcome', count=1)
# Template assertions
self.assertTemplateUsed(response, 'home.html')
self.assertTemplateNotUsed(response, 'error.html')
# Redirect assertions
login_response = self.client.post('/login/', {
'username': 'user',
'password': 'pass'
})
self.assertRedirects(login_response, '/dashboard/')
self.assertRedirects(
login_response,
'/dashboard/',
status_code=302,
target_status_code=200
)
def test_form_assertions(self):
"""Test form-related assertions"""
response = self.client.post('/contact/', {
'name': '', # Invalid: required field
'email': 'invalid-email', # Invalid: bad format
'message': 'Test message'
})
# Form error assertions
self.assertFormError(
response,
'form',
'name',
'This field is required.'
)
self.assertFormError(
response,
'form',
'email',
'Enter a valid email address.'
)
# Multiple form errors
self.assertFormError(response, 'form', 'name', ['This field is required.'])
def test_database_assertions(self):
"""Test database-related assertions"""
from myapp.models import BlogPost
# Count assertions
initial_count = BlogPost.objects.count()
BlogPost.objects.create(
title='New Post',
content='Content'
)
self.assertEqual(BlogPost.objects.count(), initial_count + 1)
# Existence assertions
self.assertTrue(
BlogPost.objects.filter(title='New Post').exists()
)
# Query assertions
posts = BlogPost.objects.filter(title__icontains='new')
self.assertQuerysetEqual(
posts,
['<BlogPost: New Post>']
)
def test_custom_assertions(self):
"""Examples of general assertions"""
# Boolean assertions
self.assertTrue(True)
self.assertFalse(False)
# Equality assertions
self.assertEqual(1 + 1, 2)
self.assertNotEqual(1 + 1, 3)
# Membership assertions
self.assertIn('item', ['item', 'other'])
self.assertNotIn('missing', ['item', 'other'])
# Exception assertions
with self.assertRaises(ValueError):
int('not-a-number')
with self.assertRaisesMessage(ValueError, 'invalid literal'):
int('not-a-number')
# Regex assertions
self.assertRegex('hello world', r'hello \w+')
self.assertNotRegex('hello world', r'goodbye \w+')
# settings/test.py - Test-specific settings
from .base import *
# Use in-memory database for speed
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# Disable migrations for faster tests
class DisableMigrations:
def __contains__(self, item):
return True
def __getitem__(self, item):
return None
MIGRATION_MODULES = DisableMigrations()
# Disable caching
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}
# Use console email backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Disable logging during tests
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'null': {
'class': 'logging.NullHandler',
},
},
'root': {
'handlers': ['null'],
},
}
# Test-specific settings
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher', # Faster for tests
]
# Disable debug toolbar in tests
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: False,
}
# Basic test execution
python manage.py test
# Run specific app tests
python manage.py test myapp
# Run specific test class
python manage.py test myapp.tests.BlogPostTestCase
# Run specific test method
python manage.py test myapp.tests.BlogPostTestCase.test_post_creation
# Run with different settings
python manage.py test --settings=myproject.settings.test
# Run with verbose output
python manage.py test --verbosity=2
# Keep test database
python manage.py test --keepdb
# Run tests in parallel
python manage.py test --parallel
# Run with coverage
coverage run --source='.' manage.py test
coverage report
coverage html # Generate HTML report
# Option 1: Single tests.py file (for small apps)
myapp/
├── tests.py
├── models.py
├── views.py
└── forms.py
# Option 2: Tests package (recommended for larger apps)
myapp/
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_views.py
│ ├── test_forms.py
│ ├── test_utils.py
│ └── factories.py
├── models.py
├── views.py
└── forms.py
# Option 3: Feature-based organization
myapp/
├── tests/
│ ├── __init__.py
│ ├── test_user_registration.py
│ ├── test_blog_posting.py
│ ├── test_comment_system.py
│ └── factories.py
class BlogPostModelTestCase(TestCase):
"""Test BlogPost model functionality"""
def test_post_creation_with_valid_data(self):
"""Test creating post with valid data succeeds"""
pass
def test_post_creation_without_title_fails(self):
"""Test creating post without title raises ValidationError"""
pass
def test_post_slug_generation_from_title(self):
"""Test that post slug is automatically generated from title"""
pass
def test_post_str_representation_returns_title(self):
"""Test that __str__ method returns post title"""
pass
# Naming conventions:
# - Class names: [Component][Type]TestCase
# - Method names: test_[what]_[condition]_[expected_result]
# - Descriptive docstrings explaining what is being tested
Now that you understand Django's testing framework fundamentals, you're ready to dive deeper into writing and running tests. The next chapter will cover practical aspects of creating comprehensive test suites, organizing tests effectively, and using Django's testing tools to their full potential.
Key concepts to remember:
With these foundations in place, you'll be able to write effective tests that ensure your Django application's reliability and quality.
Testing
Testing is a critical aspect of Django development that ensures your application works correctly, maintains quality over time, and provides confidence when making changes. Django provides a comprehensive testing framework built on Python's unittest module, along with additional tools specifically designed for web application testing.
Writing and Running Tests
Effective test writing and execution are crucial skills for Django developers. This chapter covers practical techniques for creating comprehensive test suites, organizing tests for maintainability, and running tests efficiently during development and in production environments.