为Django含通用外键(GFK)的模型编写FactoryBoy测试工厂遇报错
FactoryBoy 配置通用外键(GFK)测试工厂的错误修正
模型业务逻辑
核心模型关联关系如下:
Artwork艺术品模型通过普通外键关联到Region区域模型Region区域模型配置通用外键,既可以关联SpaceRegion空间区域,也可以关联BeaconRegion信标区域- 两类具体区域模型配置了反向通用关联,可反向查询到关联的Region记录
对应的Django模型定义为:
class Artwork(models.Model): region = models.ForeignKey( "Region", on_delete=models.SET_NULL, null=True, blank=True ) class Region(models.Model): # 可关联的目标为 BeaconRegion 或 SpaceRegion region_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) region_object_id = models.PositiveIntegerField() region = GenericForeignKey("region_content_type", "region_object_id") class SpaceRegion(models.Model): label = models.CharField(max_length=255) regions = GenericRelation( Region, content_type_field="region_content_type", object_id_field="region_object_id", related_query_name="space_region", ) class BeaconRegion(models.Model): label = models.CharField(max_length=255) regions = GenericRelation( Region, content_type_field="region_content_type", object_id_field="region_object_id", related_query_name="beacon_region", )
初始错误配置
你最初编写的工厂类代码如下:
class RegionFactory(factory.django.DjangoModelFactory): region_object_id = factory.SelfAttribute("region.id") region_content_type = factory.LazyAttribute( lambda o: ContentType.objects.get_for_model(o.region) ) class Meta: exclude = ["region"] abstract = True class BeaconRegionFactory(RegionFactory): label = factory.Faker("sentence", nb_words=2) region = factory.SubFactory(RegionFactory) class Meta: model = Region class SpaceRegionFactory(RegionFactory): label = factory.Faker("sentence", nb_words=2) region = factory.SubFactory(RegionFactory) class Meta: model = Region class ArtworkFactory(factory.django.DjangoModelFactory): region = factory.SubFactory(SpaceRegionFactory)
执行ArtworkFactory()创建测试实例时,抛出如下报错:
AttributeError: The parameter 'region' is unknown. Evaluated attributes are {}, definitions are <DeclarationSet: {'region_object_id': <SelfAttribute('region.id', default=<class 'factory.declarations._UNSPECIFIED'>)>, 'region_content_type': <factory.declarations.LazyAttribute object at 0x1068cf430>, 'label': <factory.faker.Faker object at 0x1068cf880>}>
报错根源
这套配置存在两个核心逻辑颠倒的问题,直接触发循环依赖:
- 工厂的模型指向完全错误
你把BeaconRegionFactory和SpaceRegionFactory的Meta.model指向了通用关联模型Region,但按照设计,这两个工厂本应生成对应的具体区域对象(BeaconRegion/SpaceRegion本身),而非通用关联表的记录。 - 通用关联的嵌套逻辑形成循环
你在本应生成具体区域的工厂里,又写了region = factory.SubFactory(RegionFactory),试图让具体区域对象反过来关联一条Region通用关联记录。这和模型本身的关联方向完全相反:- 正确的创建顺序应当是:先生成一条具体的
SpaceRegion/BeaconRegion记录,再基于这条具体记录生成对应的Region通用关联记录,把具体对象挂载上去。 - 当前的写法会让工厂在初始化时,既要求
RegionFactory传入具体对象才能提取ID和ContentType,又让具体区域的工厂去调用RegionFactory生成关联记录,形成死循环,最终出现参数找不到的报错。
- 正确的创建顺序应当是:先生成一条具体的
正确配置方案
按照模型真实的关联顺序调整工厂逻辑即可解决问题:
# 1. 基础抽象工厂:处理 Region 模型的通用外键字段映射 class RegionFactory(factory.django.DjangoModelFactory): # 从传入的具体区域实例中提取对象ID和对应ContentType region_object_id = factory.SelfAttribute("region.id") region_content_type = factory.LazyAttribute( lambda o: ContentType.objects.get_for_model(o.region) ) class Meta: exclude = ["region"] abstract = True # 2. 具体区域工厂:先生成实际的 SpaceRegion / BeaconRegion 实例 class SpaceRegionFactory(factory.django.DjangoModelFactory): label = factory.Faker("sentence", nb_words=2) class Meta: model = SpaceRegion class BeaconRegionFactory(factory.django.DjangoModelFactory): label = factory.Faker("sentence", nb_words=2) class Meta: model = BeaconRegion # 3. 关联工厂:把生成的具体区域实例,挂载到对应的 Region 通用关联记录上 class SpaceRegionLinkedFactory(RegionFactory): region = factory.SubFactory(SpaceRegionFactory) class Meta: model = Region class BeaconRegionLinkedFactory(RegionFactory): region = factory.SubFactory(BeaconRegionFactory) class Meta: model = Region # 4. 业务模型工厂:直接关联已经挂载好具体区域的 Region 记录 class ArtworkFactory(factory.django.DjangoModelFactory): region = factory.SubFactory(SpaceRegionLinkedFactory)
测试时如果需要切换Artwork关联的区域类型,直接在实例化时传入对应工厂的实例即可,例如关联信标区域:
ArtworkFactory(region=BeaconRegionLinkedFactory())
内容的提问来源于stack exchange,提问作者Andrew
相关产品推荐
相关产品推荐

