Передача объекта, созданного с помощью SubFactory и LazyAttribute, в RelatedFactory в factory_boy

Я использую factory.LazyAttribute в пределах SubFactory вызов передать объект, созданный в factory_parent, Это отлично работает.

Но если я передам объект, созданный RelatedFactory, LazyAttribute больше не могу видеть factory_parent и терпит неудачу.

Это отлично работает:

class OKFactory(factory.DjangoModelFactory):
    class = Meta:
        model = Foo
        exclude = ['sub_object']

    sub_object = factory.SubFactory(SubObjectFactory)

    object = factory.SubFactory(ObjectFactory,
        sub_object=factory.LazyAttribute(lambda obj: obj.factory_parent.sub_object))

Идентичный вызов LazyAttribute терпит неудачу здесь:

class ProblemFactory(OKFactory):
    class = Meta:
        model = Foo
        exclude = ['sub_object', 'object']

    sub_object = factory.SubFactory(SubObjectFactory)

    object = factory.SubFactory(ObjectFactory,
        sub_object=factory.LazyAttribute(lambda obj: obj.factory_parent.sub_object))

    another_object = factory.RelatedFactory(AnotherObjectFactory, 'foo', object=object)

Идентичный LazyAttribute Вызов больше не может видеть factory_parent, а может только доступ AnotherObject ценности. LazyAttribute выдает ошибку:

AttributeError: The parameter sub_object is unknown. Evaluated attributes are...[then lists all attributes of AnotherObjectFactory]

Есть ли способ обойти это?

Я не могу просто поместить sub_object=sub_object в вызов ObjectFactory, то есть:

    sub_object = factory.SubFactory(SubObjectFactory)
    object = factory.SubFactory(ObjectFactory, sub_object=sub_object)

потому что если я тогда сделаю:

    object2 = factory.SubFactory(ObjectFactory, sub_object=sub_object)

создается второй субобъект, тогда как мне нужно, чтобы оба объекта ссылались на один и тот же субобъект. я пытался SelfAttribute но безрезультатно.

2 ответа

Решение

Я думаю, что вы можете использовать возможность переопределения параметров, передаваемых в RelatedFactory добиться того, что вы хотите.

Например, учитывая:

class MyFactory(OKFactory):

    object = factory.SubFactory(MyOtherFactory)
    related = factory.RelatedFactory(YetAnotherFactory)  # We want to pass object in here

Если бы мы знали, какова ценность object собирался быть заранее, мы могли бы заставить его работать с чем-то вроде:

object = MyOtherFactory()
thing = MyFactory(object=object, related__param=object)

Мы можем использовать это же соглашение об именах, чтобы передать объект RelatedFactory в основном Factory:

class MyFactory(OKFactory):

    class Meta:
        exclude = ['object']

    object = factory.SubFactory(MyOtherFactory)
    related__param = factory.SelfAttribute('object')
    related__otherrelated__param = factory.LazyAttribute(lambda myobject: 'admin%d_%d' % (myobject.level, myobject.level - 1))
    related = factory.RelatedFactory(YetAnotherFactory)  # Will be called with {'param': object, 'otherrelated__param: 'admin1_2'}

Я решил это, просто назвав фабрики внутри @factory.post_generation, Строго говоря, это не решение конкретной поставленной проблемы, но я подробно объясню ниже, почему это оказалось лучшей архитектурой. Решение @ Rhunwick действительно проходит SubFactory(LazyAttribute('')) в RelatedFactoryОднако ограничения остались, что означало, что это не подходит для моей ситуации.

Мы двигаем создание sub_object а также object от ProblemFactory в ObjectWithSubObjectsFactory (и удалите exclude пункт) и добавьте следующий код в конец ProblemFactory,

@factory.post_generation
def post(self, create, extracted, **kwargs):
    if not create:
         return  # No IDs, so wouldn't work anyway

    object = ObjectWithSubObjectsFactory()
    sub_object_ids_by_code = dict((sbj.name, sbj.id) for sbj in object.subobject_set.all())

    # self is the `Foo` Django object just created by the `ProblemFactory` that contains this code.
    for another_obj in self.anotherobject_set.all():
        if another_obj.name == 'age_in':
            another_obj.attribute_id = sub_object_ids_by_code['Age']
            another_obj.save()
        elif another_obj.name == 'income_in':
            another_obj.attribute_id = sub_object_ids_by_code['Income']
            another_obj.save()

Ну, это похоже RelatedFactory вызовы выполняются раньше PostGeneration звонки.

Имена в этом вопросе легче понять, поэтому здесь приведен тот же код решения для этого примера проблемы:

Создание dataset, column_1 а также column_2 переехали на новый завод DatasetAnd2ColumnsFactoryи код ниже добавляется в конец FunctionToParameterSettingsFactory,

