Need help with your Django project?
Check our django servicesUpdate, November 2021: The article was rewritten to provide better & up to date examples, about fakes & factories in Django. Happy reading!
The model structure of a Django project is usually complicated. It's really common that you can't even create a model instance without having other instances created (because of one-to-one and foreign key relations).
When it comes to unit testing, you can always use your models directly.
Doing so, over a period of time, can introduce a lot of verbosity and you'd have to copy-paste the same code setup many times.
This approach works just fine.
Model Factories
The Model Factories are an upgrade over this process. Introducing them to your projects would solve the above problems.
Factories aim to simplify your tests' setup by providing an easy-to-use interface for building complicated objects and relations.
They can highly improve the maintainability, the readability and the development time of your tests.
Their main goal is to allow the developers to focus on the tests themselves rather than on their setup.
In all our Django projects, we use factory_boy
to achieve this.
Fakes
OK, now we know what to use to setup objects for our unit tests easily.
The point is that we'd want these model instances to be generated with different fields.
We use Faker
to achieve this! It's a package that generates fake data that perfectly fits with the idea of the factories and the randomization of the input of your unit tests.
Enough said! Let's look at some practical examples.
Practical Examples
The versions of the packages that we're going to use in the blog post:
Django==3.2.9
factory-boy==3.2.1
Faker==9.8.2
In this article, we're going to be vehicle dealers. We have a feature in our app that is used by our sales team to purchase vehicles.
We represent this in the database with the following model:
from django.db import models
from django.utils import timezone
class VehiclePurchase(models.Model):
price = models.DecimalField(max_digits=19, decimal_places=2)
color = models.ForeignKey(
VehicleColor, null=True, blank=True, on_delete=models.SET_NULL
)
vehicle = models.ForeignKey(Vehicle, on_delete=models.CASCADE)
plan = models.ForeignKey(Plan, on_delete=models.CASCADE)
customer = models.ForeignKey(
BaseUser, null=True, blank=True, on_delete=models.SET_NULL
)
sales_member = models.ForeignKey(
BaseUser, null=True, blank=True, on_delete=models.SET_NULL
)
requested_at = models.DateTimeField(db_index=True, default=timezone.now)
cancelled_at = models.DateTimeField(null=True, blank=True)
This is a very basic representation of a vehicle purchase. I'm sure you can imagine how many other relations would this model have in a more realistic example.
There is something that we'd like to point out here - the foreign key relations of our example model have required foreign key relations on their own. For example, the Vehicle
model would have a relation to a manufacturer, to a vehicle model and so on.
We have a simple service that handles purchase cancellations:
def vehicle_purchase_cancel(*, vehicle_purchase: VehiclePurchase) -> VehiclePurchase:
if vehicle_purchase.cancelled_at is not None:
raise Exception(f'{vehicle_purchase} already cancelled.')
vehicle_purchase.cancelled_at = timezone.now()
vehicle_purchase.full_clean()
vehicle_purchase.save()
return vehicle_purchase
You can check our Django Styleguide for more information about the services terminology.
This service is a critical feature for our application and we want to test it exhaustively.
Let's say we'd like to add the following unit test:
class VehiclePurchaseCancelTests(unittest.TestCase):
@patch('path_to_the_services_file.timezone.now')
def test_vehicle_purchase_cancel_sets_cancelled_at(self, now_mock):
now_mock.return_value = timezone.now()
updated_vehicle_purchase = vehicle_purchase_cancel(
vehicle_purchase=self.vehicle_purchase
)
self.assertEqual(updated_vehicle_purchase.cancelled_at, now_mock.return_value)
Simple enough, right? Well, here is how the setUp
method of this TestCase
looks like without Factories.
class VehiclePurchaseCancelTests(unittest.TestCase):
def setUp(self):
customer = User.objects.create(name='Customer', email='customer@test.com')
sales_member = User.objects.create(name='Sales', email='sales@test.com')
color = Color.objects.create(hex_code='#00000', display_name='White')
make = Make.objects.create(name='Volkswagen', abbreviation='VW')
category = VehicleCategory.objects.create(name='SUV', doors_count=5)
model = VehicleModel.objects.create(
name='Tiguan',
make=make,
year=2020,
release_date=date.today(),
starting_price=200.0
)
vehicle = Vehicle.objects.create(
make=make,
model=model,
category=category
)
plan = Plan.objects.create(name='Example plan', price=100.0)
self.vehicle_purchase = VehiclePurchase(
price=10.0,
vehicle=vehicle,
plan=plan,
customer=customer,
sales_member=sales_member,
color=color
)
As you can see, things can pile up quickly and become harder to maintain.
Here is how the above example looks like, when you use Model Factories:
class VehiclePurchaseCancelTests(unittest.TestCase):
def setUp(self):
self.vehicle_purchase = VehiclePurchaseFactory()
@patch('path_to_the_services_file.timezone.now')
def test_vehicle_purchase_cancel_sets_cancelled_at(self, now_mock):
now_mock.return_value = timezone.now()
updated_vehicle_purchase = vehicle_purchase_cancel(
vehicle_purchase=self.vehicle_purchase
)
self.assertEqual(updated_vehicle_purchase.cancelled_at, now_mock.return_value)
It's more compact and we've moved the actual creation logic elsewhere.
Creating Factories
Here is how the Factory for our VehiclePurchase
looks like:
# in my_project/current_app/tests/factories.py
import factory
from my_project.utils.tests import faker
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
name = factory.LazyAttribute(lambda _: faker.name())
email = factory.LazyAttribute(lambda _: faker.unique.email())
class VehiclePurchaseFactory(factory.django.DjangoModelFactory):
class Meta:
model = VehiclePurchase
price = factory.LazyAttribute(lambda _: faker.pyfloat(positive=True))
color = factory.SubFactory(ColorFactory)
vehicle = factory.SubFactory(VehicleFactory)
plan = factory.SubFactory(PlanFactory)
customer = factory.SubFactory(UserFactory)
sales_member = factory.SubFactory(UserFactory)
The definition of the rest of the factories follow the same pattern - we won't add it here to keep the example short.
DjangoModelFactory
DjangoModelFactory
is a basic interface from factory_boy
that gives "ORM powers" to your factories.
It's main feature here is that it provides you with a common "create" and "build" strategies that you can use to generate objects in your tests.
SomeFactory.create()
/SomeFactory()
- saves the generated object to the database. The related sub factories are also created in the database.SomeFactory.build()
- generates a model instance without saving it to the database. The related sub factories are also not stored in the database.
Faker
As you may have noticed, we don't create a Faker
instance in the factories file. We import it from another file in the application. This is intentional!
We highly recommend "proxying" the Faker instance and using it in your app that way.
You'd most likely want to have the same configuration when you use fakes around your app. Same goes if you want to customize the providers and use them in different places.
# my_project/utils/tests/base.py
from faker import Faker
faker = Faker()
SubFactory
SubFactory
is the interface that you need when you want to represent an one-to-one or a foreign key relation in your Model Factories. You just need to pass another Factory to it and it'll lazily generate the desired relation.
NOTE: SubFactory
work with Factory classes, not instances:
- ⚠️WRONG:
SubFactory(UserFactory())
- ✔️CORRECT:
SubFactory(UserFactory)
LazyAttribute
LazyAttribute
is the main highlight of this blog post.
It's an extremely simple but yet powerful abstraction that represents the symbiosis between the factories and the fakes.
It accepts a method which is invoked when a Factory instance is being generated. The return value of the method is used for the value of the desired attribute.
If you don't use it, you're actually setting a class attribute to your Factory. This means that this attribute will be generated when you define your Factory class, not when you instantiate it.
Here is what we mean by that:
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = faker.unique.email()
Defining your Factory this way will produce the following result:
for _ in range(5):
print(UserFactory.build().email)
erobinson@example.org
erobinson@example.org
erobinson@example.org
erobinson@example.org
erobinson@example.org
For most of the cases, you would want your objects to be generated with different values every time:
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = factory.LazyAttribute(lambda _: faker.unique.email())
This is the output when you use LazyAttribute
:
for _ in range(5):
print(UserFactory.build().email)
woodtammy@example.net
justin56@example.com
rachel10@example.com
michaelthompson@example.com
mkennedy@example.com
Conclusion
The Factories and fakes make your dev life easier.
They can highly increase the quality and the maintainability of your unit tests.
We'll share with you some more advanced usages of the Factories that you can benefit from in a following blog post.
You can check our blog and our Django Styleguide for more useful tips and know-hows.