Django User Profile Factories
Recently, we’ve needed to update our user model in our django project to support additional properties. Having looked at this great post about extending the user model, it was clear that at this point we needed to simply create a Profile
object with a one to one relationship on the User
model. I duly followed the approach outlined, creating a signal that would automatically create a Profile
object when a User
was created:
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, raw, **kwargs):
if not raw:
if created:
models.Profile.objects.create(user=instance)
else:
instance.profile.save()
(possibly worthy of note is that I didn’t create separate signals for creating and updating the Profile
instance)
And then I ran my tests, and ran into all sorts of trouble with our factories. We’re using FactoryBoy which has been excellent for generating the models we need in our various tests. However, the signals were causing trouble.
The current docs for dealing with this are out of date, suggesting that you override the private _generate
method, which since 2.9 is apparently no longer supported. So I followed the new suggestion of using the mute_signals
decorator. Which led to the following:
@factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):
"""
Profile Factory
"""
class Meta:
model = models.Profile
user = factory.SubFactory('main.factories.UserFactory', profile=None)
# ...
@factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):
"""
User factory
"""
class Meta:
model = User
django_get_or_create = ('username',)
username = factory.Faker('first_name')
# ...
profile = factory.RelatedFactory(ProfileFactory, 'user')
Note that I had to set the mute_signals
decorator on both factory classes. This seemed to solve the initial problem of being able to generate a user or a profile. However, we have other models that are ‘owned’ by users, and I was surprised (probably naively) to discover that the decorator was not applied for any other dependent factory classes.
The mute_signals
decorator must be applied to every model that has a dependency on a model with a post_save
signal. I expect this would also apply to any pre_save
signal as well should that be what you are using.
I thought I had everything sorted at this point, but running the tests I still had failures. After some time fiddling around in the interactive shell, I finally realised that the problem lay in my use of django_get_or_create
for the User
model.
The reason this is in place is so that if the faker
call for the username accidentally generates a user that already exists, it won’t fall over with a unique name class.
However, if this did happen, the already existing user object was being retrieved from the DB, and then the ProfileFactory
was being called by the RelatedFactory
definition.
Confession time: I spent a lot of time fiddling around with post generation hooks and the like in an effort to detect the correct state and prevent the profile generation call. Way longer than I am entirely comfortable quantifying, but there were at least two cups of tea involved.
The solution to this is, of course, to use the exact same pattern on the ProfileFactory
as well, so it now looks like:
@factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):
"""
Profile Factory
"""
class Meta:
model = models.Profile
django_get_or_create = ('user', )
user = factory.SubFactory('main.factories.UserFactory', profile=None)
# ...
And finally, I had a set of factories that worked in the same way as they had before I introduced the Profile
model.