Skip to main content

Testing Guide

Django provides a comprehensive testing framework built on Python’s unittest module. This guide covers unit tests, integration tests, and testing best practices.

Overview

Django’s testing framework provides:
  • Test classes - TestCase, TransactionTestCase, SimpleTestCase, LiveServerTestCase
  • Test client - Simulate requests without running a server
  • Assertions - Specialized assertions for Django apps
  • Fixtures - Test data management
  • Coverage - Code coverage analysis

Quick Start

1

Create Test File

Add tests in tests.py or a tests/ module:
tests.py
from django.test import TestCase
from .models import Article

class ArticleTestCase(TestCase):
    def setUp(self):
        Article.objects.create(title="Test Article", content="Test content")
    
    def test_article_creation(self):
        article = Article.objects.get(title="Test Article")
        self.assertEqual(article.content, "Test content")
2

Run Tests

Execute tests with the management command:
# Run all tests
python manage.py test

# Run specific app
python manage.py test myapp

# Run specific test class
python manage.py test myapp.tests.ArticleTestCase

# Run specific test method
python manage.py test myapp.tests.ArticleTestCase.test_article_creation

Test Classes

SimpleTestCase

For tests that don’t require database access:
from django.test import SimpleTestCase
from myapp.utils import slugify_text

class UtilsTestCase(SimpleTestCase):
    def test_slugify(self):
        result = slugify_text("Hello World!")
        self.assertEqual(result, "hello-world")
    
    def test_empty_string(self):
        result = slugify_text("")
        self.assertEqual(result, "")
SimpleTestCase runs faster than TestCase because it doesn’t set up or tear down the database.

TestCase

For tests that require database access:
from django.test import TestCase
from django.contrib.auth.models import User
from .models import Article

class ArticleModelTestCase(TestCase):
    @classmethod
    def setUpTestData(cls):
        """Set up data for the whole TestCase"""
        cls.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        cls.article = Article.objects.create(
            title='Test Article',
            content='Test content',
            author=cls.user
        )
    
    def test_article_title(self):
        self.assertEqual(self.article.title, 'Test Article')
    
    def test_article_str(self):
        self.assertEqual(str(self.article), 'Test Article')
    
    def test_article_author(self):
        self.assertEqual(self.article.author, self.user)

TransactionTestCase

For tests that need to test transaction behavior:
from django.test import TransactionTestCase
from django.db import transaction
from .models import Account

class TransactionTestCase(TransactionTestCase):
    def test_atomic_transaction(self):
        account = Account.objects.create(balance=100)
        
        try:
            with transaction.atomic():
                account.balance -= 50
                account.save()
                
                # Force error
                raise Exception("Rollback!")
        except Exception:
            pass
        
        # Balance should be unchanged
        account.refresh_from_db()
        self.assertEqual(account.balance, 100)

LiveServerTestCase

For Selenium and browser testing:
from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By

