Search code examples
pythondjangotestingfactory-boy

How to overriding model save function when using factory boy?


I'm using Factory Boy for testing a Django project and I've run into an issue while testing a model for which I've overridden the save method.

The model:

class Profile(models.Model):

    active = models.BooleanField()
    user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE,
                             related_name='profiles')
    department = models.ForeignKey(Department, null=True, blank=True)
    category_at_start = models.ForeignKey(Category)
    role = models.ForeignKey(Role)
    series = models.ForeignKey(Series, null=True, blank=True)
    status = models.ForeignKey('Status', Status)

    def save(self, *args, **kwargs):
        super(Profile, self).save(*args, **kwargs)
        active_roles = []
        active_status = []
        for profile in Profile.objects.filter(user=self.user):
            if profile.active:
                active_roles.append(profile.role.code)
                active_status.append(profile.status.name)
        self.user.current_role = '/'.join(set(active_roles))
        if 'Training' in active_status:
            self.user.current_status = 'Training'
        elif 'Certified' in active_status:
            self.user.current_status = 'Certified'
        else:
            self.user.current_status = '/'.join(set(active_status))
        self.user.save()
        super(Profile, self).save(*args, **kwargs) ### <-- seems to be the issue.

The factory:

class ProfileFactory(f.django.DjangoModelFactory):
    class Meta:
        model = models.Profile

    active = f.Faker('boolean')
    user = f.SubFactory(UserFactory)
    department = f.SubFactory(DepartmentFactory)
    category_at_start = f.SubFactory(CategoryFactory)
    role = f.SubFactory(RoleFactory)
    series = f.SubFactory(SeriesFactory)
    status = f.SubFactory(StatusFactory)

The test:

class ProfileTest(TestCase):

    def test_profile_creation(self):
        o = factories.ProfileFactory()
        self.assertTrue(isinstance(o, models.Profile))

When I run the tests, I get the following error:

django.db.utils.IntegrityError: UNIQUE constraint failed: simtrack_profile.id

If I comment out the last last/second 'super' statement in the Profile save method the tests pass. I wonder if this statement is trying to create the profile again with the same ID? I've tried various things such as specifying in the Meta class django_get_or_create and various hacked versions of overriding the _generation method for the Factory with disconnecting and connecting the post generation save, but I can't get it to work.

In the meantime, I've set the strategy to build but obviously that won't test my save method.

Any help greatly appreciated.

J.


Solution

  • factory_boy uses the MyModel.objects.create() function from Django's ORM.

    That function calls obj.save(force_insert=True): https://github.com/django/django/blob/master/django/db/models/query.py#L384

    With your overloaded save() function, this means that you get:

    1. Call super(Profile, self).save(force_insert=True)
      • [SQL: INSERT INTO simtrack_profile SET ...; ]
      • => self.pk is set to the pk of the newly inserted line
    2. Execute your custom code
    3. Call super(Profile, self).save(force_insert=True)
      • This generates this SQL: INSERT INTO simtrack_profile SET id=N, ..., with N being the pk of the object
      • Obviously, a crash occurs: there is already a line with id=N.

    You should fix your save() function, so that the second time you call super(Profile, self).save() without repeating *args, **kwargs again.

    Notes:

    • Your code will break when you add an object through Django's admin, or anytime you'd use Profile.objects.create().
    • Since you don't modify self in your overloaded save() function, you should be able to remove the second call to super(Profile, self).save() altogether; although keeping it around might be useful to avoid weird bugs if you need to add more custom behavior later.