与 Factory Boy 的一对多关系

One to many relation with Factory Boy

我的 SQLAlchemy 模型中存在多对一关系。一份报告有很多样本(为简洁起见进行了简化):

class Sample(db.Model, CRUDMixin):
    sample_id = Column(Integer, primary_key=True)
    report_id = Column(Integer, ForeignKey('report.report_id', ondelete='CASCADE'), index=True, nullable=False)
    report = relationship('Report', back_populates='samples')

class Report(db.Model, CRUDMixin):
    report_id = Column(Integer, primary_key=True)
    samples = relationship('Sample', back_populates='report')

现在在我的测试中,我希望能够生成一个 Sample 实例或一个 Report 实例,并填充缺失的关系。

class ReportFactory(BaseFactory):
    class Meta:
        model = models.Report
    report_id = Faker('pyint')
    samples = RelatedFactoryList('tests.factories.SampleFactory', size=3)

class SampleFactory(BaseFactory):
    class Meta:
        model = models.Sample
    sample_id = Faker('pyint')
    report = SubFactory(ReportFactory)

当我创建它们的实例时,工厂陷入无限循环:

RecursionError: maximum recursion depth exceeded in comparison

但是,如果我尝试使用 SelfAttributes 来停止无限循环,我最终会得到一个没有任何样本的报告:

class ReportFactory(BaseFactory):
    samples = RelatedFactoryList('tests.factories.SampleFactory', size=3, report_id=SelfAttribute('..report_id'))

class SampleFactory(BaseFactory):
    report = SubFactory(ReportFactory, samples=[])
report = factories.ReportFactory()
l = len(report.samples) # 0

但是,如果我用 SampleFactory() 生成 Sample,它正确地有一个 Report 对象。

我应该如何正确设计我的工厂,以便 SampleFactory() 将生成具有关联 ReportSample,并且 ReportFactory() 将生成具有关联的 Report 2 个关联 Samples,没有无限循环?

实例创建后,RelatedFactory 声明被评估:

  1. 实例化了Report
  2. SampleFactory 执行了 3 次调用
  3. 返回第1步实例化的Report

为了填充 Report 个实例上的字段,您必须 link Sample 个实例到 Report 在第 2 步。

一个可能的实现是:

class SampleFactory(BaseFactory):
    class Meta:
        model = Sample

    @classmethod
    def _after_postgeneration(cls, instance, create, results=None):
        if instance.report is not None and instance not in instance.report.samples:
            instance.report.samples.append(instance)

    id = factory.Faker('pyint')
    # Enfore `post_samples = None` to prevent creating additional samples
    report = factory.SubFactory('example.ReportFactory', samples=[], post_samples=None)
    report_id = factory.SelfAttribute('report.id')

class ReportFactory(factory.Factory):
    class Meta:
        model = Report

    id = factory.Faker('pyint')
    # Set samples = [] if needed by `Report.__init__`
    samples = []
    # Named `post_samples` to mark that they are instantiated
    # *after* the `Report` is ready (and never passed to the `samples` kwarg)
    post_samples = factory.RelatedFactoryList(SampleFactory, 'report', size=3)

使用该代码,当您调用 ReportFactory 时,您:

  1. 生成 Report 没有任何样本
  2. 生成 3 个样本,向它们传递对刚刚生成的报告的引用
  3. 创建后,这些 Sample 个实例会附加到 Report.samples

我最终的解决方案实际上比我想象的要简单得多:

class ReportFactory(BaseFactory):
    class Meta:
        model = models.Report

    samples = RelatedFactoryList('tests.factories.SampleFactory', 'report', size=3)


class SampleFactory(BaseFactory):
    class Meta:
        model = models.Sample

    report = SubFactory(ReportFactory, samples=[])

关键是使用 RelatedFactoryList 的第二个参数,它必须对应于子 上的父 link,在这种情况下 'report'。此外,我使用了 SubFactory(ReportFactory, samples=[]),这确保了如果我构建单个样本,不会在父级上创建额外的样本。

使用此设置,我可以构建一个样本,该样本将关联一个 Report,并且该报告只有 1 个子 Sample。相反,我可以构造一个 Report,它将自动填充 3 个子样本。

我认为没有必要生成实际的模型 ID,因为一旦模型实际插入到数据库中,SQLAlchemy 就会自动执行此操作。但是,如果您想在不使用数据库的情况下这样做,我认为@Xelnor 的 report_id = factory.SelfAttribute('report.id') 解决方案会起作用。

我遇到的唯一未解决的问题是覆盖报告中的示例列表(例如 ReportFactory(samples = [SampleFactory()])),但我已经打开了一个记录此错误的问题:https://github.com/FactoryBoy/factory_boy/issues/636