21
Mock Django models using Faker and Factory Boy
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.
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.
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
)
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 thePost
object previously generated -
create
is a boolean indicating which strategy was used -
extracted
isNone
unless a value was passed in for the -
PostGeneration
declaration atFactory
declaration time -
kwargs
are any extra parameters passed asattr__key=value
when calling theFactory
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
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.
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.
21