Testing
Test Setup
Test Configuration
# settings/test.py
from .base import *
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()
# Email backend for testing
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
# Media files for testing
MEDIA_ROOT = '/tmp/test_media'
# Cache for testing
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
# Password hashing for testing
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
Test Runner
# test_runner.py
from django.test.runner import DiscoverRunner
class NoDbTestRunner(DiscoverRunner):
def setup_databases(self, **kwargs):
pass
def teardown_databases(self, old_config, **kwargs):
pass
Basic Testing
Model Testing
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from .models import Article, Category
class ArticleModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpass123'
)
self.category = Category.objects.create(
name='Technology',
slug='technology'
)
def test_article_creation(self):
article = Article.objects.create(
title='Test Article',
content='This is a test article.',
author=self.user,
category=self.category
)
self.assertEqual(article.title, 'Test Article')
self.assertEqual(article.author, self.user)
self.assertEqual(str(article), 'Test Article')
def test_article_slug_generation(self):
article = Article.objects.create(
title='Test Article',
content='This is a test article.',
author=self.user,
category=self.category
)
self.assertEqual(article.slug, 'test-article')
def test_article_absolute_url(self):
article = Article.objects.create(
title='Test Article',
content='This is a test article.',
author=self.user,
category=self.category
)
expected_url = f'/articles/{article.slug}/'
self.assertEqual(article.get_absolute_url(), expected_url)
def test_article_validation(self):
with self.assertRaises(ValidationError):
article = Article(
title='', # Empty title should fail validation
content='This is a test article.',
author=self.user,
category=self.category
)
article.full_clean()
View Testing
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Article
class ArticleViewTest(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.article = Article.objects.create(
title='Test Article',
content='This is a test article.',
author=self.user,
published=True
)
def test_article_list_view(self):
response = self.client.get(reverse('article_list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Test Article')
self.assertIn('articles', response.context)
def test_article_detail_view(self):
response = self.client.get(
reverse('article_detail', kwargs={'slug': self.article.slug})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.article.title)
self.assertEqual(response.context['article'], self.article)
def test_article_create_requires_login(self):
response = self.client.get(reverse('article_create'))
self.assertRedirects(response, '/login/?next=/articles/create/')
def test_article_create_authenticated(self):
self.client.login(username='testuser', password='testpass123')
response = self.client.post(reverse('article_create'), {
'title': 'New Article',
'content': 'New content',
})
self.assertEqual(response.status_code, 302)
self.assertTrue(Article.objects.filter(title='New Article').exists())
def test_article_404(self):
response = self.client.get('/articles/non-existent-slug/')
self.assertEqual(response.status_code, 404)
Form Testing
from django.test import TestCase
from .forms import ArticleForm, ContactForm
class ArticleFormTest(TestCase):
def test_valid_form(self):
form_data = {
'title': 'Test Article',
'content': 'This is a test article.',
'published': True
}
form = ArticleForm(data=form_data)
self.assertTrue(form.is_valid())
def test_invalid_form_missing_title(self):
form_data = {
'content': 'This is a test article.',
'published': True
}
form = ArticleForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
def test_form_save(self):
form_data = {
'title': 'Test Article',
'content': 'This is a test article.',
'published': True
}
form = ArticleForm(data=form_data)
self.assertTrue(form.is_valid())
# Test with commit=False
article = form.save(commit=False)
self.assertIsNone(article.pk)
# Test with commit=True
article = form.save()
self.assertIsNotNone(article.pk)
class ContactFormTest(TestCase):
def test_clean_email(self):
form_data = {
'name': 'John Doe',
'email': 'john@example.com',
'message': 'Hello, world!'
}
form = ContactForm(data=form_data)
self.assertTrue(form.is_valid())
def test_clean_email_invalid(self):
form_data = {
'name': 'John Doe',
'email': 'invalid-email',
'message': 'Hello, world!'
}
form = ContactForm(data=form_data)
self.assertFalse(form.is_valid())
self.assertIn('email', form.errors)
Advanced Testing
Test Fixtures
# fixtures/test_data.json
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"username": "testuser",
"email": "test@example.com",
"is_active": true
}
},
{
"model": "myapp.category",
"pk": 1,
"fields": {
"name": "Technology",
"slug": "technology"
}
}
]
# Using fixtures in tests
class ArticleTestWithFixtures(TestCase):
fixtures = ['test_data.json']
def test_with_fixture_data(self):
user = User.objects.get(pk=1)
category = Category.objects.get(pk=1)
self.assertEqual(user.username, 'testuser')
self.assertEqual(category.name, 'Technology')
Factory Boy
# factories.py
import factory
from django.contrib.auth.models import User
from .models import Article, Category
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
username = factory.Sequence(lambda n: f'user{n}')
email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
name = factory.Faker('word')
slug = factory.LazyAttribute(lambda obj: obj.name.lower())
class ArticleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Article
title = factory.Faker('sentence', nb_words=4)
content = factory.Faker('text')
author = factory.SubFactory(UserFactory)
category = factory.SubFactory(CategoryFactory)
published = True
# Using factories in tests
class ArticleTestWithFactory(TestCase):
def test_article_with_factory(self):
article = ArticleFactory()
self.assertTrue(article.published)
self.assertIsNotNone(article.author)
def test_multiple_articles(self):
articles = ArticleFactory.create_batch(5)
self.assertEqual(len(articles), 5)
self.assertEqual(Article.objects.count(), 5)
Mock and Patch
from unittest.mock import patch, Mock
from django.test import TestCase
from django.core.mail import send_mail
from .utils import send_notification_email
class EmailTest(TestCase):
@patch('myapp.utils.send_mail')
def test_send_notification_email(self, mock_send_mail):
mock_send_mail.return_value = True
result = send_notification_email('Test Subject', 'Test Message', 'test@example.com')
self.assertTrue(result)
mock_send_mail.assert_called_once_with(
'Test Subject',
'Test Message',
'from@example.com',
['test@example.com']
)
@patch('requests.get')
def test_external_api_call(self, mock_get):
mock_response = Mock()
mock_response.json.return_value = {'status': 'success'}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Test your function that makes the API call
result = fetch_external_data()
self.assertEqual(result['status'], 'success')
mock_get.assert_called_once()
Database Testing
Transaction Testing
from django.test import TestCase, TransactionTestCase
from django.db import transaction
from .models import Article
class TransactionTest(TransactionTestCase):
def test_transaction_rollback(self):
try:
with transaction.atomic():
Article.objects.create(
title='Test Article',
content='Test content'
)
# Simulate an error
raise Exception("Test exception")
except Exception:
pass
# Article should not exist due to rollback
self.assertEqual(Article.objects.count(), 0)
def test_transaction_commit(self):
with transaction.atomic():
Article.objects.create(
title='Test Article',
content='Test content'
)
# Article should exist
self.assertEqual(Article.objects.count(), 1)
Database Queries Testing
from django.test import TestCase
from django.test.utils import override_settings
from django.db import connection
from django.db.models import Count
from .models import Article, Category
class QueryTest(TestCase):
def setUp(self):
# Create test data
self.category = Category.objects.create(name='Tech', slug='tech')
for i in range(10):
Article.objects.create(
title=f'Article {i}',
content=f'Content {i}',
category=self.category
)
def test_query_count(self):
with self.assertNumQueries(2):
# Should make 2 queries: one for categories, one for articles
categories = Category.objects.all()
for category in categories:
articles = category.articles.all()
def test_select_related(self):
with self.assertNumQueries(1):
# Should make only 1 query with select_related
articles = Article.objects.select_related('category').all()
for article in articles:
category_name = article.category.name
def test_prefetch_related(self):
with self.assertNumQueries(2):
# Should make 2 queries with prefetch_related
categories = Category.objects.prefetch_related('articles').all()
for category in categories:
articles = category.articles.all()
Testing Utilities
Custom Test Base Classes
class BaseTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
def setUp(self):
self.client.login(username='testuser', password='testpass123')
def create_article(self, **kwargs):
defaults = {
'title': 'Test Article',
'content': 'Test content',
'author': self.user,
}
defaults.update(kwargs)
return Article.objects.create(**defaults)
class AuthenticatedTestCase(BaseTestCase):
def setUp(self):
super().setUp()
# Additional setup for authenticated tests
pass
class AdminTestCase(BaseTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.admin_user = User.objects.create_superuser(
username='admin',
password='adminpass123',
email='admin@example.com'
)
def setUp(self):
self.client.login(username='admin', password='adminpass123')
Test Helpers
# test_helpers.py
from django.test import Client
from django.contrib.auth.models import User
def create_test_user(username='testuser', password='testpass123', **kwargs):
return User.objects.create_user(
username=username,
password=password,
**kwargs
)
def login_user(client, user):
client.force_login(user)
def assert_redirects_to_login(test_case, response, next_url=None):
if next_url:
expected_url = f'/login/?next={next_url}'
else:
expected_url = '/login/'
test_case.assertRedirects(response, expected_url)
class TestHelperMixin:
def assertContainsMessage(self, response, message_text):
messages = list(response.context['messages'])
message_texts = [str(m) for m in messages]
self.assertIn(message_text, message_texts)
def assertFormError(self, response, form_name, field_name, error_text):
form = response.context[form_name]
self.assertIn(error_text, form.errors[field_name])
API Testing
REST API Testing
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from django.contrib.auth.models import User
from .models import Article
class ArticleAPITest(APITestCase):
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
)
def test_get_article_list(self):
url = reverse('article-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_get_article_detail(self):
url = reverse('article-detail', kwargs={'pk': self.article.pk})
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Test Article')
def test_create_article_authenticated(self):
self.client.force_authenticate(user=self.user)
url = reverse('article-list')
data = {
'title': 'New Article',
'content': 'New content'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 2)
def test_create_article_unauthenticated(self):
url = reverse('article-list')
data = {
'title': 'New Article',
'content': 'New content'
}
response = self.client.post(url, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
WebSocket Testing
from channels.testing import WebsocketCommunicator
from channels.db import database_sync_to_async
from django.test import TransactionTestCase
from .consumers import ChatConsumer
class ChatConsumerTest(TransactionTestCase):
async def test_chat_consumer(self):
communicator = WebsocketCommunicator(ChatConsumer.as_asgi(), "/ws/chat/")
connected, subprotocol = await communicator.connect()
self.assertTrue(connected)
# Send message
await communicator.send_json_to({
'message': 'Hello, world!'
})
# Receive message
response = await communicator.receive_json_from()
self.assertEqual(response['message'], 'Hello, world!')
# Disconnect
await communicator.disconnect()
Performance Testing
Load Testing
from django.test import TestCase
from django.test.utils import override_settings
import time
class PerformanceTest(TestCase):
def test_view_performance(self):
start_time = time.time()
for i in range(100):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
end_time = time.time()
execution_time = end_time - start_time
# Assert that 100 requests take less than 1 second
self.assertLess(execution_time, 1.0)
@override_settings(DEBUG=False)
def test_production_performance(self):
# Test with production settings
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
Test Coverage
Coverage Configuration
# Install coverage
pip install coverage
# Run tests with coverage
coverage run --source='.' manage.py test
# Generate coverage report
coverage report
# Generate HTML coverage report
coverage html
Coverage Settings
# .coveragerc
[run]
source = .
omit =
*/migrations/*
*/venv/*
*/settings/*
manage.py
*/tests/*
*/test_*.py
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
Test Running
Running Tests
# Run all tests
python manage.py test
# Run specific app tests
python manage.py test myapp
# Run specific test class
python manage.py test myapp.tests.ArticleTest
# Run specific test method
python manage.py test myapp.tests.ArticleTest.test_article_creation
# Run tests with verbosity
python manage.py test --verbosity=2
# Run tests in parallel
python manage.py test --parallel
# Keep test database
python manage.py test --keepdb
Test Discovery
# Custom test discovery
import unittest
def suite():
loader = unittest.TestLoader()
suite = unittest.TestSuite()
# Add specific test modules
suite.addTests(loader.loadTestsFromModule('myapp.tests.test_models'))
suite.addTests(loader.loadTestsFromModule('myapp.tests.test_views'))
return suite
Debugging Tests
Test Debugging
import pdb
from django.test import TestCase
class DebugTest(TestCase):
def test_with_debugger(self):
article = Article.objects.create(title='Test')
pdb.set_trace() # Debugger breakpoint
self.assertEqual(article.title, 'Test')
def test_with_print_statements(self):
response = self.client.get('/')
print(f"Status: {response.status_code}")
print(f"Content: {response.content}")
self.assertEqual(response.status_code, 200)
Test Output
from django.test import TestCase
from django.test.utils import override_settings
import logging
class OutputTest(TestCase):
def test_with_logging(self):
logger = logging.getLogger('myapp')
logger.info('Test log message')
# Test something
self.assertTrue(True)
@override_settings(LOGGING_CONFIG=None)
def test_without_logging(self):
# Test without logging
self.assertTrue(True)