单独跑能过,一起跑就红
两个测试分别插入同一个演示邮箱,单独运行都通过。放进同一套测试后,一个出现唯一键冲突;另一个测试提前结束并清理数据,又把还没完成的测试的用户删掉了。
共享数据库中的测试需要知道哪些数据属于自己。准备、断言和清理使用不同的范围,即使每个步骤单独正确,组合起来仍会互相影响。只给数据加随机后缀,还没有解决整表断言和跨测试清理。
本文使用归档实验中的固定交错顺序解释这些路径,环境为 MySQL 8.4.11、Connector/J 9.7.0、容器内 JDK 25.0.4.1。实验安排两条连接依次执行,证明特定顺序下的结果,没有测量真实并行运行中的失败概率。
一、准备、断言和清理要使用同一归属
实验中的几种干扰发生在不同阶段:
| 阶段 | 操作 | 归档结果 |
|---|---|---|
| 准备 | A、B 都插入 test@example.com | A 成功,B 唯一键冲突 |
| 断言 | A、B 各创建一条 NEW 订单,A 数全部 NEW 订单 | 结果 2 条,期望 1 条 |
| 清理 | B 删除邮箱以 test 开头的用户 | 删除 2 行,A 的用户也消失 |
| 下次准备 | 前次运行退出,固定邮箱没有清理 | 再次插入唯一键冲突 |
固定交错让错误能稳定复现,无需等待调度碰巧触发。排查失败时,可以先记录两个测试的准备、请求、断言和清理时间线,再安排同样的顺序。关于保留第一次失败的信息,见重跑怎样掩盖失败。
运行标识需要贯穿数据的整个生命周期:创建时写入,断言时限定,清理时精确匹配。标识还应包含测试用例或场景的区分,因为同一个测试进程中也可能并行执行多个用例。以下只展示条件,参数由程序绑定:
SELECT COUNT(*) FROM orders
WHERE run_id = ? AND user_email = ? AND status = 'NEW';
DELETE FROM orders WHERE run_id = ?;
DELETE FROM users WHERE run_id = ?;清理需要遵守外键顺序,并只在专用测试环境执行。归档中,B 按自己的 run_id 删除了 1 行,A 的用户仍有 1 行。另一场景把邮箱换成本次运行专属的值后,插入成功,但前次残留仍然存在:避免冲突和回收残留是两项工作。
对崩溃后残留的回收,应先确认所属运行已经结束,再按归属执行。仅凭数据创建时间较早就删除,可能误伤仍在执行的长测试。不得把测试环境的宽范围清理搬到共享业务库。
二、回滚只覆盖参与该事务的动作
测试先开启事务、准备数据,结束后回滚,能让参与同一事务的数据库写入不被持久保留。但异步线程、独立事务和经 HTTP 调用的另一个进程,通常不会自动加入测试的事务。
归档实验中,连接 A 在未提交事务里插入用户,同一连接查到 1 行,连接 B 查到 0 行。若被测服务从连接 B 查询,失败来自准备数据不可见,不能据此判断业务逻辑没有读取用户。该结果对应实验使用的隔离条件,不涵盖允许脏读的隔离级别;InnoDB 的一致性读规则解释了这层差异。
事务也不能阻止所有干扰。同一实验让 B 插入 A 尚未提交的相同唯一键,B 等待锁后抛出 MySQLTransactionRollbackException。实验将锁等待参数设为 1 秒,这不是默认超时,也不构成所有调度下精确 1 秒的承诺。
回滚后再插入,实验得到的自增 ID 是 3,表里却只有 1 行。已分配的自增值不随事务回滚而重新可用,具体数字来自该实验执行序列。断言应该检查业务字段、关联和数量,别把 ID 连续或从 1 开始当成测试前提。MySQL 自增机制文档说明了回滚后的空洞。
三、按干扰的范围选择隔离
| 方式 | 可以处理的范围 | 仍要检查 |
|---|---|---|
| 数据带运行与用例标识 | 行级归属、断言与清理范围 | 唯一键、共享计数器、无归属的遗留数据 |
| 同一事务内准备并回滚 | 参与该事务的数据库写入 | 独立连接的可见性、提交、DDL、外部副作用 |
| 每个测试进程一个数据库 | 不同进程之间的表数据和模式 | 同进程用例、服务器资源、队列和缓存 |
实验为两个执行者分别建立数据库。它们都使用固定邮箱、统计整张表;第二个库清理用户后,第一个库的用户仍在。这证明了数据库之间的数据分离,没有测量建库和迁移成本。共享同一个 MySQL 实例时,CPU、连接额度和服务器配置仍然共享。
数据库测试还有几种常见的写法,会让结果依赖执行顺序或者出现假的通过:表结构在类级别建一次、每个用例插入数据却不清理,后面的用例就看到前面的数据;准备数据时吞掉 SQLException,数据没插进去,后面的断言如果恰好检查的是「没有某条记录」就会通过;用「删除全部对象」一类的清理语句时,它只适合测试独占的库,用在共享库上会删掉别人的数据。这几点是工程推论,本实验没有逐一复现;生成测试数据时怎样校验和原子写入,见让模型产出数据,而不是导入脚本。
测试会经过消息队列、缓存或对象存储时,数据库隔离还不够。主题、键前缀、对象路径和消费者任务需要对应的归属与生命周期,这些属于工程推论,本实验没有验证。先画出被测请求的实际资源路径,再逐个决定使用行级标识、独立命名空间还是独立实例;已有压测标记传播讨论了上下文跨线程和消息边界丢失的问题。
配套实验
- codesphere-labs/testing/test-data-ownership:MySQL 8.4.11(JDK 25.0.4.1),两条连接按固定顺序交替执行两个测试的准备、断言与清理:固定数据相撞、整表断言、按模式清理、上次运行的残留、事务回滚的可见性与锁等待、每进程一个库(验证记录)
参考资料