Difficulty: Advanced
Test data management is crucial for maintainable tests. Django provides fixtures (JSON/YAML files loaded into the database) and the community provides factory libraries (Factory Boy) for generating test data programmatically.
Django fixtures are JSON, XML, or YAML files that contain serialized model data. You create them with 'python manage.py dumpdata appname > fixture.json' and load them with 'python manage.py loaddata fixture.json'. In tests, add a 'fixtures' class attribute to load fixtures automatically before each test.
However, fixtures have significant drawbacks. They are brittle - any model change requires updating all fixtures. They are opaque - it is hard to see what data a test depends on. They are slow to maintain and do not compose well. For these reasons, most Django projects prefer factory-based test data.
Factory Boy is a popular library for generating test data. Instead of maintaining JSON fixtures, you define factories that know how to create model instances. Factories use sensible defaults and can be customized for specific tests. They integrate with Faker to generate realistic random data.
Factory Boy provides several factory types: Factory is the base class, DjangoModelFactory integrates with Django's ORM, SubFactory creates related objects, LazyAttribute computes values from other attributes, and Sequence generates unique values.
Faker generates realistic fake data: names, emails, addresses, texts, dates, and more. Factory Boy integrates with Faker through the factory.Faker() class. Each test run generates different random data, which can uncover edge cases that static fixtures miss.
Code coverage measures how much of your code is executed during tests. Use 'coverage run manage.py test' followed by 'coverage report' to see which lines are covered. Aim for high coverage on business logic but don't obsess over 100% - focus on testing behavior, not implementation details.
# Create a fixture from existing data
# $ python manage.py dumpdata blog.Article --indent 2 > blog/fixtures/articles.json
# blog/fixtures/articles.json
[
{
"model": "blog.article",
"pk": 1,
"fields": {
"title": "Test Article",
"content": "Test content",
"author": 1,
"is_published": true,
"created_at": "2024-01-15T10:30:00Z"
}
}
]
# Using fixtures in tests
class ArticleViewTest(TestCase):
fixtures = ['articles.json', 'users.json']
def test_article_list(self):
response = self.client.get('/articles/')
self.assertEqual(response.status_code, 200)
Fixtures are loaded before each test method. They are useful for small, stable datasets but become hard to maintain as models evolve. Each fixture file can contain data for multiple models.
# pip install factory-boy
# tests/factories.py
import factory
from django.contrib.auth import get_user_model
from blog.models import Article, Category
User = get_user_model()
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')
password = factory.PostGenerationMethodCall('set_password', 'testpass123')
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=5)
content = factory.Faker('paragraph', nb_sentences=10)
author = factory.SubFactory(UserFactory)
category = factory.SubFactory(CategoryFactory)
is_published = True
Factories define how to create model instances. Sequence generates unique values. LazyAttribute computes values from other fields. SubFactory creates related objects. Faker generates realistic random data.
from django.test import TestCase
from django.urls import reverse
from .factories import UserFactory, ArticleFactory
class ArticleViewTest(TestCase):
def test_article_list(self):
# Create 5 published articles
ArticleFactory.create_batch(5, is_published=True)
ArticleFactory.create_batch(3, is_published=False) # Not published
response = self.client.get(reverse('blog:article-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context['articles']), 5)
def test_article_detail(self):
article = ArticleFactory(title='Specific Title')
response = self.client.get(
reverse('blog:article-detail', kwargs={'pk': article.pk})
)
self.assertContains(response, 'Specific Title')
def test_create_requires_auth(self):
response = self.client.get(reverse('blog:article-create'))
self.assertEqual(response.status_code, 302) # Redirects to login
def test_create_article(self):
user = UserFactory()
self.client.login(username=user.username, password='testpass123')
data = {
'title': 'New Article',
'content': 'Article content here',
}
response = self.client.post(reverse('blog:article-create'), data)
self.assertEqual(response.status_code, 302)
self.assertTrue(
Article.objects.filter(title='New Article', author=user).exists()
)
Factories make tests readable and maintainable. create_batch() creates multiple instances. Override factory defaults by passing keyword arguments. Each test creates only the data it needs.
# Install coverage
# pip install coverage
# Run tests with coverage
# coverage run manage.py test
# View coverage report
# coverage report
# Name Stmts Miss Cover
# -----------------------------------------------
# blog/models.py 25 3 88%
# blog/views.py 40 8 80%
# blog/forms.py 15 2 87%
# -----------------------------------------------
# TOTAL 80 13 84%
# Generate HTML report
# coverage html
# Open htmlcov/index.html in browser
# .coveragerc configuration
# [run]
# source = .
# omit =
# */migrations/*
# */tests/*
# manage.py
# */wsgi.py
# */asgi.py
#
# [report]
# show_missing = True
# fail_under = 80
coverage measures which lines of code are executed during tests. The report shows statement count, missed lines, and coverage percentage. Set fail_under to enforce minimum coverage. HTML reports highlight uncovered lines.
Fixtures, Factory Boy, Faker, setUp, Test Data, Coverage