@factory.post_generation
def post(self, create, extracted, **kwargs):
    if not create:
         return

    dataset = DatasetAnd2ColumnsFactory()
    column_ids_by_name = 
        dict((column.name, column.id) for column in dataset.column_set.all())

    # self is the `FunctionInstantiation` Django object just created by the `FunctionToParameterSettingsFactory` that contains this code.
    for parameter_setting in self.parametersetting_set.all():
        if parameter_setting.name == 'age_in':
            parameter_setting.column_id = column_ids_by_name['Age']
            parameter_setting.save()
        elif parameter_setting.name == 'income_in':
            parameter_setting.column_id = column_ids_by_name['Income']
            parameter_setting.save()

Затем я расширил этот подход, передав опции для настройки фабрики, например так:

whatever = WhateverFactory(options__an_option=True, options__another_option=True)

Затем этот заводской код обнаружил параметры и сгенерировал необходимые тестовые данные (обратите внимание, что метод переименован в options чтобы соответствовать префиксу в именах параметров):

@factory.post_generation
def options(self, create, not_used, **kwargs):

    # The standard code as above

    if kwargs.get('an_option', None):
        # code for custom option 'an_option'
    if kwargs.get('another_option', None):
        # code for custom option 'another_option'

Затем я расширил это. Поскольку мои желаемые модели содержали самостоятельные соединения, моя фабрика рекурсивна. Так что для звонка, такого как:

whatever = WhateverFactory(options__an_option='xyz',
                           options__an_option_for_a_nested_whatever='abc')

В @factory.post_generation Я имею:

class Meta:
    model = Whatever
# self is the top level object being generated

@factory.post_generation
def options(self, create, not_used, **kwargs):

    # This generates the nested object
    nested_object = WhateverFactory(
        options__an_option=kwargs.get('an_option_for_a_nested_whatever', None))

    # then join nested_object to self via the self join
    self.nested_whatever_id = nested_object.id

Некоторые заметки, которые вам не нужно читать, почему я выбрал эту опцию, а не правильное решение @ rhunwicks моего вопроса выше. Было две причины.

То, что помешало мне поэкспериментировать с ним, заключалось в том, что порядок RelatedFactory и post-generation ненадежен - на него влияют, по-видимому, несвязанные факторы, предположительно, следствие ленивой оценки. У меня были ошибки, когда ряд заводов внезапно перестал работать без видимой причины. Когда-то, потому что я переименовал переменные RelatedFactory были назначены. Это звучит смешно, но я проверил это до смерти (и разместил здесь), но в этом нет никаких сомнений - переименование переменных надежно переключило последовательность RelatedFactory и выполнение post-gen. Я все еще предполагал, что это был некоторый упущение от моего имени, пока это не повторилось по какой-то другой причине (которую мне так и не удалось диагностировать).

Во-вторых, я обнаружил, что декларативный код запутанный, негибкий и трудно перефакторинг. Непросто передать разные конфигурации во время создания экземпляра, чтобы одна и та же фабрика могла использоваться для разных вариантов тестовых данных, то есть мне пришлось повторять код, object нужно добавить на завод Meta.exclude list - звучит тривиально, но на страницах кода, генерирующих данные, это была надежная ошибка. Как разработчику, вам придется пройти несколько заводов несколько раз, чтобы понять поток управления. Код генерации будет распространяться между декларативным телом, пока вы не исчерпаете эти уловки, тогда все остальное перейдет в пост-генерацию или станет очень замысловатым. Типичным примером для меня является триада взаимозависимых моделей (например, структура категории "родители-дети" или набор данных / атрибуты / сущности) в качестве внешнего ключа другой триады взаимозависимых объектов (например, моделей, значений параметров и т. Д.) к значениям параметров других моделей). Некоторые из этих типов структур, особенно если они вложенные, быстро становятся неуправляемыми.

Я понимаю, что это на самом деле не в духе factory_boy, но включение всего в постпоколение решило все эти проблемы. Я могу передать параметры, поэтому одна и та же фабрика удовлетворяет всем моим требованиям к данным испытаний составной модели, и никакой код не повторяется. Последовательность создания легко увидеть сразу, она очевидна и полностью надежна, а не зависит от запутанных цепочек наследования и переопределения и подвержена некоторой ошибке. Взаимодействия очевидны, поэтому вам не нужно переваривать все это, чтобы добавить некоторую функциональность, а различные пункты функциональности сгруппированы в предложениях if-пост-генерации. Нет необходимости исключать рабочие переменные, и вы можете ссылаться на них во время действия заводского кода. Код модульного теста упрощен, потому что описание функциональности идет в именах параметров, а не в именах классов Factory - поэтому вы создаете данные с помощью вызова WhateverFactory(options__create_xyz=True, options__create_abc=True.., скорее, чем WhateverCreateXYZCreateABC..(), Это делает хорошее разделение обязанностей достаточно чистым для кода.

Другие вопросы по тегам