Generating Fake Django Model Instances with Factory Boy
Tags: webdev, django, factoryboy, python
Published: 2024-05-01 12:00:00

As you might know, I am developing [PyBazaar](https://pybazaar.com), a Python Developer Marketplace. For a project of that scope, I need to create hundreds or thousands of data entries to ensure that everything works as expected. Factory Boy is a tool that allows me to create model instances in batches, and this blog post is about it.

## The benefits of using Factory Boy

By creating a bunch of fake entries, I can achieve the following:

- Work on list and detail representation and styling.
- Work on and try functionality like filters, sorting, and pagination.
- Check and improve performance with loads of data entries.
- Create dummy data for unit or functional tests.

Factory Boy seemed like a pretty complex package, so I want to simplify things and introduce you to all the necessary parts for creating a fake model instances.

## Model preparation

At PyBazaar, I have users with profiles, job offers, and resources that can be faked in batch. The related categories are predefined and don't need to be faked.

To make it possible to distinguish between real and fake entries, I added a new boolean field `is_fake` to all those models that I can create in batch:

```python
# For testing and debugging
is_fake = models.BooleanField(_("Fake"), default=False)
```

Here is what the list of profiles can look like when created with Factory Boy:


![Generated user list with Factory Boy at PyBazaar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ziyd5hp9dfc7kaez74kk.png)

## The setup

The installation is pretty straightforward:

```bash
(venv)$ pip install factory-boy==3.3.0
```

And then in each app where you need to create fake entries, create a file `factories.py` with factory classes, e.g.:

```python
import random
import factory
from pybazaar.apps.accounts.models import User

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    
    # ...
    
    is_fake = True
```

For factory classes, I also add custom class methods `delete_fake()` and `recreate_batch()` so that I can quickly create entries or delete them:

```python
class ProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)

    # ...
        
    is_fake = True

    @classmethod
    def recreate_batch(cls, size, **kwargs):
        cls.delete_fake()
        cls.create_batch(size=size, **kwargs)

    @classmethod
    def delete_fake(cls):
        for profile in Profile.objects.filter(is_fake=True):
            profile.delete()
        for user in User.objects.filter(is_fake=True):
            user.delete()
```

Factory class attributes tell the system what values to assign to the models when creating instances. Let's explore multiple cases that we can use as values.

## Assigning a static value

If it's a simple static value, you can just assign it. It will be the same for all fake entries:

```python
is_fake = True
publishing_status = Profile.PublishingStatusChoices.PUBLISHED
```

## Assigning a value from a list

If it's a value from a list, use the `Iterator` class:

```python
title = factory.Iterator([
    "Developer",
    "Software Engineer",
    "Programmer",
])

experience_level = factory.Iterator(
    Profile.ExperienceLevelChoices.values
)
```

## Assigning generated value of a certain type

Factory Boy uses the Faker package to allow the creating of fake names, paragraphs, or locations. You can use those as follows:

```python
first_name = factory.Faker("first_name")
last_name = factory.Faker("last_name")
summary = factory.Faker("paragraph")
city = factory.Faker("city")
state = factory.Faker("state")
country = factory.Faker("country_code")
```

## Assigning an instance

If it's a foreign key and you want a random value, use this:

```python
resource_type = factory.LazyAttribute(
    lambda o: ResourceType.objects.order_by("?").first()
)
```

## Assigning a value from a function

Similarly, you can assign a value from a function:

```python
description = factory.LazyAttribute(
    lambda o: generate_quill_content()
)
```

## Assigning a random value

Or a random value:

```python
is_available_for_work = factory.LazyAttribute(
    lambda o: random.choice([True, False])
)
```

## Assigning a value based on attributes or methods of the model instance

Once you define attributes like `first_name` or `last_name`, you can set other values depending on those:

```python
username = factory.LazyAttribute(
    lambda o: f"{o.first_name}_{o.last_name}".lower()
)
email = factory.LazyAttribute(
    lambda o: f"{o.first_name}_{o.last_name}@example.com".lower()
)
```

## Assigning a password

There is a special `django.Password` class for generating password values:

```python
password = factory.django.Password("Pa$$w0rd")
```


## Assigning dummy images

Here is how to create and assign a dummy single-color image:

```python
avatar = factory.django.ImageField(
    width=200, height=200, color="rgb(2,132,199)"
)
```

## Having two factories depending on each other

As we have profiles depending on users, we can define the codependence with `SubFactory` class.

```python
class ProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)
```

Then, when creating profiles, the users will be created automatically for them, too.

## Attaching many-to-many relations

In Django, many-to-many relationships must be added after creating a model instance. We can achieve that with the `PostGeneration` class:

```python
def attach_categories(obj, create, extracted, **kwargs):
    obj.specializations.add(
        *list(Specialization.objects.order_by("?")[: random.randint(3, 7)])
    )

class ProfileFactory(factory.django.DjangoModelFactory):
    # ...
    do_afterwards = factory.PostGeneration(attach_categories)
```

How you call this attribute doesn't matter - it should just not clash with other field names or attributes.

## A complete example

So the final `factories.py` file could look like this:

```python
import random
import factory
import json
from pybazaar.apps.accounts.models import User
from pybazaar.apps.profiles.models import Profile
from pybazaar.apps.categories.models import Specialization

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    first_name = factory.Faker("first_name")
    last_name = factory.Faker("last_name")
    username = factory.LazyAttribute(
        lambda o: f"{o.first_name}_{o.last_name}".lower()
    )
    email = factory.LazyAttribute(
        lambda o: f"{o.first_name}_{o.last_name}@example.com".lower()
    )
    password = factory.django.Password("Pa$$w0rd")
    is_fake = True

def generate_quill_content():
    return json.dumps(
        {
            "delta": '',
            "html": "<p>Hey there</p>",
        }
    )

def attach_categories(obj, create, extracted, **kwargs):
    obj.specializations.add(
        *list(Specialization.objects.order_by("?")[: random.randint(3, 7)])
    )

class ProfileFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Profile

    user = factory.SubFactory(UserFactory)
    title = factory.Iterator(
        [
            "Developer",
            "Software Engineer",
            "Programmer",
        ]
    )
    avatar = factory.django.ImageField(
        width=200, height=200, color="rgb(2,132,199)"
    )
    bio = factory.LazyAttribute(lambda o: generate_quill_content())
    city = factory.Faker("city")
    state = factory.Faker("state")
    country = factory.Faker("country_code")
    is_available_for_work = factory.LazyAttribute(
        lambda o: random.choice([True, False])
    )
    experience_level = factory.Iterator(
        Profile.ExperienceLevelChoices.values
    )
    publishing_status = Profile.PublishingStatusChoices.PUBLISHED
    is_fake = True

    do_afterwards = factory.PostGeneration(attach_categories)

    @classmethod
    def recreate_batch(cls, size, **kwargs):
        cls.delete_fake()
        cls.create_batch(size=size, **kwargs)

    @classmethod
    def delete_fake(cls):
        for profile in Profile.objects.filter(is_fake=True):
            profile.delete()
        for user in User.objects.filter(is_fake=True):
            user.delete()

```

## Creating fake entries

Lastly, I can create the fake entries from the Django shell as follows:

```python
>>> from pybazaar.apps.profiles.factories import ProfileFactory
>>> ProfileFactory.recreate_batch(100)
```

And later, when I don't need those anymore:

```python
>>> from pybazaar.apps.profiles.factories import ProfileFactory
>>> ProfileFactory.delete_fake()
```

Whenever I add new fields to the models, I can easily tweak the factories and recreate the whole bunch of models in one step.

## Final words

Factory Boy doesn't guarantee data validation and integrity. For example, city, state, and country will be three separate random values that don't match a real location. However, that is sufficient to test your website's basic look and feel or performance.

---
Cover image by [Google DeepMind](https://www.pexels.com/photo/an-artist-s-illustration-of-artificial-intelligence-ai-this-image-represents-the-ways-in-which-ai-can-solve-important-problems-it-was-created-by-vincent-schwenk-as-part-of-the-visualis-17485819/)