FactoryBoy — вложенные фабрики / максимальная глубина?

#python #django #factory-boy

#python #django #factory-boy

Вопрос:

Я пишу тесты для большого приложения Django, в рамках этого процесса я постепенно создаю фабрики для всех моделей различных приложений в рамках проекта Django.

Тем не менее, я столкнулся с некоторым запутанным поведением с FactoryBoy, когда кажется SubFactories , что он имеет максимальную глубину, за пределами которой экземпляры не генерируются.

Ошибка возникает, когда я пытаюсь запустить следующий тест:

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

        order = OrderFactory()
        self.assertTrue(order)
        self.assertTrue(order.user.profile.tenant)
 

Последняя строка завершится ошибкой ( AssertionError: None is not true ), запуск этого теста через отладчик показывает, что действительно order.user.profile .клиент возвращает None вместо ожидаемого Tenant экземпляра.

Здесь задействовано довольно много фабрик / моделей, но компоновка относительно проста.

User (django по умолчанию) и Profile модель связаны через OneToOneField, которое (после некоторых проблем) представлено UserFactory и ProfileFactory

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

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

    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.Sequence(lambda n: "user_%d" % n)
    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)

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

 

Приведенное TenantFactory ниже представлено как a SubFactory на приведенном ProfileFactory выше.

 class TenantFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = elearning_models.Tenant

    name = factory.Faker('company')
    slug = factory.LazyAttribute(lambda obj: text.slugify(obj.name))
    name_manager = factory.Faker('name')
    title_manager = factory.Faker('job')
    street = factory.Faker('street_name')
    house_number = factory.Faker('building_number')
    house_number_addition = factory.Faker('secondary_address')
 

Связан Order с a User , но многие из его методов вызывают поля его self.user.profile.tenant

 class OrderFactory(factory.DjangoModelFactory):
    class Meta:
        model = Order

    user = factory.SubFactory(UserFactory)
    order_date = factory.LazyFunction(timezone.now)
    price = factory.LazyFunction(lambda: Decimal(random.uniform(1, 100)))
    site_tenant = factory.SubFactory(TenantFactory)
    no_tax = fuzzy.FuzzyChoice([True, False])
 

Опять же, большинство утверждений в тесте проходят без сбоев, все отдельные фабрики могут инициализировать значения выборки из своих непосредственных отношений с внешним ключом. Однако, как только фабрики / модели будут удалены друг от друга на три шага, вызов вернет None вместо ожидаемого Tenant экземпляра.

Поскольку я не смог найти никаких ссылок на это поведение в документации FactoryBoy, это, вероятно, ошибка с моей стороны, но пока я не смог определить ее происхождение. Кто-нибудь знает, что я делаю не так?

метод post_save

 def create_user_profile(sender, instance, created, **kwargs):
    if created:
        profile = Profile.objects.create(user=instance)
        resume = profile.get_resume()
        resume.initialize()


post_save.connect(create_user_profile, sender=User)
 

Комментарии:

1. Добавьте код OrderFactory

2. Хороший улов, я исправил свой пример — спасибо!

3. Дикое предположение: попробуйте order.refresh_from_db() перед утверждениями. Кроме того, существуют ли какие-либо пользовательские save методы в этих моделях? Постгенерация (ваша User.profile ) включает в себя вызов instance.save() перед завершением. Делайте таблицы для Profile чего-либо и Tenant храните что-либо / что вы ожидаете?

4. Я не могу избавиться от ощущения, что что-то UserFactory.profile не так (да, я знаю, что ошибка, похоже , связана с ProfileFactor.tenant ). Проходит ли тест, если вы используете ранее созданный экземпляр пользователя: order = OrderFactory(user = user) ?

5. Спасибо за ваш ответ! — в том числе .refresh_from_db() до того, как утверждение не прошло — order = OrderFactory(user = user) проходит! — У Order есть пользовательский метод сохранения для установки некоторых полей даты при сохранении, но отключение этого метода ничего не изменило — Ваши комментарии о UserProfile привели меня к дальнейшему изучению Profile объекта, существует метод post_save (включенный в мой пост выше), который создал Profile при User создании. Я учел этот сигнал, используя @factory.django.mute_signals декоратор как UserFactory для, так и для ProfileFactory

Ответ №1:

Как я упоминал в комментарии, я обнаружил источник проблемы: post-save метод, связанный с UserProfile (я включил код в свой пост).

Этот post-save метод создал Profile при User создании. Я учел этот сигнал, используя @factory.django.mute_signals декоратор как UserFactory для, так и ProfileFactory для .

Я предполагал, что любые вызовы Order.user будут вызывать UserFactory то, что уже было вложено в декоратор, но это предположение не оказалось ошибочным. Тесты прошли только тогда, когда я применил декорированный к тому OrderFactory же.

Таким образом @factory.django.mute_signals , декоратор следует использовать не только на фабриках, на которые влияют эти сигналы, но и на любой фабрике, которая использует эти фабрики в качестве SubFactory !