Test Fixtures & Factories

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.

Code examples

Django Fixtures

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

Factory Boy Basics

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

Using Factories in Tests

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.

Code Coverage

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

Key points

Concepts covered

Fixtures, Factory Boy, Faker, setUp, Test Data, Coverage