← 中文资源

简体中文版本

确定性测试数据工厂

使用固定种子的随机源生成可复现的假用户,支持逐字段覆盖与稳定编号,便于快照测试。

阅读英文原文

功能说明

测试里随手调用随机库会带来两个问题:失败无法复现,快照测试每次都要更新。这个工厂把随机源封装成独立实例,用固定种子初始化,因此同样的种子和调用顺序必然产生同样的数据。编号从一递增,邮箱中带上编号,保证唯一且可读;创建时间固定,避免时间字段造成快照漂移。

user 方法支持关键字覆盖,测试可以在保持其他字段确定的前提下只改一个字段,例如把邮箱换成固定值来验证重复检测。users 批量生成并检查数量非负。生成的 User 是冻结数据类,字段不可变,适合作为断言基准。

关键代码

构造函数创建 random.Random(seed),不触碰全局随机状态,因此并行测试互不干扰。_word 从 a 到 z 中逐字符抽样,长度由参数控制。user 先取当前编号并自增,再用编号和随机词拼出邮箱与显示名,最后通过 dataclasses.replace 应用覆盖;replace 会重新构造冻结实例,不会修改原对象。users 只是简单循环。

使用注意

确定性依赖调用顺序,若中间插入一次额外生成,后续数据会整体偏移,快照测试需要同步更新。覆盖传入未知字段名会抛出 TypeError,可通过类型检查提前发现。邮箱域名使用 example.test,避免真实外发。不要把该工厂用于性能测试或需要真实分布的统计场景。生产代码中误用该模块会引入测试依赖,应限制在测试目录。

验证方式

用种子七生成三个用户,再用同样的种子重新生成,两个列表相等,输出 deterministic 为 True。换成种子八后列表不同,输出 True,说明随机源确实生效。最后的覆盖示例只改变邮箱,其余字段与原对象一致。把生成结果写入快照文件,重复运行应无差异。

代码示例

import random
import string
from dataclasses import dataclass, replace


@dataclass(frozen=True)
class User:
    identifier: int
    email: str
    display_name: str
    created_at: str


class FixtureFactory:
    def __init__(self, seed: int = 20261021) -> None:
        self._rng = random.Random(seed)
        self._next_id = 1
        self._created_at = "2026-10-21T00:00:00Z"

    def _word(self, length: int = 6) -> str:
        return "".join(self._rng.choice(string.ascii_lowercase) for _ in range(length))

    def user(self, **overrides: object) -> User:
        identifier = self._next_id
        self._next_id += 1
        user = User(
            identifier=identifier,
            email=f"{self._word(5)}.{identifier}@example.test",
            display_name=f"{self._word(4).title()} {self._word(6).title()}",
            created_at=self._created_at,
        )
        return replace(user, **overrides)  # type: ignore[arg-type]

    def users(self, count: int) -> list[User]:
        if count < 0:
            raise ValueError("count must not be negative")
        return [self.user() for _ in range(count)]


def main() -> None:
    first = FixtureFactory(seed=7).users(3)
    again = FixtureFactory(seed=7).users(3)
    other = FixtureFactory(seed=8).users(3)
    print(first)
    print("deterministic:", first == again)
    print("seed changes output:", first != other)
    print("override:", replace(first[0], email="[email protected]"))


if __name__ == "__main__":
    main()