How to overriding model save function when using factory boy?
Asked Answered
E

1

9

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.

Enolaenormity answered 16/8, 2017 at 12:47 Comment(0)
T
7

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.
Teresiateresina answered 16/8, 2017 at 15:23 Comment(1)
removing the *args, **kwargs from the second save worked great! Obviously hadn't clicked that I wasn't editing self (but rather self.user) in the save overload. Thanks a lot. And thanks for the detailed explanation of what's going on with the save overload. For the record, as it was it was working both for creating objects through the admin and via objects.create() in the python shell.Enolaenormity

© 2022 - 2024 — McMap. All rights reserved.