Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FactoryBoy / Django - OneToOneField duplicate key error

I am writing tests for a large Django application with multiple apps. As part of this process I am gradually creating factories for all models of the different apps within the Django project.

However, I've run into some confusing behavior with FactoryBoy

Our app uses Profiles which are linked to the default auth.models.User model with a OneToOneField

class Profile(models.Model):
    user = models.OneToOneField(User)
    birth_date = models.DateField(
        verbose_name=_("Date of Birth"), null=True, blank=True)
    ( ... )

I created the following factories for both models:

@factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = profile_models.Profile

    user = factory.SubFactory('yuza.factories.UserFactory')
    birth_date = factory.Faker('date_of_birth')
    street = factory.Faker('street_name')
    house_number = factory.Faker('building_number')
    city = factory.Faker('city')
    country = factory.Faker('country')
    avatar_file = factory.django.ImageField(color='blue')
    tenant = factory.SubFactory(TenantFactory)


@factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = auth_models.User

    username = factory.Faker('user_name')
    first_name = factory.Faker('first_name')
    last_name = factory.Faker('last_name')

    email = factory.Faker('email')
    is_staff = False
    is_superuser = False
    is_active = True
    last_login = factory.LazyFunction(timezone.now)

    profile = factory.RelatedFactory(ProfileFactory, 'user')

Which I then run the followings tests for:

class TestUser(TestCase):

    def test_init(self):
        """ Verify that the factory is able to initialize """
        user = UserFactory()
        self.assertTrue(user)
        self.assertTrue(user.profile)
        self.assertTrue(user.profile.tenant)


class TestProfile(TestCase):

    def test_init(self):
        """ Verify that the factory is able to initialize """
        profile = ProfileFactory()
        self.assertTrue(profile)

All tests in TestUser pass, but the TestProfile fails on the factory initialization ( profile = ProfileFactory()) and raises the following error:

IntegrityError: duplicate key value violates unique constraint "yuza_profile_user_id_key"
DETAIL:  Key (user_id)=(1) already exists.

Its not clear to me why a duplicate User would already exist, (there should only be one call to create one right?, especially since any interfering signals have been disabled)

My code was based on the example from the FactoryBoy documentation, which also dealt with Users / Profiles that are connected via a OneToOneKey

Does anyone know what I am doing wrong?

Update

As per the suggestions of both Bruno and ivissani I've changed the user line in the ProfileFactory to

user = factory.SubFactory('yuza.factories.UserFactory', profile=None)

Now all the tests described above pass successfully!

However I still run into the following issue - when other factories call the UserFactory the

IntegrityError: duplicate key value violates unique constraint "yuza_profile_user_id_key"
DETAIL:  Key (user_id)=(1) already exists.

still returns.

I've included an example of a factory calling the UserFactory below, buts its happening to every factory that has a user field.

class InvoiceFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = Invoice

    user = factory.SubFactory(UserFactory)
    invoice_id = None
    title = factory.Faker('catch_phrase')
    price_paid = factory.LazyFunction(lambda: Decimal(0))
    tax_rate = factory.LazyFunction(lambda: Decimal(1.21))
    invoice_datetime = factory.LazyFunction(timezone.now)

Changing the user field on the InvoiceFactory to

user = factory.SubFactory(UserFactory, profile=None)

Helps it pass some of the tests, but eventually runs into trouble since it no longer has a profile associated with it.

Weirdly the following (declaring the user before the factory) DOES work:

self.user = UserFactory()
invoice_factory = InvoiceFactory(user=self.user)

Its not clear to me why I still keep running into the IntegrityError here, calling the UserFactory() now works fine.

like image 334
Jasper Avatar asked Mar 04 '23 04:03

Jasper


1 Answers

I think it's because your ProfileFactory creates a User instance, using the UserFactory which itself tries to create a new Profile instance using the ProfileFactory.

You need to break this cycle, as described in the documentation you link to:

# We pass in profile=None to prevent UserFactory from 
# creating another profile (this disables the RelatedFactory)
user = factory.SubFactory('yuza.factories.UserFactory', profile=None)

If this doesn't work for you and you need more advanced handling, then I suggest implementing a post_generation hook where you can do more advanced things.

EDIT:

Another option is to tell Factory Boy to not recreate a Profile if there is already one for the User by using the django_get_or_create option:

@factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = profile_models.Profile
        django_get_or_create = ('user',)

If you do so, you might be able to remove the profile=None that I suggested before.

EDIT 2:

This might also help, change the UserFactory.profile using a post_generation hook:

@factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = auth_models.User

    ...

    # Change profile to a post_generation hook
    @factory.post_generation
    def profile(self, create, extracted):
         if not create:
             return
         if extracted is None:
             ProfileFactory(user=self)

EDIT 3

I've just realised that the username field in your UserFactory is different from the one in factroy boy's documentation, and it's unique in Django. I wonder if this doesn't cause some old instances to be reused because the username is the same.

You may want to try changing this field to a sequence in your factory:

@factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):

    class Meta:
        model = auth_models.User

    # Change to sequence to avoid duplicates
    username = factory.Sequence(lambda n: "user_%d" % n)
like image 140
Bruno A. Avatar answered Apr 27 '23 11:04

Bruno A.