This post cross-published with OnePublish
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_boyis 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:
dataclassmodule 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.
-
objis thePostobject previously generated -
createis a boolean indicating which strategy was used -
extractedisNoneunless a value was passed in for the -
PostGenerationdeclaration atFactorydeclaration time -
kwargsare any extra parameters passed asattr__key=valuewhen calling theFactory
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.
Latest comments (3)
Nice one ;)
Nice, I heard about it but didn't had the chance to do it.
Thanks, you should give a try :)