July 17, 2013
by Mark Lavin
0 comments
Categories:
Technical

Factory Boy as an Alternative to Django Testing Fixtures

When testing a Django application you often need to populate the test database with some sample data. The standard Django TestCase has support for fixture loading but there are a number of problems with using fixtures:

  • First, they must be updated each time your schema changes.
  • Second, they force you to hard-code dates which can create test failures when your date, which was “very far in the future” when the fixture was created, has now just passed.
  • Third, fixtures are painfully slow to load. They are discovered, deserialized and the data inserted at the start of every test method. Then at the end of the test that transaction is rolled back. Many times you didn’t even use the data in the fixture.

What is the alternative to fixtures? It’s simple: your test cases should create the data they need. Let’s take a simple model:

# models.py
from django.db import models


class Thing(models.Model):
    name = models.CharField(max_length=100)
    description = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name

Now we want to write some tests which need some Things in the database.

# tests.py
import random
import string

from django.test import TestCase

from .models import Thing


def random_string(length=10):
    return u''.join(random.choice(string.ascii_letters) for x in range(length))


class ThingTestCase(TestCase):

    def create_thing(self, **kwargs):
        "Create a random test thing."
        options = {
            'name': random_string(),
            'description': random_string(),
        }
        options.update(kwargs)
        return Thing.objects.create(**options)

    def test_something(self):
        # Get a completely random thing
        thing = self.create_thing()
        # Test assertions would go here

    def test_something_else(self):
        # Get a thing with an explicit name
        thing = self.create_thing(name='Foo')
        # Test assertions would go here

Instead of using a fixture we have a create_thing method to create a new Thing instance. In our tests we can get a new random thing object. In the cases where some of the fields need explicit values we can pass those into the creation. The tests create the exact number of Things that they need and any requirements about these instances is explicit in how they are created.

Writing these methods to create new instances can be rather repetitive. If you prefer, you can use something like Factory Boy to help you. Factory Boy is a Python port of a popular Ruby project called Factory Girl. It provides a declarative syntax for how new instances should be created. It also has helpers for common patterns such as sub-factories for foreign keys and other inter-dependencies. Rewriting our tests to use Factory Boy would look like this:

# tests.py
import random
import string

import factory
from django.test import TestCase

from .models import Thing


def random_string(length=10):
    return u''.join(random.choice(string.ascii_letters) for x in range(length))


class ThingFactory(factory.DjangoModelFactory):
    FACTORY_FOR = Thing

    name = factory.LazyAttribute(lambda t: random_string())
    description = factory.LazyAttribute(lambda t: random_string())


class ThingTestCase(TestCase):

    def test_something(self):
        # Get a completely random thing
        thing = ThingFactory.create()
        # Test assertions would go here

    def test_something_else(self):
        # Get a thing with an explicit name
        thing = ThingFactory.create(name='Foo')
        # Test assertions would go here

Here the create_thing method is removed in favor of the ThingFactory. Calling it is similar to how we were previously calling the method. One advantage of the ThingFactory is that we can also call ThingFactory.build() which will create an unsaved instance for tests where we don’t need the instance to be saved. Any time we can avoid a write to the database can save time for the test suite.

One handy pattern when working with models which have images is to have a test image file which is used by default. Let’s create a new model with an image.

# models.py
from django.db import models

class ImageThing(models.Model):
    name = models.CharField(max_length=100)
    image = models.ImageField(upload_to='images/')

    def __unicode__(self):
        return self.name

And a factory to test it

# tests.py
import os
import random
import string

import factory
from django.core.files import File

from .models import ImageThing

# Assumes there is a test.png next to our tests.py
TEST_IMAGE = os.path.join(os.path.dirname(__file__), 'test.png')


def random_string(length=10):
    return u''.join(random.choice(string.ascii_letters) for x in range(length))


class ImageThingFactory(factory.DjangoModelFactory):
    FACTORY_FOR = ImageThing

    name = factory.LazyAttribute(lambda t: random_string())
    image = factory.LazyAttribute(lambda t: File(open(TEST_IMAGE)))

Unlike the name this image isn’t random but by default all of your model instances will have images associated with them.

In summary, using fixtures for complex data structures in your tests is fraught with peril. They are hard to maintain and they make your tests slow. Creating model instances as they are needed is a cleaner way to write your tests which will make them faster and more maintainable. With tools like Factory Boy it can be very easy.

blog comments powered by Disqus