Table of Contents
Before adopting Django and Wagtail as my frameworks of choice for web development projects, I preferred Laravel. While there's a few things which Django handles differently to Laravel, one of the biggest things for my workflow was the lack of a built-in system for test model factories.
What is a factory?
A factory is a system that lets you easily mock data for your tests and creates as many records as you need for your test cases. When declaring a factory, you're giving yourself a tool to create database records for a given model with all the necessary fields pre-populated with randomised (and pre-declared) data.
What does Django use instead of factories?
Instead of programmatically creating dynamic data via factories, Django favours using fixtures.
"A fixture is a collection of files that contain the serialized contents of the database. Each fixture has a unique name, and the files that comprise the fixture can be distributed over multiple directories, in multiple applications."
Fixtures documentation
Creating fixtures requires you to manually create the data within a database and then serialise (save/export) it to a file. This file would typically be a CSV or TOML format and can then be loaded up at the beginning of your tests to provide data to test against.
While this approach is fine, it's not a workflow I'm personally comfortable with. I've found that it can be awkward and somewhat inflexible. When making database changes to a project with existing fixtures, updating the fixtures to match can be awkward and has cost me a fair amount of time when trying to adopt fixtures into my workflow.
factory-boy and wagtail-factories
To solve this problem I've used a combination of 2 excellent packages which are available on Pypi:
factory-boy
"As a fixtures replacement tool, it (factory-boy) aims to replace static, hard to maintain fixtures with easy-to-use factories for complex objects.
Instead of building an exhaustive test setup with every possible combination of corner cases, factory_boy allows you to use objects customized for the current test, while only declaring the test-specific fields"
factory-boy documentation
Defining a model factory is as easy as:
import factory
from . import models
class UserFactory(factory.Factory):
class Meta:
model = models.User
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
admin = False
This factory can no dynamically create users for your database with randomised first and last names.
You can then utilise the factory and optionally override these fields like follows:
>>> UserFactory()
<User: Lucy Murray>
>>> UserFactory(first_name="Jack", last_name="Whitworth")
<User: Jack Whitworth>
wagtail-factories
wagtail-factories is built on top of factory-boy to provide base factory classes for Wagtail page models.
import factory
from wagtail_factories import PageFactory
from . import models
class BlogIndexPageFactory(PageFactory):
title = factory.Faker("sentence", nb_words=4)
class Meta:
model = models.BlogIndexPage
class BlogPageFactory(PageFactory):
parent = factory.SubFactory(BlogIndexPageFactory)
title = factory.Faker("sentence", nb_words=4)
class Meta:
model = models.BlogPage
This example shows how we can use the wagtail_factories.PageFactory class to create page factories for our own models. These factories are using factory.Faker to dynamically title the pages, and then factory.SubFactory to automatically assign page parents where required.
Writing tests with factories
Finally, putting this all together, here's a short example of some unit tests written for a blog index page's get_context method with some extra commenting to help explain what's happening:
class BlogIndexPageTestCase(TestCase):
def test_get_context_returns_children_of_this_index(self):
"""Test get_context returns only blogs that are children of this index."""
# Create a blog index with 3 blog posts
expected_length = 3
blog_index = BlogIndexPageFactory()
blog_pages = BlogPageFactory.create_batch(expected_length, parent=blog_index)
# Create another blog index with 2 blog posts
other_blog_index = BlogIndexPageFactory()
other_blog_pages = BlogPageFactory.create_batch(2, parent=other_blog_index)
# Fetch the results of the `get_context` method
request = RequestFactory().get("/")
context = blog_index.get_context(request)
blog_entries = context["blog_entries"]
# Assert that we have the correct number of results
self.assertEqual(len(blog_entries), expected_length)
for blog in blog_entries:
# Assert the posts are those we created for `blog_index`
self.assertIn(blog, blog_pages)
self.assertNotIn(blog, other_blog_pages)