我通过简单地调用工厂解决了这个问题@factory.post_generation
。严格来说,这不是针对所提出的具体问题的解决方案,但我在下面详细解释了为什么这最终会成为一个更好的架构。@rhunwick 的解决方案确实传递了SubFactory(LazyAttribute(''))
to RelatedFactory
,但是仍然存在限制,这意味着这不适合我的情况。
我们将sub_object
和object
from的创建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 和后生成的顺序不可靠 - 显然不相关的因素会影响它,可能是惰性评估的结果。我有一些错误,一组工厂会无缘无故地突然停止工作。曾经是因为我重命名了相关工厂分配给的变量。这听起来很荒谬,但我将它测试死了(并在此处发布)但毫无疑问 - 重命名变量可靠地切换了 RelatedFactory 和后生成执行的顺序。我仍然认为这是我的一些疏忽,直到由于其他原因再次发生(我从未设法诊断)。
其次,我发现声明性代码令人困惑、不灵活且难以重构。在实例化过程中传递不同的配置并不简单,因此同一个工厂可以用于测试数据的不同变化,这意味着我必须重复代码,object
需要添加到工厂Meta.exclude
列表 - 听起来微不足道,但是当您有几页代码生成数据时,这是一个可靠的错误。作为开发人员,您必须多次通过几个工厂才能了解控制流程。生成代码将在声明性主体之间传播,直到您用尽这些技巧,然后其余部分将进入生成后或变得非常复杂。对我来说,一个常见的例子是三元组相互依赖的模型(例如,父子类别结构或数据集/属性/实体)作为另一个三元组相互依赖的对象(例如,模型、参数值等)的外键,引用到其他模型的参数值)。其中一些类型的结构,尤其是嵌套的结构,很快就会变得难以管理。
我意识到这并不真正符合 factory_boy 的精神,但是将所有内容都放入后生成解决了所有这些问题。我可以传入参数,因此同一个工厂可以满足我所有的复合模型测试数据需求,并且不会重复任何代码。创建的顺序很容易立即看到,明显且完全可靠,而不是依赖于令人困惑的继承和覆盖链并受到一些错误的影响。交互很明显,因此您无需消化整个内容即可添加一些功能,并且不同的功能区域在生成后的 if 子句中分组。无需排除工作变量,您可以在工厂代码期间参考它们。单元测试代码被简化,WhateverFactory(options__create_xyz=True, options__create_abc=True..
, 而不是WhateverCreateXYZCreateABC..()
. 这使得一个很好的职责划分对代码来说非常干净。