Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 798
DrJMcAuliffe Avatar asked Aug 16 '17 12:08

DrJMcAuliffe


1 Answers

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.
like image 168
Xelnor Avatar answered Nov 15 '22 16:11

Xelnor