2959 字
约 9 分钟
20
S16-A09 · 测试数据的治理:造数、脱敏与隔离
2024-12-31
2026-11-09

测试库里的数据是人手工插的,没人知道哪条能删。这句话描述的不是脏数据,而是所有权缺失:数据没有主人,就没人能回答"这条为什么存在"、"删了会影响谁"、"重建要多久"。治理要解决的不是"怎么造数据",而是让数据可重建、可解释、可丢弃。本文只给判据与代价,不写测试框架操作手册。


1. 三个来源,三种维护成本曲线

来源 初始成本 维护成本 可重建性 适用
手工插入 最低 随时间线性上升且不可逆 无 一次性演示
生产脱敏 高(管道 + 合规) 平稳,结构随生产演进 有 要真实分布与规模
夹具文件 中(需设计格式) 平稳,改动集中 强(随代码版本化) 绝大多数功能测试

判断口径是**"三个月后谁还能改它"**。手工数据三个月后没人敢改,因为删了不知道谁会红;夹具文件新人也能改,因为有 diff、有 review。测试数据的第一属性不是真实,是可维护。


2. 造数的最小结构

def make_order(*, order_id="ORD-FIX-001", user_id="USR-FIX-001",
               amount_cents=1999, status="PAID"):
    return repo.insert_order(order_id=order_id, user_id=user_id,
                             amount_cents=amount_cents, status=status)

三条硬约束:主键可预测(随机主键让断言只能靠返回值,跨进程即失效;固定 id 让清理能按前缀定位)、默认值合法(默认参数要能直接构造出业务成立的对象,否则每个调用点都在补参数)、不依赖顺序(跨实体依赖用显式参数传 id,不靠自增)。

纪律 反例 代价
进仓库、走 review SQL 存在某人笔记里 没人知道数据从哪来
幂等可重跑 每次执行都 INSERT 新行 重复执行即脏数据
有清理入口 只能手工 DELETE 库越用越乱
有版本记录 直接改生产导出文件 无法复现历史失败

造数脚本的读者是未来的同事,不是数据库。写在仓库里的能被 review 拦住,写在聊天记录里的只能靠记忆。


3. 生产脱敏:四类字段

字段类 例子 处理 可逆性
标识类 手机号、身份证、邮箱、openid 保留格式替换(位数、前缀、校验位) 不可逆
联系类 姓名、地址、公司 假名化(字典或生成) 不可逆,需保持唯一
金额类 订单额、余额、费率 保序变换或加噪 可逆(需真实统计时)
自由文本 备注、工单、聊天记录 截断或模板化 建议不保留

最容易漏的是第四类:"备注"里出现手机号、地址的概率远高于预期,而它通常不在规则表里。判据:凡不受结构约束的文本字段,默认按含 PII 处理。脱敏管道的成本不在技术而在规则维护——新增字段不同步加规则,下次导出就漏。兜底方案是导出后跑一次 PII 扫描(正则 + 校验位),把漏网字段变成可发现问题。


4. 脱敏后仍可用的两个条件

-- 反例:分别脱敏,关联关系断掉
UPDATE orders SET user_id = 'U-' || md5(random()::text);

-- 正例:同一映射表驱动,外键关系不变
UPDATE users u SET phone = m.fake_phone FROM pii_map m WHERE m.real = u.phone;
UPDATE orders o JOIN users u ON o.user_id = u.user_id
  SET o.user_id = u.user_id;
条件 要求 不满足的后果
关联一致性 同一实体在所有表映射到同一假值 联表查不到,测试大面积假失败
分布形态 保留量级、稀疏度、长尾 查询计划失真,性能结论无效
约束合法 长度、格式、校验位、唯一 插入被约束拒绝

只有关联一致性是硬要求,其余越接近越好。为了分布真实而牺牲一致性,是最常见的错误取舍。


5. 脱敏失败的两个方向

方向 表现 影响
脱得不够 真实 PII 进了测试环境 泄露,往往很久后才发现
脱得过头 关联断裂、格式非法、全表同值 测试不可用,团队转回手工造数

后者更常见也更容易被忽视,因为它的表现只是"测试老是红"。可操作判据:脱敏后跑核心用例集,通过率应接近未脱敏时水平;显著下降说明规则破坏了可用性。


6. 隔离的三种方式与并发问题

方式 隔离强度 并行友好 主要问题
独立数据库 最高 好(按 worker 分库) 建库成本、连接数、迁移同步
独立 schema 高 好 权限与迁移脚本要支持多 schema
事务回滚 低 差(共享序列与锁) 不覆盖非事务副作用,自增不回收
前缀分区 中 好(数据带运行 id) 所有表都要有分区键

事务回滚被高估:它只回滚事务内可见变更,缓存、消息、外部调用、文件都不回滚。"单跑能过、并行就红"多半是把回滚当成了完全隔离。判据:产生事务外副作用的测试不能依赖回滚。


