(guest@joequery.me)~ $ |

Handling unique constraints with factory boy

Suppose you are using Factory Boy to create a factory for the following example Django models.

class Group(models.Model):
    name = models.CharField(max_length=100, unique=True)

class User(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    group = models.ForeignKey(Group)

Notice our Group class has a unique constraint on the name column.

The problem

Now let's say our factories for these models looked like so:

class GroupFactory(factory.DjangoModelFactory):
    name = factory.Sequence(lambda n: 'Group {0}'.format(n))

    class Meta:
        models = models.Group

class AdminGroupFactory(GroupFactory):
    name = 'ADMIN'

class AdminUserFactory(factory.DjangoModelFactory):
    first_name = factory.Sequence(lambda n: 'Admin First {0}'.format(n))
    last_name = factory.Sequence(lambda n: 'Admin Last {0}'.format(n))
    group = factory.SubFactory(AdminGroupFactory)

    class Meta:
        models = models.User

In our AdminGroupFactory, we've hardcoded the name to 'ADMIN'. Perhaps our application looks for this special group when it comes to user permissions, and some tests expect this Group to exist.

However, if we ever instantiate AdminGroupFactory more than once at any point in a test (whether within the testcase itself or through SubFactory chaining), we'll recieve an error resembling the following:

duplicate key value violates unique constraint "app_group_name_key"
DETAIL:  Key (name)=('ADMIN') already exists.

In our example, creating two instances of AdminUserFactory will be enough to trigger the error. This is because each AdminUserFactory instantiation causes a AdminGroupFactory instantiation, resulting in an attempt to create multiple Group entries with the name 'ADMIN'. This violates the unique constraint.

Solution

Within the actual Django application, we have a way of quickly dealing with the issue of creating/retrieving a Model instance in a single step: get_or_create.

Factory boy provides a similar solution for the problem demonstrated above.

class GroupFactory(factory.DjangoModelFactory):
    name = factory.Sequence(lambda n: 'Group {0}'.format(n))

    class Meta:
        models = models.Group
        django_get_or_create = ('name',)

The django_get_or_create Meta option takes a tuple of field names you would like passed to Django's get_or_create. You should pass the This makes it safe to instantiate factories that may contain SubFactories with unique constraints.

You don't have to worry about passing all fields to the django_get_or_create option - factory boy ensures the fields not passed to the django_get_or_create will still have their values added as part of the creation process. (Proof)

Tagged as django, python, testing

Date published - May 13, 2015