Quick Note: Setting Up Pytest Model Factory Fixtures for Django

📝 Note

This will not be like my standard "Quick Note". It is a little bit longer. By the way, you can also check other "Quick Note" in my blog.

Generating model instances in Django test is vital and it needs to be done quick and in a painless way. In this article, I'd like to present a common pattern that I've figured out.

Factory Fixtures

I'd like to first discuss about factory fixtures. These are fixtures that simply return functions. To give an example:

@pytest.fixture
def foo_factory():
    def factory(number):
        return "foo{}".format(number)
    return factory

This way we can simply inject them into our tests and invoke them on the fly.

def test_foo(foo_factory):
    assert foo_factory(1) == "foo1"

This is useful when you'd like to generate instances based on some context and especially useful when creating model instances.

Model Instance Factories

Assuming we have a model as below:

class Person(models.Model):
    name = models.CharField(max_length=128)
    surname = models.CharField(max_length=128)

We can construct a factory fixture as below:

@pytest.fixture
def person_factory():
    def factory(**kwargs):
        return Person.objects.create(**kwargs)
    return factory

So now we can create many instances as we need in a test:

def test_person(person_factory):
    person = person_factory(name="Eray", surname="Erdin")
    assert (person.name, person.surname) == ("Eray", "Erdin")

While this might seem good, it is not still perfect. We would probably like to create an instance quickly, without providing any data such as:

def test_person(person_factory):
    person = person_factory()  # what we'd like to do
    assert (person.name, person.surname) == ("Eray", "Erdin")

This will probably fail because name and surname fields are implicitly NOT NULL.

IntegrityError: NOT NULL constraint failed: app_person.name

Setting Defaults

What we'd like to do is to write our fixture in such a way that it will provide a default value to the field that's not provided. We provide the values with kwargs and it's a dict of keyword-only arguments. Since it is a plain dict, there is a helper method called setdefault for dict. This method adds the key and value if the key does not exist. With this lore, see the example below:

@pytest.fixture
def person_factory():
    def factory(**kwargs):
        kwargs.setdefault("name", "Eray")
        kwargs.setdefault("surname", "Erdin")
        return Person.objects.create(**kwargs)
    return factory

So name and surname will have a default value and will not needed to be provided.

def test_person(person_factory):
    person = person_factory(name="Şenay")  # now this won't error
    assert (person.name, person.surname) == ("Şenay", "Erdin")  # `surname` defaults to `"Erdin"` in this case

Unique Constraint

However, this is not the end yet. In this case, neither name nor surname fields have unique constraints. Let's add a field with unique to our model.

class Person(models.Model):
    name = models.CharField(max_length=128)
    surname = models.CharField(max_length=128)
    phone = models.CharField(max_length=128, unique=True)  # new field

⚠️ Warning

In this example, I do not really care about the formatting and validation of phone field. It's added only for the sake of example. I will count "1", "2", "3" and such values as valid.

And we should update our related factory fixture accordingly:

@pytest.fixture
def person_factory():
    def factory(**kwargs):
        kwargs.setdefault("name", "Eray")
        kwargs.setdefault("surname", "Erdin")
        kwargs.phone("phone", "1")  # See warning above.
        return Person.objects.create(**kwargs)
    return factory

This is okay with one instance in a test, however will immediately fail at the initialization of the second instance.

def test_person(person_factory):
    person1 = person_factory()
    person2 = person_factory()  # will fail here
    assert (person1.name, person1.surname) == ("Eray", "Erdin")
    assert (person2.name, person2.surname) == ("Eray", "Erdin")

This will fail saying:

django.db.utils.IntegrityError: UNIQUE constraint failed: app_person.phone

This is because the first time the factory is invoked the phone is "1" and the second time it is the same, yet we explicitly defined phone field to be unique.

These kind of errors usually require different solutions but the foundation is the same. We need to set unique field to a different value each time its factory fixture is invoked. In this example, I will use the count of Person instances and use them to set the phone field.

@pytest.fixture
def person_factory():
    def factory(**kwargs):
        kwargs.setdefault("name", "Eray")
        kwargs.setdefault("surname", "Erdin")
        kwargs.setdefault("phone", str(Person.objects.count()))  # person instance count as phone
        return Person.objects.create(**kwargs)
    return factory

ℹ️ Tip

You can also format strings as below:

"foo{}".format(bar)

This is what I usually use with CharFields and alikes.

Now we will not get those IntegrityErrors because the phone will be different in each invocation.

