You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django测试加载fixtures报ForeignKeyViolation/IntegrityError问题

问题根因
  • 核心冲突来自Django测试隔离机制的认知偏差+静态fixture硬编码主键:
    1. 之前验证得出的“每个TestCase执行时数据库会被重建”是错误结论。Django默认测试流程仅在所有测试启动时创建一次测试库,后续TestCase默认通过「测试前开启事务、测试后回滚事务」实现数据隔离,不会每次全量删表重建,也不会自动重置表的自增主键序列。
    2. 静态fixture文件硬编码了自增主键与外键关联值:比如preference_questions的fixture中写死关联的分类ID为2,但第一个TestCase执行完事务回滚后,preference_question_category表的自增序列已经偏移,第二个TestCase加载fixture插入分类数据时,自增ID不会从1开始分配,预期ID=2的分类记录实际拿到了其他ID值,后续插入关联问题数据时找不到ID=2的分类记录,直接触发外键约束报错,也就是观测到的IntegrityError/ForeignKeyViolation,报错信息里的Key (preference_question_category_id)=(2) is not present in table "preference_question_category"就是该问题的典型表现。
    3. 单测试文件运行时仅触发一轮fixture加载,没有自增序列偏移问题,所以可以正常执行。
可行解决方案

按推荐优先级从高到低排列:

  • 优先替换静态硬编码fixture:使用factory_boy这类动态测试数据工厂,在setUpTestData中按照模型依赖顺序生成测试对象,不需要手动维护主键映射,完全不受自增序列状态影响,长期维护成本远低于静态fixture。
  • 若要保留现有静态fixture,给所有加载fixture的测试类添加reset_sequences = True类属性,强制每个TestCase执行前重置所有相关表的自增主键序列,保证fixture中硬编码的主键值和实际插入数据的ID匹配。示例代码:
    from django.test import TestCase
    # 若使用DRF测试类可替换为 from rest_framework.test import APITestCase
    
    class BaseApiTestCase(TestCase):
        reset_sequences = True
        # 注意有外键依赖的fixture要放在被依赖表fixture的后面
        fixtures = ["preference_question_category.json", "preference_questions.json"]
    
  • 若不想全局重置序列,可改造fixture使用自然键关联:为有外键依赖的模型实现natural_key()方法,对应模型管理器实现get_by_natural_key()方法,fixture中外键字段不再写死数字ID,而是写入能唯一标识记录的自然键值(比如分类的唯一标识code),从根源上避免主键自增顺序带来的关联错误。
  • 临时兼容方案:给测试类设置transaction = True,让TestCase每次执行完真的清空对应数据库表,而非仅回滚事务,这种方式能保证每次fixture加载时表是空的、自增序列从初始值开始,但会明显降低测试执行速度,不推荐大规模使用。

内容的提问来源于stack exchange,提问作者Thirsty

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 16:57:20