如何设计数据库测试、校验与数据正确性护栏
不要罗列测试类型;先证明这次数据库改动没有把业务语义写坏
这一页只解决一个问题: 数据库设计做完后,怎样证明它真的可靠。不是“接口返回 200”,而是账目不重、余额能平、月报口径不漂、迁移不漏、并发不乱、读模型能对账。
分类从文本字段迁移到 category_id。测试只覆盖“新增账目能选分类”。上线后历史账目有空格、旧分类、停用分类,月报少算一批。脚本成功、接口成功,但数据库语义失败。
| 你以为测到了 | 其实没测到 | 需要的护栏 |
| 接口能新增 | 历史数据能否被正确映射 | 迁移前后行数和异常样本 |
| SQL 能跑 | 月报口径是否仍一致 | 事实源和报表对账 |
| 单线程通过 | 重复提交和并发导入是否会写重 | 幂等键、唯一约束、并发测试 |
| 结果正确 | 查询计划是否退化 | EXPLAIN 基线和扫描行数门禁 |
数据库测试保护的是业务不变量。先写“什么绝不能被破坏”,再决定用单测、集成测试、SQL 校验还是生产巡检。
| 不变量 | 可执行校验 |
| 同一外部流水不能导入两次 | ledger_id + source + external_txn_id 唯一,并发请求只落一条 |
| 转账必须一出一入 | 按 transfer_id 分组,检查 count=2、方向相反、金额相等 |
| 作废账目不进入月报 | 月报结果和事实表按同一 status 规则对账 |
| 账目不能引用别的账本分类 | entry、account、category 的 ledger_id 必须一致 |
| 余额必须能从流水重算 | 余额快照与流水聚合差异必须为 0 或可解释 |
-- transfer invariant
SELECT transfer_id
FROM ledger_entries
WHERE transfer_id IS NOT NULL
GROUP BY transfer_id
HAVING COUNT(*) <> 2
OR SUM(CASE WHEN direction='out' THEN amount_minor ELSE 0 END)
<> SUM(CASE WHEN direction='in' THEN amount_minor ELSE 0 END);
每次数据库改动上线前,不需要写一份宏大的测试战略。先凑齐下面这个最小证据包。
| 证据 | 最低要求 |
| 影响范围 | 改了哪些表、字段、索引、查询、任务、读模型 |
| 不变量校验 | 至少覆盖这次最可能破坏的 3 条业务规则 |
| 迁移校验 | 迁移前基线、迁移后差异、异常样本、回填进度 |
| 并发校验 | 重复提交、双窗口修改、worker 抢任务至少测一个 |
| 查询校验 | 结果样本正确,执行计划没有明显退化 |
| 上线巡检 | 发布后检查差异、慢查询、异常分布,并有 owner |
不要装全面: 当前阶段最重要的不是测试名词完整,而是这次改动最可能把哪里写坏,你有没有证据证明它没坏。
| 故意制造 | 护栏应该怎样反应 |
| 插入重复 external_txn_id | 唯一约束或导入幂等测试失败 |
| 只插入一侧转账 | 不变量 SQL 找出 transfer_id |
| 把 posted 改成 voided 后不重算月报 | 事实源和月报差异报警 |
| 迁移分类时留下无法映射行 | 迁移门禁阻断切读 |
| 删掉关键索引 | 执行计划或扫描行数门禁失败 |
| 停掉读模型消费者 | 派生数据延迟和差异巡检发现问题 |
吸收点: 护栏必须真的能拦一次。永远不变红的测试,可能只是没有测到事故。
| 巡检 | 看什么 | 触发动作 |
| 不变量巡检 | 转账不成对、跨账本引用、重复外部流水 | 进入修复队列 |
| 差异巡检 | 事实表 vs 月报 / 搜索 / 缓存 | 按范围重建或人工复核 |
| 迁移巡检 | 新旧字段差异、回填进度、双写差异 | 不允许旧字段退场 |
| 性能巡检 | 慢 SQL、扫描行数、锁等待、连接池水位 | 阻断发布或进入容量治理 |
总结: 数据库测试不是测试类型大全,而是一组能证明设计没有写坏业务语义的证据。先写不变量,再造会失败的样本,最后把迁移、并发、读模型、性能和生产巡检接成门禁。