Mock Django models using Faker and Factory Boy

Mock Django models using Faker and Factory Boy

ยท

6 min read

In this post, you'll learn how real-world apps are mocking Django models by using Factory Boy. Before we start, make sure you have a basic understanding of TDD (Test-Driven Development). I will show you the simple working logic of mocking Django models and provide some key takeaways where you can easily apply them to your project.

Installation and Basic Usage

Install factory boy package by the following command:

pip install factory-boy

In a nutshell, factory boy will create a mocked instance of your class based on the values that you'll provide. Why even do we need this? Assume that you have a blog app where you want to test the posts and comments. You are going to need real examples of datasets to test functionalities more accurately. So instead of creating the data manually or by using fixtures, you can easily generate instances of a particular model by using factory boy.

The purpose of factory_boy is to provide a default way of getting a new instance, while still being able to override some fields on a per-call basis.

Now let's see how to mock dataclass in python. Create an empty directory named data and also add __init__.py file inside to mark it as a python package. We'll follow the example above which is going to generate the mocked Post instances.

data/models.py

from dataclasses import dataclass

@dataclass
class Post:
    title: str
    description: str
    published: bool
    image: str

Quick reminder about dataclass:

dataclass module is introduced in Python 3.7 as a utility tool to make structured classes specially for storing data - geeksforgeeks.org

We created a dataclass which holds four attributes. Now, this class needs a factory where we can create mocked instances.

data/factories.py

import factory
import factory.fuzzy

from data.models import Post

class PostFactory(factory.Factory):

    class Meta: 
        model = Post

    title = factory.Faker("name")
    description = factory.Faker("sentence")
    published = factory.fuzzy.FuzzyChoice(choices=[True, True, True, False])
    image = factory.Faker("image_url")

In Django, you'll need to inherit from factory.django.DjangoModelFactory class instead of just factory.Factory.

We are setting model = Post that defines the particular class we are going to mock.

The package has a built-in fake data generator class named FuzzyAttributes. However, in newer versions of factory_boy, they also included Faker class where you can use it to generate fake data. So, we used a few of them to mock our fields with fake data.

Now, it's time to put all these together:

main.py

from data.factories import PostFactory

posts = PostFactory.create_batch(10)

for post in posts:
    print(post)

The first argument of create_batch function takes a number (size) of generated items and allows override the factory class attributes.

python main.py

Output:

Post(title='Tracy Hernandez', description='Similar house wind bit win anything process even.', published=True, image='https://placekitten.com/209/389')
Post(title='Kimberly Henderson', description='Behavior wife phone agency door.', published=True, image='https://www.lorempixel.com/657/674')
Post(title='Jasmine Williams', description='Action experience cut loss challenge.', published=True, image='https://placekitten.com/365/489')
Post(title='Nicholas Moody', description='Consumer language approach risk event lose.', published=True, image='https://placekitten.com/756/397')
Post(title='Dr. Curtis Monroe', description='Firm member full.', published=True, image='https://dummyimage.com/238x706')
Post(title='David Martin', description='Join fall than.', published=False, image='https://dummyimage.com/482x305')
Post(title='Seth Oliver', description='Including most join resource heavy.', published=True, image='https://www.lorempixel.com/497/620')
Post(title='Daniel Berger', description='Summer mean figure husband read.', published=True, image='https://dummyimage.com/959x180')
Post(title='Samantha Romero', description='Window leader subject defense lawyer.', published=False, image='https://placeimg.com/965/518/any')
Post(title='Jessica Carroll', description='Would try religious opportunity future blood our.', published=True, image='https://placekitten.com/911/434')

Once you run the program, the output should look above, which means the factory successfully generated instances from our model class.

Advanced Usage in Django

Try to mock each model with factory_boy that you're going to test rather than create thousands of fixtures ( JSON files that hold dummy data ). Assuming that, in future, you'll have critical changes in your models where all these JSON objects must be refactored to fit the current state.

The logic works same for Django as well. But before applying it to your project, let me share the best practices and use cases with you.

Hold your factories.py inside the tests directory for each app.

Override Factory Boy Attributes

You can set some attributes as None if they don't require initial values:

import factory
import factory.django

from blog.models import Post


class PostFactory(factory.django.DjangoModelFactory):
    title = factory.Faker('name')
    image_url = factory.Faker('image_url')
    tags = None

    class Meta:
        model = Post

then override them later:

PostFactory(
    tags=['spacex', 'tesla']
)

Note that, create_batch also receives kwargs where it allows overriding attributes:

PostFactory.create_batch(
    5, #number of instances
    tags=['spacex', 'tesla'] #override attr
)

Helper Methods and Hooks

post_generation

Assume that Comment model has a field post as a foreign key where it built from Post object. At this point, we can use post_generation hook to assign created post instance to CommentFactory. Don't confuse the name conventions here:

post_generation is a built-in decorator that allows doing stuff with generated factory instance.

post object is an instance of PostFactory that we are using as an example.

import factory
import factory.fuzzy

from blog.models import Post, Comment


class CommentFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = Comment

    post = None
    message = factory.Faker("sentence")


class PostFactory(factory.Factory):

    class Meta: 
        model = Post

    title = factory.Faker("name")
    description = factory.Faker("sentence")
    published = factory.fuzzy.FuzzyChoice(choices=[True, True, True, False])
    image = factory.Faker("image_url")


    @factory.post_generation
    def post_related_models(obj, create, extracted, **kwargs):
        if not create:
            return
        CommentFactory.create(
            post=obj
        )

You can change the name of the function whatever you want.

  • obj is the Post object previously generated
  • create is a boolean indicating which strategy was used
  • extracted is None unless a value was passed in for the
  • PostGeneration declaration at Factory declaration time
  • kwargs are any extra parameters passed as attr__key=value when calling the Factory

lazy_attribute

Now, let's say you have a field name slug where you can't use upper case or any hyphens. There is a decorator named @factory.lazy_attribute which kind of behaves like lambda :

from django.utils.text import slugify

class PostFactory(factory.django.DjangoModelFactory):

    class Meta: 
        model = Post

    title = factory.Faker("name")
    description = factory.Faker("sentence")
    published = factory.fuzzy.FuzzyChoice(choices=[True, True, True, False])
    image = factory.Faker("image_url")

    @factory.lazy_attribute
    def slug(self):
        return slugify(self.title)

The name of the method will be used as the name of the attribute to fill with the return value of the method. So, you can access slug value simply like instance.slug

_adjust_kwargs

There are also other helper methods are available such as adjusting kwargs of instance. Assume that you need to concatenate post title with _example string:

class PostFactory(factory.Factory):

    class Meta: 
        model = Post

    title = factory.Faker("name")
    description = factory.Faker("sentence")
    published = factory.fuzzy.FuzzyChoice(choices=[True, True, True, False])
    image = factory.Faker("image_url")


    @classmethod
    def _adjust_kwargs(cls, **kwargs):
        kwargs['title'] = f"{kwargs['title']}_example"
        return kwargs

Actually, the task above might be achieved by using factory.lazy_attribute but in some cases, you'll need access to the instance attributes after its generated but not while generation.

For Video Explanation:

Support ๐ŸŒ

If you feel like you unlocked new skills, please share them with your friends and subscribe to the youtube channel to not miss any valuable information.

ย