Передача объекта, созданного с помощью 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..()
, Это делает хорошее разделение обязанностей достаточно чистым для кода.