7. 并行冲突来自共享命名空间

NS = os.environ.get("TEST_NS", f"w{os.getpid()}")
def uid(base): return f"{NS}-{base}"     # 断言与清理都按 NS 定位
def cleanup(): repo.delete_where("id LIKE :p", p=f"{NS}-%")
冲突 成因 解法
主键冲突 各 worker 用同一固定 id id 带 ns 前缀
唯一约束 手机号/邮箱写死同值 按 ns 派生
清理互杀 某 worker TRUNCATE 全表 只按前缀删
序列竞争 断言依赖自增值 断言改用业务键

随机化只应出现在"值"上,不应出现在"结构"上:随机 id 让断言无法引用,随机组合让失败无法复现。可复现随机 = 种子固定、序列确定。


8. 版本化与清理

./tools/testdb rebuild --schema head --seed fixtures/base.sql --ns "$TEST_NS"
./tools/testdb clean   --ns "$TEST_NS"      # 兜底,必须限定命名空间
策略 成本 适用
每次运行前重建 高(建库耗时) 集成测试、CI
增量迁移 + 按需重建 中 本地开发
只清理不重建 低 短期可行,长期失控
快照恢复 中 有稳定基线的项目

终局判断很简单:测试库坏了,能不能三分钟重建? 能,清理策略就不重要;不能,清理脚本写得再细也只是延缓腐烂。快照最易误用——与 schema 版本脱节后恢复出"看似正常但缺列"的库。


9. 种子数据、夹具与业务初始化

类型 内容 归属 变更频率
种子数据 字典、枚举、权限点、地区表 随代码版本 低
测试夹具 用例所需最小实体图 随测试代码 高
业务初始化 真实运营配置(费率、开关) 随运营决策 不定

混在一个脚本里的结果是:改测试要动生产初始化脚本,或运营改配置把测试跑红。三者 review 路径不同,混在一起就都没有 review。


10. 分布偏差导致的假通过

偏差 典型缺失 后果
数量分布 永远只有 1–2 条 分页、批量、N+1 测不到
值域边界 缺 0、负数、最大值、NULL 校验与溢出漏测
状态组合 只造终态 状态机迁移错误漏测
时间跨度 全是当前时间 过期、跨期、时区漏测

对策不是"造更多数据",而是在夹具里显式声明边界样本:涉及数量或状态的功能,至少一组"0 条 / 1 条 / 多条 / 边界值",由测试代码生成,不靠生产数据碰巧覆盖。


11. 外部依赖固定样本与联邦数据

fixtures/thirdparty/
  payment_success.json       # 录制的真实响应,去掉敏感字段
  payment_timeout.json       # 构造的超时样本
  payment_dup_notify.json    # 重复通知(验证幂等)

原则是录制 + 版本化 + 显式失败样本。只录成功响应等于只测顺利路径,必须构造超时、重复、乱序、部分成功,否则重试与幂等永远"看起来对"。

联邦数据(跨服务共享 id)更隐蔽:服务 A 依赖 B 的固定用户 id,B 改种子后 A 静默失败。解法两条:共享版本化 id 契约(fixtures/ids.json 进同一仓库),或各服务自建数据、通过 API 建关联。前者简单但耦合,后者干净但慢;服务数超过五个必须转后者。


12. 排错:测试数据相关的典型故障

现象 根因 处理
单跑通过、并行全红 共享命名空间或全表清理互杀 id 加 ns 前缀,清理按前缀
偶发唯一约束报错 手机号/邮箱写死同值 按 ns 派生唯一值
本地通过、CI 失败 本地残留数据掩盖缺失依赖 CI 每次重建
脱敏后大量用例失败 关联断裂或格式非法 映射表驱动,保外键一致
清理后仍有脏数据 事务外副作用未清 显式清缓存/消息/文件
断言依赖自增 id 造数未固定主键 固定 id 或改业务键断言

13. 边界:什么时候不必建治理流程

场景 合理做法
纯逻辑测试(无 IO) 内存对象即可
无数据库依赖 只治理外部依赖样本
团队一两人 一个造数模块 + 重建脚本
一次性原型 不治理,随项目丢弃
强合规环境 禁止生产数据入测试,只用合成数据
已有成熟平台 用平台的隔离与快照能力

治理成本是持续的(规则维护、review、重建耗时)。出现"没人敢删的数据"或"失败无法复现"才该开始治理;两者都没出现,先写一条重建脚本。


14. 自测

  1. 手工插入的测试数据,三个月后的主要问题是什么?
  2. 为什么随机主键会让断言变脆?
  3. 脱敏必须保住的两个条件里,哪一个是硬要求?
  4. 事务回滚做隔离,什么情况下会失效?
  5. 并行测试的主键冲突,最简解法是什么?
  6. 测试全绿但上线出事,数据可能缺了什么?
S16-A09 · 测试数据的治理:造数、脱敏与隔离
https://www.clxhxhhr.top/posts/6218/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。