FactoryBoy / Django - OneToOneField 重复键错误

Jas*_*per 4 django factory-boy

我正在为具有多个应用程序的大型 Django 应用程序编写测试。作为这个过程的一部分,我逐渐为 Django 项目中不同应用程序的所有模型创建工厂。

但是,我在 FactoryBoy 中遇到了一些令人困惑的行为

我们的应用程序使用Profiles链接到默认auth.models.User模型的OneToOneField

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

我为两种模型创建了以下工厂:

@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')
Run Code Online (Sandbox Code Playgroud)

然后我运行以下测试:

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)

Run Code Online (Sandbox Code Playgroud)

所有测试都TestUser通过,但TestProfile工厂初始化 ( profile = ProfileFactory())失败并引发以下错误:

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

我不清楚为什么已经存在重复的用户,(应该只有一个调用来创建一个对吗?,尤其是因为任何干扰信号都已被禁用)

我的代码基于FactoryBoy 文档中的示例,该示例还处理通过OneToOneKey

有谁知道我做错了什么?

更新

根据布鲁诺和伊维萨尼的建议,我已将user行更改ProfileFactory

user = factory.SubFactory('yuza.factories.UserFactory', profile=None)
Run Code Online (Sandbox Code Playgroud)

现在上面描述的所有测试都成功通过了!

不过,我仍然会碰到以下问题-当其他工厂调用UserFactory

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

仍然返回。

我已经包含了一个调用UserFactory下面的工厂的例子,但是它发生在每个有一个user字段的工厂中。

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)
Run Code Online (Sandbox Code Playgroud)

user字段更改InvoiceFactory

user = factory.SubFactory(UserFactory, profile=None)
Run Code Online (Sandbox Code Playgroud)

帮助它通过一些测试,但最终遇到麻烦,因为它不再有与之关联的配置文件。

奇怪的是以下(在工厂之前声明用户)确实有效:

self.user = UserFactory()
invoice_factory = InvoiceFactory(user=self.user)
Run Code Online (Sandbox Code Playgroud)

我不清楚为什么我仍然不断遇到IntegrityError这里,调用UserFactory()now 工作正常。

Bru*_* A. 6

我认为这是因为您ProfileFactory创建了一个User实例,使用UserFactory它本身尝试Profile使用ProfileFactory.

您需要打破这个循环,如您链接到的文档中所述

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

如果这对您不起作用并且您需要更高级的处理,那么我建议实施一个post_generation钩子,您可以在其中做更高级的事情。

编辑:

另一种选择是告诉工厂男孩不要重新创建一个,Profile如果已经有一个User使用django_get_or_create选项

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

    class Meta:
        model = profile_models.Profile
        django_get_or_create = ('user',)
Run Code Online (Sandbox Code Playgroud)

如果你这样做,你也许可以删除profile=None我之前建议的。

编辑2:

这也可能有帮助,UserFactory.profile使用post_generation钩子更改:

@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)
Run Code Online (Sandbox Code Playgroud)

编辑 3

我刚刚意识到username您的领域UserFactory与事实上的男孩文档中的领域不同,它unique在 Django 中。我想知道这是否不会导致一些旧实例被重用,因为用户名是相同的。

您可能想尝试将此字段更改为您工厂中的序列:

@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)
Run Code Online (Sandbox Code Playgroud)