def test_person(person_factory):
    person1 = person_factory()  # phone: "0"
    person2 = person_factory()  # phone: "1"
    assert (person1.name, person1.surname) == ("Eray", "Erdin")
    assert (person2.name, person2.surname) == ("Eray", "Erdin")

Relations

This pattern also lets us create instances with relations pretty easily. This time, let's use a different kind of example with two models where they are bound by a ForeignKey.

class Category(models.Model):
    name = models.CharField(max_length=128)
    # other fields if necessary ...

class Product(models.Model):
    price = models.DecimalField()
    category = models.ForeignKey(Category, models.CASCADE, "products")
    # other fields if needed ...

⚠️ Warning

In this example, I assume that each Product will have only one Category and a Category is required.

In this case, if I'd like to create a Product instance, I will definitely need a Category instance as well. First, write up our standard Category factory fixture:

@pytest.fixture
def category_factory():
    def factory(**kwargs):
        kwargs.setdefault("name", "foo")
        # same for other fields
        return Category.objects.create(**kwargs)
       return factory

And the factory fixture for Product. Remember we will need a Category instance for that.

@pytest.fixture
def product_factory(category_factory):  # see how i injected `category_factory`
    def factory(**kwargs):
        kwargs.setdefault("price", 1.5)
        kwargs.setdefault("category", category_factory())  # and i invoked `category_factory` here
        # same for other fields
        return Product.objects.create(**kwargs)
    return factory

Finally, whenever I need a Product instance in test, I can only inject product_factory and invoke it. It will implicitly create a Category instance for itself.

@pytest.fixture
def test_product(product_factory):
    product = product_factory()
    assert product.price == 1.5
    assert product.category.name == "foo"

Final Words

This is how I deal with generating models on the test and I do that with each model and update the factory fixtures when I update models. I will put some possible questions/thoughts in your mind in case you are interested.

User is a built-in model in Django. Do you know a method for User model?

Ah, yes. I can even give away the code.

@pytest.fixture
def author_factory(db):
    def factory(**kwargs):
        total_instances = User.objects.count()
        username = f"user{total_instances}"  # (i)
        email = f"foo{total_instances}@bar.baz"

        kwargs.setdefault("username", username)
        kwargs.setdefault("password", "111111")
        kwargs.setdefault("email", email)

        return User.objects.create_user(**kwargs)  # (ii)

    return factory

As you probably can see, (i) I generate username based on the user instances and (ii) I have used create_user instead of create mainly because it is a special method for User's manager.

Nice, now I can generate a lot of instances with faker (or alike).

While I find faker to be useful in some cases, I recommend to avoid overusing it. I do not usually use it because, in my opinion, you usually test the logic instead of the content.

⚠️ Warning

In the example below, I assume you use client fixture of Pytest Django and you have already written required models, views, serializers and factory fixtures.

To give a more concrete example (also assuming it's going to be an API endpoint and will use Django Rest Framework), let's assume that you have an endpoint /whatever/ that will return a list of, well, whatevers and those objects are related to Users.

# model of it
class Whatever(models.Model):
    # ...
    user = models.ForeignKey(User)
    # ...

So, if user1 is logged in, he will see his whatevers and not user2's whatevers.

In this case, it is just enough to create 2 instances of Whatever for user1 and 1 instance of Whatever for user2 and do the test.

def test_whatever_list_endpoint(client, user_factory, whatever_factory):  # assuming you've written factory for it
    user1 = user_factory()  # (ii)
    user2 = user_factory()  # (ii)

    for i in range(2):  # 2 whatevers for user1 # (i)
        whatever_factory(user=user1)

    whatever_factory(user=user2)   # 1 whatever for user2 # (i)

    url = reverse("whatever-list")  # from django.urls import reverse

    # whatever list of user1
    client.login(username=user1.username, password="111111")  # (ii)
    data1 = client.get(url).json()
    client.logout()

    # whatever list of user2
    client.login(username=user2.username, password="111111")  # (ii)
    data2 = client.get(url).json()

    # assertions
    assert len(data["results"]) == 2  # because we've created 2 for user1 # (i)
    assert len(data["results"]) == 1  # because we've created 1 for user2 # (i)

In this context, you can notice many things:

  • (i) I have never needed the test the content (the other fields and values or its serializer's values) of Whatever instances. I do not need to because they will most likely render the same. You should test whenever your content is bound by a logic.
  • (ii) By not using faker, I could keep the content of User instances predictable and consistent. I know the password for every testing user will be "111111" and I know I would not (and you should never attempt to) test the password anyway.

No Comments Yet