Skip to main content

Testing & Debugging

Django provides a comprehensive testing framework and debugging tools for building reliable applications.

Writing Tests

Test Structure

# tests.py
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Article

class ArticleModelTest(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
)

def test_string_representation(self):
self.assertEqual(str(self.article), 'Test Article')

def test_article_creation(self):
self.assertTrue(isinstance(self.article, Article))
self.assertEqual(self.article.title, 'Test Article')
self.assertEqual(self.article.author, self.user)

View Testing

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='Test content',
author=self.user
)

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.assertQuerysetEqual(
response.context['articles'],
[self.article],
transform=repr
)

def test_article_detail_view(self):
response = self.client.get(
reverse('article_detail', kwargs={'pk': self.article.pk})
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['article'], self.article)

def test_article_create_requires_login(self):
response = self.client.get(reverse('article_create'))
self.assertRedirects(response, '/accounts/login/?next=/articles/create/')

def test_article_create_with_login(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())

Form Testing

from .forms import ArticleForm

class ArticleFormTest(TestCase):
def test_valid_form(self):
form_data = {
'title': 'Test Article',
'content': 'Test content'
}
form = ArticleForm(data=form_data)
self.assertTrue(form.is_valid())

def test_form_save(self):
user = User.objects.create_user(
username='testuser',
password='testpass123'
)
form_data = {
'title': 'Test Article',
'content': 'Test content'
}
form = ArticleForm(data=form_data)
if form.is_valid():
article = form.save(commit=False)
article.author = user
article.save()
self.assertEqual(article.title, 'Test Article')

def test_empty_form(self):
form = ArticleForm(data={})
self.assertFalse(form.is_valid())
self.assertIn('title', form.errors)
self.assertIn('content', form.errors)

Advanced Testing

Database Testing

from django.test import TransactionTestCase
from django.db import transaction

class DatabaseTest(TransactionTestCase):
def test_transaction_rollback(self):
initial_count = Article.objects.count()

try:
with transaction.atomic():
Article.objects.create(
title='Test',
content='Content',
author_id=999 # Invalid foreign key
)
except Exception:
pass

self.assertEqual(Article.objects.count(), initial_count)

Mock Testing

from unittest.mock import patch, Mock
from django.test import TestCase

class EmailTest(TestCase):
@patch('myapp.utils.send_email')
def test_article_notification(self, mock_send_email):
mock_send_email.return_value = True

# Create article that triggers email
article = Article.objects.create(
title='Test Article',
content='Content',
author=self.user
)

# Verify email was sent
mock_send_email.assert_called_once_with(
subject='New Article Published',
message=f'Article "{article.title}" has been published.',
recipient_list=['admin@example.com']
)

API Testing

import json
from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status

class ArticleAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)

def test_create_article_unauthenticated(self):
data = {
'title': 'Test Article',
'content': 'Test content'
}
response = self.client.post('/api/articles/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_create_article_authenticated(self):
self.client.force_authenticate(user=self.user)
data = {
'title': 'Test Article',
'content': 'Test content'
}
response = self.client.post('/api/articles/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Article.objects.count(), 1)

Test Data and Fixtures

Fixtures

# Load data from fixtures
class ArticleTest(TestCase):
fixtures = ['users.json', 'articles.json']

def test_loaded_data(self):
self.assertEqual(User.objects.count(), 5)
self.assertEqual(Article.objects.count(), 10)

Factory Boy

# tests/factories.py
import factory
from django.contrib.auth.models import User
from myapp.models import Article

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 ArticleFactory(factory.django.DjangoModelFactory):
class Meta:
model = Article

title = factory.Faker('sentence', nb_words=4)
content = factory.Faker('text')
author = factory.SubFactory(UserFactory)

# Usage in tests
class ArticleTest(TestCase):
def test_article_creation(self):
article = ArticleFactory()
self.assertTrue(isinstance(article, Article))

def test_multiple_articles(self):
articles = ArticleFactory.create_batch(5)
self.assertEqual(len(articles), 5)

Debugging Tools

Django Debug Toolbar

# settings.py
INSTALLED_APPS = [
# ... other apps
'debug_toolbar',
]

MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ... other middleware
]

INTERNAL_IPS = [
'127.0.0.1',
]

# Debug toolbar configuration
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': lambda request: DEBUG,
}
# urls.py
if settings.DEBUG:
import debug_toolbar
urlpatterns = [
path('__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

Logging Configuration

# settings.py
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': 'django.log',
'formatter': 'verbose',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple',
},
},
'loggers': {
'django': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propagate': True,
},
'myapp': {
'handlers': ['file', 'console'],
'level': 'DEBUG',
'propagate': True,
},
},
}

Using Logging in Code

import logging

logger = logging.getLogger(__name__)

def my_view(request):
logger.debug('Entering my_view')

try:
# Some operation
result = perform_operation()
logger.info(f'Operation successful: {result}')
except Exception as e:
logger.error(f'Operation failed: {str(e)}', exc_info=True)
raise

logger.debug('Exiting my_view')
return render(request, 'template.html')

Performance Testing

Database Query Analysis

from django.test import TestCase
from django.test.utils import override_settings
from django.db import connection

class PerformanceTest(TestCase):
def test_query_count(self):
with self.assertNumQueries(1):
list(Article.objects.all())

def test_select_related(self):
# Create test data
ArticleFactory.create_batch(10)

# Test without select_related (N+1 queries)
with self.assertNumQueries(11): # 1 + 10 queries
articles = Article.objects.all()
for article in articles:
str(article.author.username)

# Test with select_related (1 query)
with self.assertNumQueries(1):
articles = Article.objects.select_related('author')
for article in articles:
str(article.author.username)

Memory Usage Testing

import tracemalloc
from django.test import TestCase

class MemoryTest(TestCase):
def test_memory_usage(self):
tracemalloc.start()

# Your code here
articles = list(Article.objects.all())

current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()

# Assert memory usage is reasonable
self.assertLess(current / 1024 / 1024, 100) # Less than 100MB

Test Organization

Test Discovery

# 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.ArticleModelTest

# Run specific test method
python manage.py test myapp.tests.ArticleModelTest.test_string_representation

# Run tests with coverage
coverage run --source='.' manage.py test
coverage report
coverage html

Test Settings

# test_settings.py
from .settings import *

# Use in-memory SQLite for faster tests
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 logging during tests
LOGGING_CONFIG = None

# Use dummy cache
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
}
}

Continuous Integration

GitHub Actions

# .github/workflows/django.yml
name: Django CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run tests
run: |
python manage.py test
coverage run --source='.' manage.py test
coverage xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1