主题
界面
主题
功能写完了,怎么证明它真的对?
★ 本项目的测试分四层,共 30 个,全部通过 ✓✓:
| 层 | 测试类 | 数量 | 时间 |
|---|---|---|---|
| ① 领域对象 | BorrowRecordTest | 6 | 0.16 s ✓✓ |
| ② 接口层 | BookControllerTest | 9 | 5.0 s |
| ③ 集成(真实库) | BorrowFlowIntegrationTest | 12 | 0.7 s ✓✓ |
| ④ 并发 | BorrowConcurrencyTest | 3 | 21.6 s ⚠️ |
★★ 最有价值的结论:"并发下会不会超借"只有第 ④ 层能回答 ✓✓
@Transactional 是陷阱 ★★★;★ 这个项目里,真正值钱的断言只有一条 ✓✓:
10 个线程抢 1 本库存 → 恰好 1 个成功,库存恰好 0★ 换成"先查再改"的写法,这个测试必然失败 ✗✗ —— 这就是测试的价值:它把"并发安全"从"我觉得没问题"变成了"可验证" ✓✓
| 层 | 测什么 | 测不出什么(盲区) |
|---|---|---|
| ① 领域对象 ★★★ | 计算规则、边界值 | 数据库相关的任何问题 ✗ |
| ② 接口层 ★★ | 状态码、JSON 结构、参数默认值 | 业务规则(它只关心协议)✗ |
| ③ 集成 ★★★ | SQL 对不对、事务回不回滚、约束生效没 | 并发安全 ✗✗ |
| ④ 并发 ★★★ | 并发下会不会超借 / 重复归还 | 单线程的正确性(由前三层覆盖)✓ |
★★★ 关键认识:Mock 的盲区 ✓✓:
Mock 掉的 Repository 是"我说它返回什么就返回什么" ✗
所以用 Mock 测不出: · SQL 写错了 · 事务没生效 · 并发下会超借
这些必须靠真实数据库 ✓✓
① 不能加 @Transactional ✗✗:
事务里的改动,其他线程看不到 ✗
→ 10 个线程会"各借各的" → 测试假通过 ✗✗
→ 这比不测还危险:你以为验证了,其实什么都没验证② 要自己准备与清理数据(不能靠事务回滚)✓
③ 要断言"恰好 N 个成功",而不是"至少 N 个" ✓✓
assertEquals(1, success.get(), "★ 只能有 1 个线程借到(多一个就是超借)");
assertEquals(9, noCopy.get(), "★ 其余 9 个都应该是「库存不足」这一种明确结果");
assertEquals(0, otherError.get(), "不该出现意料之外的错误");
assertEquals(0, stock, "库存应该恰好扣到 0,不能为负");★★ 第三条最重要 ✓✓:"至少 1 个成功"在超借的情况下也会通过 ✗✗ —— 那等于没测 ✓
| # | 坑 | 现象 | 修复 |
|---|---|---|---|
| ① | Mockito 在 JDK 25 上无法 mock ✗✗ | 9 个 Service 测试全 Error | 改用真实数据库测规则 ✓ |
| ② | KeyHolder 收到多列 ✗✗ | 12 个集成测试 9 个 Error | prepareStatement(sql, new String[]{"id"}) ✓ |
| ③ | 测试数据被自己的准备工作清掉 ✗ | 并发测试成功数 = 0(不报错,只是结果不对)⚠️ | 调整顺序:先建书、再建读者 ✓ |
| ④ | MERGE 语法不生效 ✗ | 读者没插进去,全部"读者不存在" | 改成"先删后插" ✓ |
坑 ① 的现状注(2026-09 复核):实录环境为 JDK 25(基线 21,差异说明见 《JDK 与 Java 版本》)。 该坑的最终修复是在
labs/testing-unit/pom.xml显式升级byte-buddy(Mockito 自带版本只支持到 Java 24)—— 升级后 Mockito 用例已在 JDK 25 下正常运行(88 个用例全绿)✓。 「当时先绕开(用真实库测规则)、回头再根治」正是踩坑修复的推荐节奏 ✓✓。
★★★ 第 ① 个坑最值得记住 ✓✓:
org.mockito.exceptions.base.MockitoException:
Mockito cannot mock this class: class ...BookRepository.
Java : 25教训:测试方案要先验证"在当前环境能不能跑起来" ✓ —— 而不是照抄文档里的写法 ✗
本项目改成了"真实数据库 +
@Transactional",代价只是慢一点, 换来的是断言真实数据、不会出现假通过 ✓✓
★★ 第 ③ 个坑最隐蔽 ⚠️:
prepareReaders(threads); // 插入 CT100~CT109
prepareBook(1); // ← 内部先调用 cleanup(),把刚插的读者删了!测试不报错,只是结果不对 ✗ —— 这类问题比崩溃更难发现 ✓✓
★ 归还的幂等测试 ✓✓:
@Test
void concurrentReturnAddsStockOnce() {
// 借一本
BorrowRecord record = borrowService.borrow(TEST_BOOK_ID, "CT100");
// 10 个线程同时归还
// 期望:恰好 1 次成功,库存只 +1
assertEquals(1, success.get());
assertEquals(5, stock, "★ 库存只能 +1(加多次就是重复归还的 bug)");
}★ 关键在于"标记归还"的 SQL 带条件 ✓:
UPDATE borrow_record SET status = 'RETURNED', return_at = ?
WHERE id = ? AND status = 'BORROWED' -- ★ 第二次影响 0 行★ 而且"先改记录、成功才加库存" ✓✓ —— 顺序反了就会出现"库存多了但书没还"✗
| # | 纪律 | 不做会怎样 |
|---|---|---|
| ① | 每个测试自己准备数据(不依赖其它测试的执行顺序)✓✓ | 换个执行顺序就挂 ✗✗ |
| ② | 用完清理(尤其是并发测试)✓ | 污染后续测试 ⚠️ |
| ③ | 准备动作不要反过来删掉别人的数据 ✓✓ | 结果不对但不报错(本项目踩过)⚠️ |
★ 每个业务动作至少要覆盖这四类 ✓:
| 类别 | 例子 |
|---|---|
| 正常流程 ✓ | 借书成功:库存 -1、记录状态正确、到期日正确 |
| 业务拒绝 ★★★ | 停用读者、超限、重复借、库存不足 |
| 幂等 ★★★ | 重复归还:只加一次库存 |
| 并发 ★★★ | 抢库存:恰好 N 个成功 |
★ 本项目的覆盖情况 ✓:
| 动作 | 正常 | 拒绝 | 幂等 | 并发 |
|---|---|---|---|---|
| 借书 | ✓ | ✓(4 种) | — | ✓✓ |
| 还书 | ✓ | ✓ | ✓✓ | ✓✓ |
| 续借 | ✓ | ✓(超期/超限) | — | — |
@Test
void oneDayOverdue() {
LocalDateTime dueAt = LocalDateTime.now().minusDays(1).toLocalDate().atStartOfDay();
assertEquals(1, record(dueAt, null, "BORROWED").overdueDays());
}★ 0.16 秒跑 6 个 ✓✓ —— 每次保存都能跑 ✓
见 5.2 ✓
// ✗ Mock 的 Repository 不会真的并发扣减 —— 测不出超借 ✗✗
when(bookRepository.tryBorrowOne(1L)).thenReturn(true); // 永远成功
// → 10 个线程全部"成功" → 假通过代码:
labs/project-a/src/test/java/cn/kb/learn/library/
├── domain/ BorrowRecordTest (6 个,0.16 s)
├── web/ BookControllerTest (9 个,5.0 s)
└── integration/ BorrowFlowIntegrationTest (12 个,0.7 s)
BorrowConcurrencyTest (3 个,21.6 s)运行:
cd labs
.\mvnw.cmd -pl project-a test★ 实测结果 ✓:
Tests run: 30, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS★ 并发测试的实际输出 ✓:
10 个线程抢 1 本:成功 1 / 库存不足 9 / 其他错误 0
20 个线程抢 5 本:成功 5 / 剩余库存 0
并发归还 10 次:成功 1 次 / 最终库存 5tryBorrowOne 改成"先查再改":跑并发测试,看它失败 ★★★;@Transactional:观察它变成假通过 ⚠️;finally 里的清理:观察下一个测试是否受影响 ✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 并发测试加 @Transactional ✗✗ | 假通过(最危险)✓✓✓ |
| 2 | 断言"至少 N 个成功" ✗✗ | 超借也能通过 ✓✓ |
| 3 | 只用 Mock 测,不用真实库 ✗✗ | 测不出 SQL/事务/并发问题 ✓✓ |
| 4 | 测试数据准备顺序错误 ✗ | 结果不对但不报错 ⚠️ |
| 5 | 测试之间互相依赖 ✗✗ | 换个顺序就挂 ✓ |
| 6 | 不做幂等测试 ✗✗ | 重复提交会出脏数据 ✓ |
| 7 | 测试数据不清理 ✗ | 污染后续测试 ⚠️ |
目标:亲手证明"并发安全"是真的。
步骤:
tryBorrowOne 改成"先查再改",跑并发测试看它失败 ★★★;@Transactional,观察假通过 ✓✓;验收方式:能说出"你的哪个功能最需要并发测试、为什么"。
扩展任务:给项目加契约测试(《接口管理平台》):
@Transactional 来保证数据干净。(对 / 错)assertEquals(1, success.get(), "只能有 1 个借到");如果改成 assertTrue(success.get() >= 1) 会怎样? 5. (设计)给"入库(增加副本)"设计测试:要覆盖正常、异常、并发。
>= 1 依然成立 ✓ ——等于没测 ✓✓delta <= 0 拒绝、书不存在拒绝 ✓; 并发:10 个线程各加 1 本 → 最终总量恰好 +10(不能少)✓✓; 关键:"增加副本"也要用原子 UPDATE(SET total = total + ? 本身就是原子的 ✓)@Transactional ★★★;《项目 A:部署与运维》:怎么把它跑起来并保持健康。