Skip to main content

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)