服务测试不止检查“请求返回 200”。预约返回成功但库存扣了两次,或者越权用户看到他人记录,都是状态与边界错误。先列不变量,再选能观察它的测试层级。通用测试与调试介绍证据与最小复现,这里关注跨 HTTP、数据库和并发的服务行为。

让每种测试回答明确的问题

层级适合验证不足以证明
纯函数/服务单元测试金额计算、状态转换、错误分支真实 SQL、依赖配置、网络兼容
数据库集成测试约束、事务回滚、锁竞争和映射其他数据库引擎完全相同行为
HTTP/安全集成测试序列化、校验、身份、权限和状态码真实代理、TLS、浏览器部署行为
契约测试消费者依赖的字段和错误约定所有运行时容量与业务正确性
端到端与演练发布后的关键流程与恢复每个内部边界已充分覆盖

Mock 很适合制造暂时错误,但如果把数据库所有行为都替换成 Map,就无法证明事务与唯一约束有效。H2 能快速测试 JDBC 结构和事务路径,却不能代替 PostgreSQL 的 MVCC、执行计划和实际锁行为。对这些要求,应使用同类真实数据库,例如由 Testcontainers 启动隔离实例。

并发测试需要可控制的顺序

用 sleep 等待“另一个线程应该已经执行”会让测试依赖机器速度。更可靠的方式是用 CountDownLatch、Barrier 或 channel 明确表示已开始、允许继续和已完成。

started := make(chan struct{})
release := make(chan struct{})
go func() {
    close(started)   // 告诉测试:工作已进入指定位置
    <-release        // 等待测试允许继续
}()
<-started
// 在这里检查工作尚未完成时的行为。
close(release)

这是同步结构片段;等待必须有测试总超时,失败路径也要释放阻塞任务。Go 任务工程用这个方式稳定制造队列满与取消,不靠“多跑几次应该出现”。Go 的 race detector 只能检测实际执行路径上的数据竞争,测试通过不证明所有未执行路径无竞争。

给预约接口写状态断言

依次检查首次创建、同键重试、同键换参数、库存不足、缺少身份、缺少 CSRF token、他人读取。数据库中应只出现一条成功预约、一笔扣减和一条 outbox 事件;失败请求不应留下半成品。并发提交同一键时,最终状态仍保持这一规则。

Java 预约工程包含这些集成测试。测试清理数据库时注意不要因为测试方法外围的大事务,把并发线程中的真实提交关系隐藏掉。为每个测试准备明确初始状态,避免依赖执行先后顺序。

调试先收集哪条证据

“接口慢”可以来自 DNS、连接、排队、数据库、锁、序列化或网络发送。先建立时间线和请求标识,再缩小范围。日志里的错误要保留异常原因链;Java 包装异常时传入 cause,Go 用 fmt.Errorf("load reservation: %w", err),让上层仍能判断错误类型。

CPU 高时采样 profile;CPU 低但响应慢时检查等待、线程/goroutine 堆栈与池状态。不能拿一次调试器单步耗时当生产性能结论。负载测试记录并发模型、请求组合、数据规模、预热、错误率和延迟分位数,否则两次结果难以比较。

练习与面试自查

故意移除预约事务,运行“库存不足不留下记录”用例,确认测试能失败。再恢复事务,给对象查询去掉 owner 条件,确认越权测试能发现回归。这个过程验证测试是否真的保护业务规则。

  • 为什么 100% 行覆盖不代表权限和事务都正确?
  • 同一份 SQL 在 H2 上通过,为什么仍需要 PostgreSQL 测试?
  • 并发测试偶尔失败时,怎样区分产品竞争与测试本身的竞态?

参考资料