将使用SubFactory和LazyAttribute创建的对象传递给factory_boy中的RelatedFactory

Chr*_*ris 10 python django associated-object factory-boy

factory.LazyAttributeSubFactory调用中使用传入一个对象,在对象中创建factory_parent.这很好用.

但是,如果我将创建的对象传递给a 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))
Run Code Online (Sandbox Code Playgroud)

同样的呼叫在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)
Run Code Online (Sandbox Code Playgroud)

相同的LazyAttribute调用不能再看到factory_parent,并且只能访问AnotherObject值.LazyAttribute抛出错误:

AttributeError: The parameter sub_object is unknown. Evaluated attributes are...[then lists all attributes of AnotherObjectFactory]
Run Code Online (Sandbox Code Playgroud)

这有什么方法吗?

我不能只将sub_object = sub_object放入ObjectFactory调用,即:

    sub_object = factory.SubFactory(SubObjectFactory)
    object = factory.SubFactory(ObjectFactory, sub_object=sub_object)
Run Code Online (Sandbox Code Playgroud)

因为如果我这样做:

    object2 = factory.SubFactory(ObjectFactory, sub_object=sub_object)
Run Code Online (Sandbox Code Playgroud)

创建了第二个子对象,而我需要两个对象来引用相同的子对象.我试着SelfAttribute无济于事.

rhu*_*cks 9

我认为你可以利用覆盖传入的参数的能力RelatedFactory来实现你想要的.

例如,给定:

class MyFactory(OKFactory):

    object = factory.SubFactory(MyOtherFactory)
    related = factory.RelatedFactory(YetAnotherFactory)  # We want to pass object in here
Run Code Online (Sandbox Code Playgroud)

如果我们object事先了解了它的价值,我们可以使用以下方法:

object = MyOtherFactory()
thing = MyFactory(object=object, related__param=object)
Run Code Online (Sandbox Code Playgroud)

我们可以使用相同的命名约定将对象传递给RelatedFactorymain 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'}
Run Code Online (Sandbox Code Playgroud)


Chr*_*ris 8

我通过简单地调用@factory.post_generation. 严格来说,这并不是针对所提出的特定问题的解决方案,但我在下面详细解释了为什么这最终成为一个更好的架构。@rhunwick 的解决方案确实确实传递了 a SubFactory(LazyAttribute(''))to RelatedFactory,但是限制仍然存在,这意味着这不适合我的情况。

sub_object我们将and的创建objectProblemFactoryto移至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()
Run Code Online (Sandbox Code Playgroud)

所以看起来RelatedFactory调用是在PostGeneration调用之前执行的。

这个问题中的命名更容易理解,因此这里是该示例问题的相同解决方案代码:

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

然后,我扩展了这种方法,传递选项来配置工厂,如下所示:

whatever = WhateverFactory(options__an_option=True, options__another_option=True)
Run Code Online (Sandbox Code Playgroud)

然后这个工厂代码检测到选项并生成所需的测试数据(注意该方法被重命名为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'
Run Code Online (Sandbox Code Playgroud)

然后我进一步扩展了这一点。因为我想要的模型包含自连接,所以我的工厂是递归的。因此,对于诸如以下的调用:

whatever = WhateverFactory(options__an_option='xyz',
                           options__an_option_for_a_nested_whatever='abc')
Run Code Online (Sandbox Code Playgroud)

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

您不需要阅读一些注释来了解为什么我选择此选项,而不是 @rhunwicks 对我上面的问题的正确解决方案。有两个原因。

阻止我尝试它的是,RelatedFactory 和后生成的顺序不可靠 - 显然不相关的因素会影响它,大概是惰性评估的结果。我遇到过一些错误,其中一组工厂会突然无缘无故地停止工作。有一次是因为我重命名了分配给RelatedFactory的变量。这听起来很荒谬,但我对它进行了彻底的测试(并发布在这里),但毫无疑问——重命名变量可靠地切换了RelatedFactory和后生成执行的顺序。我仍然认为这是我的一些疏忽,直到由于其他原因再次发生(我从未设法诊断)。

其次,我发现声明性代码令人困惑、不灵活且难以重构。在实例化过程中传递不同的配置并不简单,以便同一个工厂可以用于测试数据的不同变体,这意味着我必须重复代码,object需要添加到工厂Meta.exclude列表中 - 听起来很简单,但是当您有几页代码时生成数据时这是一个可靠的错误。作为开发人员,您必须多次经过多个工厂才能理解控制流。生成代码将在声明性主体之间传播,直到您用尽这些技巧,然后其余部分将在生成后进行或变得非常复杂。对我来说,一个常见的例子是相互依赖的模型的三元组(例如,父子类别结构或数据集/属性/实体)作为另一个相互依赖的对象的三元组(例如,模型、参数值等,参考到其他模型的参数值)。其中一些类型的结构,特别是嵌套的结构,很快就会变得难以管理。

我意识到这并不真正符合factory_boy的精神,但将所有内容放入后期解决了所有这些问题。我可以传入参数,因此同一个工厂可以满足我所有的复合模型测试数据需求,并且不会重复代码。创建的顺序很容易立即看到、明显且完全可靠,而不是依赖于令人困惑的继承和重写链并容易出现一些错误。交互是显而易见的,因此您不需要消化整个内容来添加一些功能,并且不同的功能区域被分组在生成后的 if 子句中。无需排除工作变量,您可以在工厂代码期间引用它们。单元测试代码得到了简化,因为描述功能采用参数名称而不是工厂类名称 - 因此您可以使用WhateverFactory(options__create_xyz=True, options__create_abc=True.., 而不是 等调用来创建数据WhateverCreateXYZCreateABC..()。这使得代码的职责划分变得非常清晰。