Converting Django Tests to Use Factory Boy

I heard about Factory Boy this last summer at a Django meetup in LA and figured I’d give it a shot. I ended up using it on a small project and intending to write a post back then, but I got distracted and never finished the project. I’ve been working on another Django site the past month or two and decided today would be a good day to convert all of my partner’s code into using Factory Boy.

First off, Factory Boy is a tool for generating data in tests. Pretty much any project you have is going to need some way of doing this. There are basically 3 options that I can think of:

  1. Use fixtures – this works, but quickly becomes a pain in the ass to maintain. If you change a model, you need to modify your fixtures to keep them up to date.
  2. Use calls to your Model.objects.create() and pass in all the arguments you need. This works, but you end up repeating a lot of code.
  3. Create some sort of factory for building objects. I’ve tried all of these and think #3 is probably the best, and Factory Boy is a lot better implemented than the solution we came up with to solve #3 at Mahalo. Fixtures are very hard to maintain, and making create() calls all over your tests leads to a lot of repeated code.

What I’m doing now is converting #2 into #3. It’s a pretty simple process, here’s the basics for the set up:

  • Install factory_boy – check out the github page for details – https://github.com/dnerdy/factory_boy
  • Set up your app – there’s no specific way you have to do this, but I like to make tests.py into a module and put all my factories in a file inside of that. Here’s the steps for that:
    • $ mkdir myapp/tests
    • $ cp myapp/tests.py myapp/tests/tests.py
    • $ echo “from tests import *” > myapp/tests/__init__.py
    • From here you can start making your factories. I like to call my factory file factories.py ( myapp/tests/factories.py ), but you can call it whatever you want. Don’t call it factory.py though, that gave me some import issues because the package itself is called factory.
  • From here you’re good to start converting your code over.
For me, I decided to do it one model at a time. So let’s look at some code (I’m going to keep it pretty simple, but you’ll get the idea) Here’s my existing code. In this example, the view displays the products in the database, so we create two products and then check to see that they are displayed in this view:
import django.test as django_test
import products.models as products_models
class ProductViewTests(django_test.TestCase):
    def test_basic_view(self):
        product1 = products_models.Product.objects.create(name="Test Product",
            features="Awesome Features")
        product2 = products_models.Product.objects.create(name="Another Test Product",
            features="Even More Awesome Features")
        c = django_test.client.Client()

        response = c.get("/products/")
        self.assertContains(response, product1.name)
        self.assertContains(response, product1.features)
        self.assertContains(response, product2.name)
        self.assertContains(response, product2.features)

So, as you can see, we need a factory to create product objects, let’s put that into our factories.py file:

import factory
from products.models import Product
class ProductFactory(factory.Factory): # factory boy knows this is for the Product model
    name = "Test Product"
    features = "Awesome feature set brah!"

That’s technically all I need to start using the factory to make objects in the database (or not in the database if you don’t want to save them, but that’s up to you to figure out — hint: it’s in the docs.) However, what if you want automagically make objects that don’t all have the same name, you can modify your factory a bit. Factory Boy provides some cool ways to automate things. For this we can use the Sequence object:

    name = factory.Sequence(lambda n: 'Test Product {0}'.format(n))

This will make Products with names like ‘Test Product 1″, “Test Product 2”, etc. Pretty cool huh?

Ok, back to our test code. Let’s pull out the two object creation lines and replace them with our factory.

import django.test as django_test
import products.models as products_models
from products.tests.factories import ProductFactory
# One note here: I tried to import this like the others:
# import products.tests.factories as prod_fac - but it
# kept giving me an import error, so I gave up
class ProductViewTests(django_test.TestCase):
    def test_basic_view(self):
        product1 = ProductFactory()
        product2 = ProductFactory()
        c = django_test.client.Client()

        response = c.get("/products/")
        self.assertContains(response, product1.name)
        self.assertContains(response, product1.features)
        self.assertContains(response, product2.name)
        self.assertContains(response, product2.features)

There you have it. There are a lot more features to Factory Boy, such as having a model generate any other model dependencies it has, but this post is pretty long already, so I’ll save that for later. You can always check out the docs on github too, they’re good.

4 thoughts on “Converting Django Tests to Use Factory Boy

  1. Thanks for the article. I’m have recently burned all my fixtures and switched to using factory_boy. I’m just wondering if there’s a clean way of creating a Factory for a Model that has m2m field in it ( instead of doing foo.bars.add(BarFactory()) ). Thanks in advance. Cheers.

    Like

  2. Thanks for this article.

    One correction:

    > I like to make tests.py into a module and put all my factories in a file inside of that.

    ‘tests.py’ is already a module. You mean “make a ‘tests’ package and put all my factories in a module inside of that” 🙂

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s