A singleton attempt with Django and factory_boy
First things first. Let’s define the singleton pattern1:
The singleton pattern is one of the simplest design patterns: it involves only one class which is responsible to make sure there is no more than one instance; it does it by instantiating itself and in the same time it provides a global point of access to that instance.
Why would you need singleton-like behaviour at Django model layer during tests?
This is not a common pattern for a
Model class. But I did encounter it a few times1:
Usually singletons are used for centralized management of internal or external resources and they provide a global point of access to themselves.
In another case a system had an
Account table. For multiple client accounts. But then the Django project was implemented per account in a multi-tenant architecture. From then on, the
Account table had, at any point in time, one
Account record only.
For this post’s sake we’ll use the
Config model with a
data JSONField and a
key unique character field:
class Config(models.Model): key = models.CharField(max_length=32, unique=True) data = models.JSONField()
Do we need
key field? For this model’s purposes? Stricly speaking, no. We’ll get back to it later on.
Test data management with factory_boy
factory_boy is the most widely used fixtures replacement package. At least according to the 2022 Django Developer Survey2. In which 12% of respondents listed
factory_boy as one of the “top 5 Python packages [they] rely on”.
factory_boy‘s main competitor, i.e. model bakery was listed by only 2% of the respondents in the same survey.
Singleton with factory_boy
Django queryset offers the get_or_create convenient method:
for looking up an object with the given kwargs (may be empty if your model has defaults for all fields), creating one if necessary.
In order to have the database enforce one entry per table we’ll configure factory_boy to always “get or create”. For this factory_boy provides
Fields whose name are passed in this list will be used to perform a
Model.objects.get_or_create()instead of the usual
keyto always have the same value, and
- configure the
then that factory will, by default, get or create the same
Config record with the same
Model factory class looks like this:
class ConfigFactory(factory.django.DjangoModelFactory): class Meta: model = "myapp.Config" django_get_or_create = ["key"] key = "test-key"
What the above does is:
ConfigFactoryis called it either gets or creates a
Configdatabase entry with key
ConfigFactorymultiple times will return the same
- Even when
create_batchis called, it returns an array of
Configentries, but in reality it’s the same database record:
In: config_list = ConfigFactory.create_batch(3) In: [config.id for config in config_list] Out: [1, 1, 1]
Importantly, what the above does not is guarantee one entry in that table. If a factory with a different
key is instantiated, then that record will be created at database level:
In: another_config = ConfigFactory(key="another-key") In: another_config.id Out: 2
But like this the factory always gets or creates the same
Config, unless a
Config with a
key other than the default key
test-key is created.
So unless you specify a different key, you don’t need to worry about duplicates anymore.
Hope this helps. Happy coding!