PAYATHON 2026

为何动态生成测试时 StaticLiveServerTestCase 的 fixtures 会异常?

数据阿明

这段代码涉及 fixtures 属性被 MRO 遮蔽、模块导入时查询数据库,以及两种测试基类的数据库隔离差异。现有材料可以直接解释 fixtures 未加载的原因,但不能证明 StaticLiveServerTestCase 本身有缺陷。permission_id=40 的具体来源还需要检查 fixture 内容和完整 traceback。

为什么交换基类顺序后,fixtures 不加载?

Django 的 TransactionTestCase 定义了 fixtures = None。基类顺序为 (StaticLiveServerTestCase, TestMultiUser) 时,沿 MRO 查找 fixtures 会先找到这个 None,后面的 TestUserMixin.fixtures 因此被遮蔽。Django 不会自动合并各个基类的 fixtures。

把测试 mixin 放在前面,或在最终生成的类中显式设置 fixtures,都能解决这个属性查找问题。依据见 Django 5.2 测试类源码。

这个规则也适用于 TestCase,因为它同样继承 TransactionTestCase。因此,“使用 TestCase 时正常”需要结合实际的基类顺序判断,不能据此推导出两种顺序对 TestCase 都有效。

为什么切换基类会暴露数据库问题?

StaticLiveServerTestCase 沿用 LiveServerTestCase 的 TransactionTestCase 隔离方式,以便测试代码与服务器线程交互。支持事务和保存点时,TestCase 主要依靠事务回滚隔离测试;TransactionTestCase 则在测试后清空数据,并在后续测试前重新加载 fixtures。两者的数据恢复机制不同。参见 Django 测试工具文档。

这种差异可能暴露原先依赖固定主键、迁移数据或测试执行顺序的问题,但不意味着普通 fixtures 不受支持。

另外,Django 5.2 的 TransactionTestCase 会在 setUpClass 中提前执行首次数据准备,因此 fixture 加载失败出现在 setUpClass 阶段与其实现相符。后续测试方法仍有各自的数据准备和清理过程,不能只在类初始化时准备一次用户对象。

为什么不能在模块顶层查询用户?

问题中的循环会立即执行数据库查询:

for i, user in enumerate(models.CustomUser.objects.all()):
    ...

在默认测试流程中,测试发现在测试数据库准备之前发生。这个查询可能读到当前配置的数据库中的数据,而不是稍后加载到测试数据库中的 fixtures。Django 文档明确提醒应避免导入时查询。参见 测试数据库说明。

随后将查询得到的模型实例保存为类属性,也不能保证它对应当前测试数据库中的记录。即使 isinstance 检查通过,记录仍可能不存在,或者实例保留着旧数据。

应在发现测试时只生成用户标识,每次测试时再通过 setUp 查询当前数据库,取得新的模型实例。

如何保留按用户动态生成测试类的设计?

可以从 YAML 文件读取用户名并生成测试类,这一步不访问数据库;实际用户对象由 setUp 获取。

保留题中的 TestMixin 和 TestUserMixin,用下面的代码替换 TestMultiUser 和生成循环。示例假设测试模块直接位于 core/tests/,用户 fixture 使用题中展示的 username 字段;文件位置不同时需要调整路径。

from pathlib import Path

import yaml
from django.contrib.staticfiles.testing import StaticLiveServerTestCase

from core import models


class TestMultiUser(TestUserMixin, TestMixin):
    def setUp(self):
        super().setUp()
        self.user = models.CustomUser.objects.get(
            username=self.user_username,
        )

    def test_user_mapping(self):
        self.assertIsInstance(self.user, models.CustomUser)
        self.assertTrue(
            models.CustomUser.objects.filter(pk=self.user.pk).exists()
        )


fixture_path = (
    Path(__file__).resolve().parent / "fixtures" / "customusers.yaml"
)
with fixture_path.open(encoding="utf-8") as fixture_file:
    records = yaml.safe_load(fixture_file) or []

usernames = [
    record["fields"]["username"]
    for record in records
    if record["model"] == models.CustomUser._meta.label_lower
]
if not usernames:
    raise ValueError("customusers.yaml 中没有可用于生成测试的用户")

for index, username in enumerate(usernames):
    test_name = f"TestMultiUser_{index}"
    globals()[test_name] = type(
        test_name,
        (TestMultiUser, StaticLiveServerTestCase),
        {
            "__module__": __name__,
            "fixtures": TestUserMixin.fixtures,
            "user_username": username,
        },
    )

这里显式设置 fixtures,避免配置被继承链中的同名属性遮蔽。示例只枚举该 YAML 文件直接声明的用户,不包括其他 fixture 或数据迁移创建的用户。它解决了测试生成和用户获取方式的问题,权限外键问题仍需单独修复。

如果 mixin 后续新增 setUp、setUpClass 等方法,应继续调用 super(),让 Django 的初始化链正常执行。StaticLiveServerTestCase 的测试数据应放在 fixtures 或每次测试的 setUp 中准备,不应照搬 TestCase 的 setUpTestData 用法。

permission_id=40 应如何排查?

这条错误只说明,检查外键约束时,某条关系引用了 auth_permission 中不存在的主键 40,不能证明整张权限表为空。

先查看完整 traceback 中的约束名称,确认引用来自 Group.permissions、用户的 user_permissions,还是其他关系;再检查对应 fixture 是否写死了权限数字主键。

Permission 和 ContentType 等自动生成数据的主键不适合作为跨数据库的稳定标识。若 fixture 使用这些数字建立关系,应改用 natural keys。Django 官方文档也说明了这一风险,参见 Natural keys。

例如,重新导出 Group 时可以使用:

python manage.py dumpdata auth.group --natural-foreign --format=yaml

检查输出后更新相应 fixture;如果用户 fixture 中也包含直接权限关系,需要一并处理。Permission 的 natural key 顺序为 (codename, app_label, model),见 Permission 实现。前提是对应权限确实存在;natural keys 无法补建缺失的权限。

如果缺失的是由数据迁移创建的自定义权限或其他初始数据,则需要进一步检查 TransactionTestCase 清理后如何恢复这些数据。确实依赖迁移初始数据时,可以评估 serialized_rollback=True;它用于恢复迁移数据,不能修复被遮蔽的 fixtures,也不能修复错误的权限引用。参见 Django 关于迁移数据恢复的说明。

备注:内容仅供参考。