I have a many-to-one relationship in my SQLAlchemy models. One report has many samples (simplified for brevity):
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')
Now in my tests, I want to be able to generate a Sample
instance, or a Report
instance, and fill in the missing relationships.
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)
When I go to create an instance of these, the factories get stuck in an infinite loop:
RecursionError: maximum recursion depth exceeded in comparison
However, if I try to use SelfAttribute
s to stop the infinite loop, I end up with a report without any samples:
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
However, if I generate a Sample
with SampleFactory()
, it correctly has a Report
object.
How should I correctly design my factories such that SampleFactory()
will generate a Sample
with associated Report
, and ReportFactory()
will generate a Report
with 2 associated Samples
, without infinite loops?
My final solution was actually a lot simpler than I thought:
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=[])
The key thing was using the second argument to RelatedFactoryList
, which has to correspond to the parent link on the child, in this case 'report'
. In addition, I used SubFactory(ReportFactory, samples=[])
, which ensures that no extra samples are created on the parent if I construct a single sample.
With this setup, I can construct a sample that will have a Report
associated with it, and that report only has 1 child Sample
. Conversely, I can construct a Report
that will automatically be populated with 3 child Samples.
I don't think there's any need to generate the actual model IDs, because SQLAlchemy will do that automatically once the models are actually inserted into the database. However, if you want to do that without using the database, I think @Xelnor's solution of report_id = factory.SelfAttribute('report.id')
will work.
The only outstanding issue I have is with overriding the list of samples on the Report (e.g. ReportFactory(samples = [SampleFactory()])
), but I've opened an issue documenting this bug: https://github.com/FactoryBoy/factory_boy/issues/636