class SeleniumTestCase(LiveServerTestCase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.selenium = webdriver.Chrome()
        cls.selenium.implicitly_wait(10)
    
    @classmethod
    def tearDownClass(cls):
        cls.selenium.quit()
        super().tearDownClass()
    
    def test_login(self):
        self.selenium.get(f'{self.live_server_url}/login/')
        
        username_input = self.selenium.find_element(By.NAME, 'username')
        username_input.send_keys('testuser')
        
        password_input = self.selenium.find_element(By.NAME, 'password')
        password_input.send_keys('testpass')
        
        self.selenium.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()
        
        self.assertIn('Dashboard', self.selenium.title)

Test Client

Making Requests

from django.test import TestCase, Client

class ViewTestCase(TestCase):
    def setUp(self):
        self.client = Client()
    
    def test_homepage(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)
    
    def test_post_request(self):
        response = self.client.post('/contact/', {
            'name': 'John Doe',
            'email': '[email protected]',
            'message': 'Test message'
        })
        self.assertEqual(response.status_code, 302)  # Redirect after success
    
    def test_ajax_request(self):
        response = self.client.get(
            '/api/data/',
            HTTP_X_REQUESTED_WITH='XMLHttpRequest'
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response['Content-Type'], 'application/json')

Authentication

def test_authenticated_view(self):
    # Create user
    user = User.objects.create_user(
        username='testuser',
        password='testpass123'
    )
    
    # Login
    self.client.login(username='testuser', password='testpass123')
    
    # Access protected view
    response = self.client.get('/dashboard/')
    self.assertEqual(response.status_code, 200)
    
    # Logout
    self.client.logout()
    
    # Should redirect to login
    response = self.client.get('/dashboard/')
    self.assertEqual(response.status_code, 302)

def test_force_login(self):
    # Skip password hashing (faster)
    user = User.objects.create_user(username='testuser')
    self.client.force_login(user)
    
    response = self.client.get('/dashboard/')
    self.assertEqual(response.status_code, 200)

Assertions

Response Assertions

def test_response_assertions(self):
    response = self.client.get('/')
    
    # Status code
    self.assertEqual(response.status_code, 200)
    
    # Contains text
    self.assertContains(response, 'Welcome')
    self.assertNotContains(response, 'Error')
    
    # Contains text with count
    self.assertContains(response, 'Article', count=5)
    
    # Template used
    self.assertTemplateUsed(response, 'home.html')
    self.assertTemplateNotUsed(response, 'error.html')
    
    # Redirect
    response = self.client.post('/login/', {'username': 'test', 'password': 'test'})
    self.assertRedirects(response, '/dashboard/')

Database Assertions

def test_database_assertions(self):
    # Query count
    with self.assertNumQueries(2):
        list(Article.objects.all())
        list(User.objects.all())
    
    # Object exists
    Article.objects.create(title='Test')
    self.assertTrue(Article.objects.filter(title='Test').exists())
    
    # Count
    self.assertEqual(Article.objects.count(), 1)

Form Assertions

def test_form_validation(self):
    form_data = {'title': '', 'content': 'Test'}
    response = self.client.post('/articles/create/', form_data)
    
    # Check form errors
    self.assertFormError(response, 'form', 'title', 'This field is required.')

HTML Assertions

def test_html_assertions(self):
    html1 = '<div><p>Hello</p></div>'
    html2 = '<div>  <p>Hello</p>  </div>'
    
    # Ignores whitespace differences
    self.assertHTMLEqual(html1, html2)
    
    # Check element in HTML
    response = self.client.get('/')
    self.assertInHTML('<h1>Welcome</h1>', response.content.decode())

JSON Assertions

def test_json_response(self):
    response = self.client.get('/api/articles/')
    
    self.assertEqual(response['Content-Type'], 'application/json')
    
    data = response.json()
    self.assertEqual(len(data), 10)
    self.assertIn('title', data[0])
    
    # Compare JSON
    expected = {'status': 'success', 'count': 10}
    self.assertJSONEqual(response.content, expected)

Fixtures

Using Fixtures

class ArticleTestCase(TestCase):
    fixtures = ['articles.json', 'users.json']
    
    def test_article_from_fixture(self):
        article = Article.objects.get(pk=1)
        self.assertEqual(article.title, 'First Article')

Creating Fixtures

# Export data as fixture
python manage.py dumpdata myapp.Article --indent 2 > fixtures/articles.json

# Export all data
python manage.py dumpdata --indent 2 > fixtures/all_data.json

# Load fixtures
python manage.py loaddata articles.json

Mocking

Mock External APIs

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

class APITestCase(TestCase):
    @patch('myapp.views.requests.get')
    def test_external_api(self, mock_get):
        # Configure mock
        mock_response = Mock()
        mock_response.json.return_value = {'data': 'test'}
        mock_response.status_code = 200
        mock_get.return_value = mock_response
        
        # Test code that calls requests.get
        response = self.client.get('/fetch-data/')
        
        self.assertEqual(response.status_code, 200)
        mock_get.assert_called_once()

Mock Time

from django.test import TestCase
from unittest.mock import patch
from datetime import datetime

class TimeTestCase(TestCase):
    @patch('django.utils.timezone.now')
    def test_time_dependent_code(self, mock_now):
        mock_now.return_value = datetime(2024, 1, 1, 12, 0, 0)
        
        # Test code that depends on current time
        response = self.client.get('/current-time/')
        self.assertContains(response, '2024-01-01')

Async Tests

from django.test import TestCase
import asyncio

class AsyncTestCase(TestCase):
    async def test_async_function(self):
        result = await my_async_function()
        self.assertEqual(result, 'expected')
    
    def test_async_view(self):
        response = self.client.get('/async-view/')
        self.assertEqual(response.status_code, 200)

Test Organization

Structure Tests in Modules

myapp/
├── tests/
│   ├── __init__.py
│   ├── test_models.py
│   ├── test_views.py
│   ├── test_forms.py
│   └── test_utils.py
tests/__init__.py
# Import all test classes
from .test_models import *
from .test_views import *
from .test_forms import *
from .test_utils import *

Use Test Tags

from django.test import TestCase, tag

@tag('fast')
class FastTestCase(TestCase):
    def test_something(self):
        pass

@tag('slow', 'integration')
class SlowTestCase(TestCase):
    def test_integration(self):
        pass
# Run specific tags
python manage.py test --tag=fast
python manage.py test --exclude-tag=slow

Best Practices

1

Use setUpTestData for Shared Data

Create data once per test class:
class MyTestCase(TestCase):
    @classmethod
    def setUpTestData(cls):
        # Created once for entire class
        cls.user = User.objects.create_user('test')
    
    def setUp(self):
        # Called before each test method
        self.client.login(username='test')
2

Keep Tests Independent

Each test should be isolated:
# Good - independent
def test_create_article(self):
    article = Article.objects.create(title='Test')
    self.assertEqual(Article.objects.count(), 1)

# Bad - depends on other tests
def test_article_count(self):
    self.assertEqual(Article.objects.count(), 1)  # Brittle!
3

Test Edge Cases

Don’t just test the happy path:
def test_empty_input(self):
    form = ContactForm(data={'message': ''})
    self.assertFalse(form.is_valid())

def test_max_length(self):
    long_text = 'x' * 1000
    form = ContactForm(data={'message': long_text})
    self.assertFalse(form.is_valid())
4

Use Coverage Tools

Measure test coverage:
pip install coverage

coverage run --source='.' manage.py test
coverage report
coverage html  # Generate HTML report

Common Patterns

Testing Emails

from django.core import mail
from django.test import TestCase

class EmailTestCase(TestCase):
    def test_welcome_email(self):
        # Clear outbox
        mail.outbox = []
        
        # Trigger email
        response = self.client.post('/register/', {
            'username': 'newuser',
            'email': '[email protected]',
            'password': 'testpass123'
        })
        
        # Check email was sent
        self.assertEqual(len(mail.outbox), 1)
        
        # Check email content
        email = mail.outbox[0]
        self.assertEqual(email.subject, 'Welcome!')
        self.assertEqual(email.to, ['[email protected]'])
        self.assertIn('activate', email.body)

Testing File Uploads

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase

class FileUploadTestCase(TestCase):
    def test_image_upload(self):
        # Create test file
        test_file = SimpleUploadedFile(
            'test.jpg',
            b'file content',
            content_type='image/jpeg'
        )
        
        response = self.client.post('/upload/', {
            'image': test_file
        })
        
        self.assertEqual(response.status_code, 302)

Testing Permissions

def test_permission_required(self):
    # Create users
    regular_user = User.objects.create_user('regular', password='test')
    admin_user = User.objects.create_user('admin', password='test', is_staff=True)
    
    # Regular user denied
    self.client.force_login(regular_user)
    response = self.client.get('/admin-only/')
    self.assertEqual(response.status_code, 403)
    
    # Admin user allowed
    self.client.force_login(admin_user)
    response = self.client.get('/admin-only/')
    self.assertEqual(response.status_code, 200)
Test databases are destroyed after tests complete. Never run tests against production databases.
  • Testing Tools Reference
  • Test Client API
  • Coverage Configuration

Build docs developers (and LLMs) love