Introduction to Django Testing
Django provides a robust testing framework built on top of Python's unittest library, but with additional features specifically designed for web applications. Testing Django applications is crucial for ensuring that your models, views, forms, and templates work correctly across the entire application stack.
Real-world analogy: Think of Django testing as quality control for a car manufacturing plant. You test individual components (unit tests), how components interact (integration tests), and finally take the car for a test drive to ensure it performs as expected (functional tests).
Django Testing Setup
Project Structure
Django automatically creates a tests.py file in each app when you run startapp. For larger applications, it's often better to create a tests directory:
my_project/
├── manage.py
└── my_app/
├── __init__.py
├── models.py
├── views.py
├── forms.py
└── tests/
├── __init__.py
├── test_models.py
├── test_views.py
└── test_forms.py
Running Tests
Django tests can be run using the test command:
# Run all tests
python manage.py test
# Run tests for specific app
python manage.py test my_app
# Run specific test
python manage.py test my_app.tests.test_models.ModelTestCase
# Run tests with tags (Django 2.2+)
python manage.py test --tag=fast
Settings for Testing
Django automatically uses a separate test database and applies sensible defaults for testing:
- A separate test database is created and destroyed for each test run
- Email sending is disabled by default
- Passwords are hashed more quickly to speed up tests
You can customize test settings in your settings.py:
# settings.py
# Use a faster password hasher for testing
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
# Use in-memory SQLite for testing
if 'test' in sys.argv:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}
# Custom test runner
TEST_RUNNER = 'my_app.test_runner.CustomTestRunner'
Django Test Case Classes
Django provides several test case classes for different testing needs:
SimpleTestCase
Used when you don't need database access:
from django.test import SimpleTestCase
class UtilityFunctionTests(SimpleTestCase):
def test_normalize_email(self):
result = normalize_email("User@EXAMPLE.com")
self.assertEqual(result, "user@example.com")
TestCase
The most commonly used class, provides database access with automatic transaction rollback:
from django.test import TestCase
from myapp.models import Product
class ProductModelTests(TestCase):
def setUp(self):
# This data is saved to the test database
self.product = Product.objects.create(
name="Test Product",
price=19.99,
in_stock=True
)
def test_product_str(self):
self.assertEqual(str(self.product), "Test Product")
TransactionTestCase
Used when you need to test transactions or database savepoints:
from django.test import TransactionTestCase
from django.db import transaction
from myapp.models import Account
class BankTransferTests(TransactionTestCase):
def test_transfer_with_insufficient_funds(self):
source = Account.objects.create(balance=50)
destination = Account.objects.create(balance=0)
# This will trigger a transaction rollback
with self.assertRaises(ValueError):
with transaction.atomic():
source.transfer_to(destination, 100)
LiveServerTestCase
Launches a live Django server for testing with tools like Selenium:
from django.test import LiveServerTestCase
from selenium import webdriver
class MySeleniumTests(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}/accounts/login/')
username_input = self.selenium.find_element_by_name("username")
username_input.send_keys('myuser')
# More test code here...
Real-world example: Instagram uses Django's TestCase extensively for their backend testing. They have found that creating specialized test case subclasses for different parts of their application helps organize tests and reduce test setup boilerplate.
Testing Django Models
Model testing typically focuses on:
- Basic CRUD operations
- Model methods
- Validation
- Relationships
- Signals
from django.test import TestCase
from django.core.exceptions import ValidationError
from myapp.models import Product, Category
class ProductModelTests(TestCase):
def setUp(self):
self.category = Category.objects.create(name="Electronics")
self.product = Product.objects.create(
name="Smartphone",
price=599.99,
category=self.category,
in_stock=True
)
def test_create_product(self):
# Test object creation and retrieval
product_count = Product.objects.count()
self.assertEqual(product_count, 1)
# Test field values
self.assertEqual(self.product.name, "Smartphone")
self.assertEqual(self.product.price, 599.99)
self.assertTrue(self.product.in_stock)
def test_product_str_representation(self):
# Test __str__ method
self.assertEqual(str(self.product), "Smartphone")
def test_product_relationships(self):
# Test foreign key relationship
self.assertEqual(self.product.category, self.category)
# Test reverse relationship
self.assertIn(self.product, self.category.products.all())
def test_price_validation(self):
# Test constraints/validation
invalid_product = Product(
name="Invalid Product",
price=-10.00, # Negative price
category=self.category
)
with self.assertRaises(ValidationError):
invalid_product.full_clean()
def test_custom_manager_method(self):
# Test custom manager
in_stock_products = Product.objects.in_stock()
self.assertEqual(in_stock_products.count(), 1)
self.product.in_stock = False
self.product.save()
in_stock_products = Product.objects.in_stock()
self.assertEqual(in_stock_products.count(), 0)
def test_discount_method(self):
# Test instance method
discounted_price = self.product.apply_discount(10)
self.assertEqual(discounted_price, 539.99)
Testing Model Signals
from django.test import TestCase
from django.db.models.signals import post_save
from myapp.models import User, Profile
from myapp.signals import create_user_profile
class ProfileSignalTests(TestCase):
def setUp(self):
# Disconnect the signal temporarily to isolate tests
post_save.disconnect(create_user_profile, sender=User)
def tearDown(self):
# Reconnect signal after test
post_save.connect(create_user_profile, sender=User)
def test_profile_creation_signal(self):
# Connect the signal for this test
post_save.connect(create_user_profile, sender=User)
user = User.objects.create_user(username='testuser', password='password')
# Check that a profile was created
self.assertTrue(hasattr(user, 'profile'))
self.assertIsNotNone(user.profile)
self.assertEqual(user.profile.user, user)
Testing Django Views
Django provides the Client class for simulating HTTP requests:
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product
class ProductViewTests(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(
username='testuser',
email='user@example.com',
password='testpassword'
)
self.product = Product.objects.create(
name="Test Product",
price=19.99,
description="Test description"
)
self.list_url = reverse('product-list')
self.detail_url = reverse('product-detail', args=[self.product.id])
def test_product_list_view(self):
# Test unauthenticated access
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'myapp/product_list.html')
self.assertContains(response, "Test Product")
# Check context
self.assertIn('products', response.context)
self.assertEqual(len(response.context['products']), 1)
def test_product_detail_view(self):
response = self.client.get(self.detail_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'myapp/product_detail.html')
self.assertContains(response, "Test Product")
self.assertContains(response, "Test description")
def test_product_create_view(self):
# Anonymous users can't create products
create_url = reverse('product-create')
response = self.client.get(create_url)
self.assertEqual(response.status_code, 302) # Redirect to login
# Login and try again
self.client.login(username='testuser', password='testpassword')
response = self.client.get(create_url)
self.assertEqual(response.status_code, 200)
# Test POST request
product_data = {
'name': 'New Product',
'price': 29.99,
'description': 'A new test product'
}
response = self.client.post(create_url, product_data)
# Check redirect on success
self.assertEqual(response.status_code, 302)
# Check that product was created
self.assertEqual(Product.objects.count(), 2)
new_product = Product.objects.get(name='New Product')
self.assertEqual(new_product.price, 29.99)
Testing Django REST Framework Views
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from myapp.models import Product
class ProductAPITests(APITestCase):
def setUp(self):
self.product = Product.objects.create(
name="Test Product",
price=19.99
)
self.list_url = reverse('api:product-list')
self.detail_url = reverse('api:product-detail', args=[self.product.id])
def test_get_product_list(self):
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_get_product_detail(self):
response = self.client.get(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], 'Test Product')
def test_create_product(self):
data = {'name': 'New API Product', 'price': 39.99}
response = self.client.post(self.list_url, data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Product.objects.count(), 2)
def test_update_product(self):
data = {'name': 'Updated Product', 'price': 29.99}
response = self.client.put(self.detail_url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Refresh from database
self.product.refresh_from_db()
self.assertEqual(self.product.name, 'Updated Product')
def test_delete_product(self):
response = self.client.delete(self.detail_url)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(Product.objects.count(), 0)
Real-world example: Sentry, which is built on Django, has extensive view tests that ensure proper access control and data handling. They mock authentication and focus on testing the business logic of their views rather than the rendering of templates.
Testing Django Forms
Form testing focuses on validation and cleaning:
from django.test import TestCase
from myapp.forms import ProductForm
from myapp.models import Category
class ProductFormTests(TestCase):
def setUp(self):
self.category = Category.objects.create(name="Electronics")
self.form_data = {
'name': 'Test Product',
'price': 29.99,
'category': self.category.id,
'description': 'A test product'
}
def test_valid_form(self):
form = ProductForm(data=self.form_data)
self.assertTrue(form.is_valid())
def test_empty_form(self):
form = ProductForm(data={})
self.assertFalse(form.is_valid())
# Required fields should have errors
self.assertIn('name', form.errors)
self.assertIn('price', form.errors)
self.assertIn('category', form.errors)
# Description is optional
self.assertNotIn('description', form.errors)
def test_negative_price_validation(self):
invalid_data = self.form_data.copy()
invalid_data['price'] = -10.00
form = ProductForm(data=invalid_data)
self.assertFalse(form.is_valid())
self.assertIn('price', form.errors)
def test_name_max_length(self):
invalid_data = self.form_data.copy()
invalid_data['name'] = 'A' * 101 # Assuming max_length is 100
form = ProductForm(data=invalid_data)
self.assertFalse(form.is_valid())
self.assertIn('name', form.errors)
def test_custom_clean_method(self):
# Assuming the form has custom clean method that validates
# that premium products (price > 1000) must have a description
invalid_data = self.form_data.copy()
invalid_data['price'] = 1500.00
invalid_data['description'] = ''
form = ProductForm(data=invalid_data)
self.assertFalse(form.is_valid())
self.assertIn('__all__', form.errors) # Non-field error
def test_form_save(self):
form = ProductForm(data=self.form_data)
self.assertTrue(form.is_valid())
product = form.save()
self.assertEqual(product.name, 'Test Product')
self.assertEqual(product.price, 29.99)
self.assertEqual(product.category, self.category)
Testing ModelForms
from django.test import TestCase
from django.core.files.uploadedfile import SimpleUploadedFile
from myapp.forms import ProfileForm
from myapp.models import Profile
class ProfileFormTests(TestCase):
def setUp(self):
self.form_data = {
'name': 'John Doe',
'email': 'john@example.com',
'bio': 'Test bio'
}
# For testing file uploads
self.image = SimpleUploadedFile(
name='test_image.jpg',
content=b'file_content',
content_type='image/jpeg'
)
def test_form_with_file_upload(self):
form_data = self.form_data.copy()
form_data['avatar'] = self.image
form = ProfileForm(data=form_data, files={'avatar': self.image})
self.assertTrue(form.is_valid())
profile = form.save()
self.assertEqual(profile.name, 'John Doe')
self.assertTrue(profile.avatar.name.endswith('.jpg'))
def test_email_uniqueness(self):
# Create a profile with the same email
Profile.objects.create(
name='Existing User',
email='john@example.com'
)
form = ProfileForm(data=self.form_data)
self.assertFalse(form.is_valid())
self.assertIn('email', form.errors)
Testing Django Templates and URLs
Template Testing
Templates are typically tested indirectly through view tests, but you can also test them directly:
from django.test import TestCase
from django.template import Context, Template
class TemplateTagTests(TestCase):
def test_custom_template_tag(self):
# Load template with {% load custom_tags %}
template = Template(
'{% load custom_tags %}'
'{% format_currency 12.34 "USD" %}'
)
rendered = template.render(Context({}))
self.assertEqual(rendered, '$12.34')
def test_template_filter(self):
template = Template(
'{% load custom_filters %}'
'{{ "hello world"|capitalize }}'
)
rendered = template.render(Context({}))
self.assertEqual(rendered, 'Hello World')
URL Testing
from django.test import TestCase
from django.urls import reverse, resolve
from myapp.views import ProductListView, ProductDetailView
class UrlsTests(TestCase):
def test_product_list_url(self):
url = reverse('product-list')
self.assertEqual(url, '/products/')
# Test URL resolution
resolver = resolve('/products/')
self.assertEqual(resolver.func.__name__, ProductListView.as_view().__name__)
def test_product_detail_url(self):
url = reverse('product-detail', args=[1])
self.assertEqual(url, '/products/1/')
resolver = resolve('/products/1/')
self.assertEqual(resolver.func.__name__, ProductDetailView.as_view().__name__)
# Check kwargs
self.assertEqual(resolver.kwargs, {'pk': '1'})
def test_named_urls_in_templates(self):
response = self.client.get(reverse('home'))
self.assertContains(response, reverse('product-list'))
Testing Django Middleware and Authentication
Middleware Testing
from django.test import TestCase, RequestFactory
from django.http import HttpResponse
from myapp.middleware import CustomMiddleware
class MiddlewareTests(TestCase):
def setUp(self):
self.factory = RequestFactory()
# A simple view for testing middleware
def test_view(request):
return HttpResponse("Test view")
self.test_view = test_view
self.middleware = CustomMiddleware(self.test_view)
def test_middleware_modifies_request(self):
request = self.factory.get('/test/')
response = self.middleware(request)
# Check that middleware modified request
self.assertTrue(hasattr(request, 'middleware_was_here'))
def test_middleware_modifies_response(self):
request = self.factory.get('/test/')
response = self.middleware(request)
# Check that middleware modified response
self.assertEqual(response['X-Middleware-Header'], 'Value')
Authentication Testing
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth.models import User
class AuthenticationTests(TestCase):
def setUp(self):
self.client = Client()
self.login_url = reverse('login')
self.protected_url = reverse('profile')
# Create test user
self.user = User.objects.create_user(
username='testuser',
email='test@example.com',
password='testpassword'
)
def test_login_view(self):
# GET request should show form
response = self.client.get(self.login_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'login.html')
# Incorrect login
response = self.client.post(self.login_url, {
'username': 'testuser',
'password': 'wrongpassword'
})
self.assertEqual(response.status_code, 200) # Stay on login page
self.assertFalse(response.context['user'].is_authenticated)
# Correct login
response = self.client.post(self.login_url, {
'username': 'testuser',
'password': 'testpassword'
}, follow=True) # follow=True follows redirects
self.assertTrue(response.context['user'].is_authenticated)
self.assertRedirects(response, reverse('home'))
def test_protected_view(self):
# Unauthenticated access
response = self.client.get(self.protected_url)
self.assertEqual(response.status_code, 302) # Redirect to login
# Login
self.client.login(username='testuser', password='testpassword')
# Access protected view
response = self.client.get(self.protected_url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'profile.html')
def test_user_permissions(self):
# Create admin user
admin = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='adminpassword'
)
# Regular user can't access admin
self.client.login(username='testuser', password='testpassword')
response = self.client.get(reverse('admin:index'))
self.assertEqual(response.status_code, 302) # Redirect
# Admin can access admin
self.client.login(username='admin', password='adminpassword')
response = self.client.get(reverse('admin:index'))
self.assertEqual(response.status_code, 200)
Real-world example: Pinterest, which uses Django, has a thorough authentication test suite that verifies their multi-factor authentication flows, social logins, and permission checks.
Using Test Fixtures in Django
Django provides multiple ways to set up test data:
Using setUp method
def setUp(self):
self.user = User.objects.create_user(...)
self.product = Product.objects.create(...)
Using fixtures files
# fixtures/categories.json
[
{
"model": "myapp.category",
"pk": 1,
"fields": {
"name": "Electronics",
"description": "Electronic devices"
}
},
{
"model": "myapp.category",
"pk": 2,
"fields": {
"name": "Books",
"description": "Paper and digital books"
}
}
]
# In your test class
class ProductTests(TestCase):
fixtures = ['categories.json', 'products.json']
def test_something(self):
# Fixtures are loaded, you can use them
category = Category.objects.get(name="Electronics")
self.assertEqual(category.id, 1)
Using factory_boy
factory_boy is a popular third-party library for creating test data:
# factories.py
import factory
from django.contrib.auth.models import User
from myapp.models import Category, Product
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')
is_active = True
@factory.post_generation
def set_password(self, create, extracted, **kwargs):
self.set_password('password')
self.save()
class CategoryFactory(factory.django.DjangoModelFactory):
class Meta:
model = Category
name = factory.Sequence(lambda n: f'Category {n}')
description = factory.Faker('sentence')
class ProductFactory(factory.django.DjangoModelFactory):
class Meta:
model = Product
name = factory.Sequence(lambda n: f'Product {n}')
price = factory.Faker('pydecimal', left_digits=3, right_digits=2, positive=True)
category = factory.SubFactory(CategoryFactory)
description = factory.Faker('paragraph')
in_stock = True
# In your tests
from myapp.factories import UserFactory, ProductFactory
class ProductTests(TestCase):
def setUp(self):
self.user = UserFactory()
self.products = ProductFactory.create_batch(10)
def test_product_listing(self):
# 10 products have been created
self.client.login(username=self.user.username, password='password')
response = self.client.get(reverse('product-list'))
self.assertEqual(len(response.context['products']), 10)
Mocking in Django Tests
The unittest.mock module is useful for isolating tests from external dependencies:
from django.test import TestCase
from unittest.mock import patch, MagicMock
from myapp.services import PaymentService
from myapp.views import make_payment
class PaymentTests(TestCase):
@patch('myapp.services.PaymentService.process_payment')
def test_payment_success(self, mock_process):
# Configure the mock
mock_process.return_value = {
'success': True,
'transaction_id': '12345'
}
# Make the request
response = self.client.post(reverse('make-payment'), {
'amount': '100.00',
'card_number': '4111111111111111',
'expiry': '12/25',
'cvv': '123'
})
# Check that the mock was called with correct args
mock_process.assert_called_once()
call_args = mock_process.call_args[0]
self.assertEqual(call_args[0], '100.00')
# Check response
self.assertEqual(response.status_code, 302) # Redirect on success
self.assertIn('success', response.url)
@patch('myapp.services.PaymentService.process_payment')
def test_payment_failure(self, mock_process):
# Configure the mock to simulate failure
mock_process.return_value = {
'success': False,
'error': 'Insufficient funds'
}
# Make the request
response = self.client.post(reverse('make-payment'), {
'amount': '100.00',
'card_number': '4111111111111111',
'expiry': '12/25',
'cvv': '123'
})
# Check response
self.assertEqual(response.status_code, 200) # Stay on page
self.assertContains(response, 'Insufficient funds')
@patch('requests.post')
def test_external_api_call(self, mock_post):
# Mock the external API call
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {'status': 'success'}
mock_post.return_value = mock_response
service = PaymentService()
result = service.process_payment('100.00', '4111111111111111', '12/25', '123')
# Check the API was called correctly
mock_post.assert_called_once_with(
'https://payment.example.com/api/v1/payments',
json={
'amount': '100.00',
'card_number': '4111111111111111',
'expiry': '12/25',
'cvv': '123'
},
headers={'Authorization': 'Bearer API_KEY'}
)
# Check result
self.assertTrue(result['success'])
Testing Django Admin
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
from myapp.models import Product
from myapp.admin import ProductAdmin
class AdminTests(TestCase):
def setUp(self):
# Create superuser
self.admin_user = User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password'
)
self.client.login(username='admin', password='password')
# Create test data
self.product = Product.objects.create(
name="Test Product",
price=19.99,
description="Test description"
)
# URLs
self.admin_url = reverse('admin:index')
self.product_list_url = reverse('admin:myapp_product_changelist')
self.product_add_url = reverse('admin:myapp_product_add')
self.product_change_url = reverse(
'admin:myapp_product_change', args=[self.product.id]
)
def test_admin_accessible(self):
response = self.client.get(self.admin_url)
self.assertEqual(response.status_code, 200)
def test_product_listed(self):
response = self.client.get(self.product_list_url)
self.assertContains(response, "Test Product")
def test_product_edit(self):
response = self.client.get(self.product_change_url)
self.assertEqual(response.status_code, 200)
# Update product
response = self.client.post(self.product_change_url, {
'name': 'Updated Product',
'price': '29.99',
'description': 'Updated description'
})
# Should redirect after successful edit
self.assertEqual(response.status_code, 302)
# Check product was updated
self.product.refresh_from_db()
self.assertEqual(self.product.name, 'Updated Product')
def test_custom_admin_action(self):
# Test a custom admin action
response = self.client.post(self.product_list_url, {
'action': 'mark_as_featured',
'_selected_action': [self.product.id]
})
# Check product was updated
self.product.refresh_from_db()
self.assertTrue(self.product.is_featured)
Integration with PyTest
While Django's built-in test framework is powerful, many developers prefer to use PyTest for its additional features:
# Install pytest-django
pip install pytest-django
# pytest.ini or pyproject.toml
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = test_*.py *_test.py
django_find_project = true
Using PyTest Fixtures with Django
# conftest.py
import pytest
from django.contrib.auth.models import User
from myapp.models import Product, Category
@pytest.fixture
def user():
return User.objects.create_user(
username='testuser',
email='test@example.com',
password='password'
)
@pytest.fixture
def admin_user():
return User.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password'
)
@pytest.fixture
def category():
return Category.objects.create(name="Electronics")
@pytest.fixture
def product(category):
return Product.objects.create(
name="Test Product",
price=19.99,
category=category,
description="Test description"
)
# test_views.py
import pytest
from django.urls import reverse
@pytest.mark.django_db
def test_product_detail(client, product):
url = reverse('product-detail', args=[product.id])
response = client.get(url)
assert response.status_code == 200
assert "Test Product" in str(response.content)
@pytest.mark.django_db
def test_login_required(client, admin_client, product):
url = reverse('product-edit', args=[product.id])
# Anonymous user
response = client.get(url)
assert response.status_code == 302 # Redirect to login
# Admin user
response = admin_client.get(url)
assert response.status_code == 200
Real-world example: Mozilla's Django projects use pytest-django for its better integration with their CI/CD pipelines and for the ability to use parameterized tests more easily than with Django's built-in test framework.
Testing for Performance and Security
Performance Testing
from django.test import TestCase
from django.test.utils import CaptureQueriesContext
from django.db import connection
from django.urls import reverse
class PerformanceTests(TestCase):
def setUp(self):
# Create test data
self.categories = [Category.objects.create(name=f"Category {i}") for i in range(5)]
for i in range(20):
Product.objects.create(
name=f"Product {i}",
price=10 + i,
category=self.categories[i % 5]
)
def test_query_count(self):
with CaptureQueriesContext(connection) as context:
response = self.client.get(reverse('product-list'))
# Check that view doesn't execute too many queries
self.assertLess(len(context), 5)
def test_prefetch_related(self):
with CaptureQueriesContext(connection) as context:
# This view should prefetch related categories
response = self.client.get(reverse('product-list-optimized'))
# Should use fewer queries than non-optimized view
query_count = len(context)
with CaptureQueriesContext(connection) as context:
# This view does not prefetch
response = self.client.get(reverse('product-list-unoptimized'))
# Non-optimized view should use more queries
self.assertGreater(len(context), query_count)
Security Testing
from django.test import TestCase
from django.urls import reverse
from django.contrib.auth.models import User
class SecurityTests(TestCase):
def setUp(self):
self.regular_user = User.objects.create_user(
username='user',
password='password'
)
self.other_user = User.objects.create_user(
username='otheruser',
password='password'
)
self.admin_user = User.objects.create_superuser(
username='admin',
password='password'
)
def test_csrf_protection(self):
# Login
self.client.login(username='user', password='password')
# Try to post without CSRF token
self.client.handler.enforce_csrf_checks = True
response = self.client.post(reverse('change-password'), {
'new_password1': 'newpassword',
'new_password2': 'newpassword'
})
# Should be forbidden
self.assertEqual(response.status_code, 403)
def test_authorization(self):
# Regular user can't access other user's profile
self.client.login(username='user', password='password')
response = self.client.get(
reverse('user-profile', args=[self.other_user.id])
)
self.assertEqual(response.status_code, 403)
# Admin can access any profile
self.client.login(username='admin', password='password')
response = self.client.get(
reverse('user-profile', args=[self.other_user.id])
)
self.assertEqual(response.status_code, 200)
def test_xss_prevention(self):
# Create product with potential XSS content
product = Product.objects.create(
name='',
price=19.99
)
response = self.client.get(reverse('product-detail', args=[product.id]))
# The script tag should be escaped in the response
self.assertNotIn('', str(response.content))
self.assertIn('<script>alert("XSS")</script>', str(response.content))
Practical Exercise
Exercise: Testing a Blog Application
Let's practice by writing tests for a simple blog application:
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
published = models.BooleanField(default=False)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', args=[self.id])
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Comment by {self.author.username} on {self.post.title}"
# blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from .models import Post, Comment
from .forms import PostForm, CommentForm
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
def get_queryset(self):
queryset = super().get_queryset()
if self.request.user.is_staff:
return queryset
return queryset.filter(published=True)
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comment_form'] = CommentForm()
return context
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
model = Post
form_class = PostForm
template_name = 'blog/post_form.html'
def test_func(self):
post = self.get_object()
return self.request.user == post.author
class CommentCreateView(LoginRequiredMixin, CreateView):
model = Comment
form_class = CommentForm
def form_valid(self, form):
form.instance.author = self.request.user
form.instance.post_id = self.kwargs['pk']
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('post-detail', kwargs={'pk': self.kwargs['pk']})
Your task:
- Write tests for the Post and Comment models
- Test the PostListView to ensure it filters unpublished posts correctly
- Test the PostDetailView and its context
- Test the CreateView and UpdateView with permission checks
- Test the CommentCreateView
Summary
- Django's testing framework provides powerful tools for testing web applications
- Use TestCase for most tests, SimpleTestCase when you don't need the database, and TransactionTestCase for testing transactions
- Test models, views, forms, templates, URLs, and admin thoroughly
- Use fixtures and libraries like factory_boy to set up test data
- Use mocking to isolate tests from external dependencies
- Test for performance by monitoring query counts
- Test for security by checking CSRF protection, authorization, and XSS prevention
- Consider using PyTest with Django for additional features
Remember: Thorough testing of Django applications helps prevent regressions, ensures security, and provides documentation of expected behavior.
Assignment
Create a complete test suite for a Django e-commerce application with the following features:
- Build a simplified e-commerce application with these models:
- Product (name, price, description, category, image)
- Category (name, description)
- Order (user, items, status, created_at)
- OrderItem (order, product, quantity, price)
- UserProfile (user, address, phone)
- Implement these views:
- Product list and detail views
- Shopping cart
- Checkout process
- Order history
- User profile management
- Write a comprehensive test suite that:
- Tests all models, views, and forms
- Verifies authentication and permissions
- Tests the checkout workflow
- Checks for query optimization
- Implements both unit and integration tests
- Use fixtures or factory_boy for test data
- Implement both Django's TestCase and pytest-django tests
- Test the admin interface customizations
Bonus challenge: Implement and test a REST API for the application using Django REST Framework.