java-unit-test
Java 单元测试助手。在编写、评审、补全、重构单元测试时使用本技能—— 无论用户是否提到具体框架(unit test / JUnit / Mockito / 测试用例 / 怎么测 / 测哪些 / 写多少)。 次级触发信号——代码或 pom 中出现:spring-boot-starter-test、junit-jupiter、 @Test / @BeforeEach / @Parameterize
SKILL.mdskills/java-unit-test/SKILL.md
GitHubJava 开发期测试
开发期测试 = 纯单元测试:@Mock+@InjectMocks,毫秒级,mvn test 秒级跑完——不起 Spring Context、不引 Testcontainers / H2,零基础设施。规范分两部分——A. 设计方法(等价类/边界值/决策表/状态迁移)保证测得全;B. 工程默认值与纪律(断言库/Mock 边界/停止标准)保证每次一样。每条规则含「✗ 错误设计 → ✓ 正确设计」。
接口级验证("接口通不通")不写测试、不属本技能范围;需要真实依赖断言 / 可重复回归的重型测试 → java-integration-test,明示或确认后激活。
三条铁律
- 设计优先于编码:先确定"测哪些用例"(等价类/边界值/决策表/状态),再落成
@Test。禁止上来就写代码、想到哪测到哪——那是"覆盖率的幻觉",不是测试设计。 - 最小充分集:用等价类合并代表"无穷输入",用边界值钉住 off-by-one 高发区;穷尽测试不可能,目标是"用最少的用例覆盖最有代表性的输入"。一个方法的有效用例通常在 3~8 个之间。
- 零基础设施:纯单测不起 Spring、不引 Testcontainers / H2 / 内嵌库。需要真实依赖才能断言的行为(SQL 实际执行 / 迁移 / 缓存语义)不属单测——→ java-integration-test(激活后)。
断言库策略
默认用 JUnit 原生 Assertions(assertEquals/assertThrows/assertTrue 等)。仅当满足以下任一条件时,升级到 AssertJ:
- 集合内容断言(断言 list 元素构成/顺序/提取字段)—— AssertJ 的
containsExactlyInAnyOrder/extracting(...)。 - 字段分组断言(同一逻辑组的多个字段作为一个整体校验,如坐标 x/y/z、时间窗 start/end)—— AssertJ 的
extracting(...).containsExactly(...)。
其余场景(简单值、抛异常、各列语义独立的逐列断言如表格型 VO)一律 JUnit 原生,不切换。升级标准可机械执行:是否集合?是否同一逻辑组?两条都不是 → 留在原生逐列断言(失败定位准、加列只改一处)。
- Spring 项目
spring-boot-starter-test已传递 AssertJ,触发条件时直接用;非 Spring 项目首次触发时按references/03§1 引入assertj-core。 - 既有代码已用 AssertJ 的不主动改写;新测试默认 JUnit 原生,触发条件时该测试用 AssertJ,同文件内可混用。
快速入门(第一次用,按这条线走)
| 你想做的 | 看这份 | 读多少 |
|---|---|---|
| 不知道测什么 / 测多少 / 测到什么程度算够 | references/01 四维度 + 覆盖率立场 + 三个停止信号(完成判定用本文「DoD 锚点」) |
前 70 行 |
| 写纯计算/校验 / 多条件组合 / 状态机测试 | references/02 设计方法速查 |
按需查 |
| 设计好的用例怎么落代码 / Mock 怎么写 / 栈版本 | references/03 |
按需查 |
第 0 步:探测被测对象(技能触发时先执行)
读被测方法/类,一次性判断(读不到则问用户,勿分多轮):
- 工具栈:是否 Spring Boot(决定
spring-boot-starter-test一行 vs 逐库坐标);JUnit 4 还是 5;Mockito 版本(决定mockStatic能否用)。决策表见references/03§1。 - 被测对象类型(决定用哪种设计方法,详见路由表):纯计算/校验 → 等价类+边界值;多条件组合 → 决策表;有状态对象 → 状态迁移;有外部依赖 → Mock,界定边界。
设计方法 → 场景路由
| 被测对象特征 | 用什么方法 | 典型用例数 | 详见 |
|---|---|---|---|
| 任何方法(先读它) | 四维度 + 黑盒优先 | — | references/01 |
| 纯计算/校验(输入→输出) | 等价类划分 + 边界值 | 3~8 | references/02 |
| 多条件组合(促销/费率/权限) | 决策表(列数=用例数) | 化简后列数 | references/02 |
| 有状态对象(状态机) | 状态迁移 | 合法迁移数 + 非法迁移数 | references/02 |
| "写到什么程度算够" | 覆盖率反向校验 + 成本收益 | — | references/01 |
| 设计好的用例怎么落代码 | JUnit5/Mockito/Parameterized 映射 | — | references/03 |
每个方法的四维度(必检清单)
任何被测方法,至少回答这四组"有没有测到"——缺一个维度就是设计漏洞:
| 维度 | 测什么 | 设计方法 |
|---|---|---|
| 正向 | 合法、正常输入的预期输出 | 等价类有效代表 |
| 反向 | 非法、异常输入的拒绝行为 | 等价类无效代表 → assertThrows |
| 边界 | 区间端点、空/null、极值 | 边界值分析(off-by-one 高发区) |
| 异常 | 抛出的业务异常/受检异常路径 | 异常等价类 |
S/A 规则表(设计层)
- S 级(设计缺陷级):导致"测试存在但测不出 bug"——新测试禁止;审查既有测试命中 → 立即指出补全。
- A 级(约定):约束新生成测试。
| 级别 | ✗ 错误设计 | ✓ 正确设计 |
|---|---|---|
| S | 只写 happy path,缺反向/边界/异常 | 四维度各至少一个代表用例 |
| S | "想到几个写几个",无等价类/边界分析 | 先划等价类再取代表,边界必取端点 |
| S | 多条件逻辑只测一两种组合 | 画决策表,列数=用例数,不漏不重 |
| S | 有状态对象只测正常流转,不测非法/不可达转移 | 状态迁移:每条合法迁移+每个非法转移的拒绝 |
| S | 被测类内部 new 的依赖被 mock 掉 |
重构为构造器注入,再 mock 注入的依赖 |
| S | @MockBean 用于纯单元测试 |
纯单测默认 @Mock+@InjectMocks;项目既定的 mock 装配模式(如 TestConfig 手写 @Bean mock 轻量容器)优先于本默认——跟随项目,不强加。@MockBean 的红线是"用于纯单测起全量 Spring Context" |
| S | mockStatic 未包 try-with-resources |
try (MockedStatic<x> m = mockStatic(X.class)) {...}(不释放会污染同线程其他测试) |
| S | 为跑单测引入 Testcontainers / H2 / 内嵌库 | 纯单测零基础设施;需要真实依赖才能断言的行为 → java-integration-test(激活后) |
| S | 直接采信 AI 一次性生成的"四维度齐全"测试 | 必须人工复核三维度的真代表性;异常维度断言被测契约指定的具体异常类,勿断言 RuntimeException/Exception 根父类 |
| A | 测试方法间共享可变状态 | 每个 @Test 自包含、可乱序执行 |
| A | 测试名 test1/testMethod |
should_<期望>_when_<条件> 或 Given-When-Then |
| A | 一个 @Test 塞多个无关断言 |
一个测试一个行为(可多断言但同一关注点) |
| A | 手写一堆重复 @Test 仅输入不同 |
@ParameterizedTest+@MethodSource |
| A | JUnit4 @Before 与 JUnit5 混用 |
按探测到的 JUnit 版本统一 |
| A | 无断言测试(调一下方法即算"覆盖") | 每个测试必须有行为断言 |
| A | 反射测私有方法 | 通过公有方法间接测;私有方法复杂到要单测→提取为独立类 |
| A | 测框架胶水(@Autowired 能否注入、getter/setter) |
胶水不测;POJO 用 Lombok 不测 |
| A | 一个测试 mock 了 5+ 依赖仍硬测 | 提示被测类违反 SRP,建议拆分而非硬测 |
Mock 边界
- 只 Mock 外部依赖(DB / RPC / 时间 / 第三方),不 Mock 被测对象自身及其内部协作对象。
- 其余 Mock 纪律(内部
new的依赖、5+ 依赖、mockStatictry-with-resources、spy/doReturn)见 S/A 表 +references/03§3。
使用流程
- 第 0 步探测:工具栈 + 被测对象类型。
- 读对应 reference:查路由表,设计用例前先读「详见」列。
- 设计用例:四维度逐项检视 → 应用对应设计方法 → 最小充分用例集。
- 落成代码:按
references/03映射表。 - 输出前自检 S 级:尤其"四维度是否齐全""多条件是否画了决策表""有状态对象是否测了非法转移""
new的依赖是否被误 mock"。
测试完成判定(DoD 锚点)
本节供外部 PR/DoD checklist 直接引用——回答"这个方法的测试做完了没有"。三条同时满足才算最小充分(不是穷尽)。判定陷阱见 references/01「三个停止信号」。
- 四维度齐全:正向 / 反向 / 边界 / 异常各至少一个代表用例。
- 分支盲区清零:被测方法的每个
if/switch分支至少被一个用例走到(JaCoCo 口径下即 Branch 列 Missed = 0,无 JaCoCo 时人工分支清单全勾)。 - 等价类清单已显式走完:列出有效/无效等价类清单,逐类打勾(
null/空串/越界这些无效类最易漏)。
三条全勾即判"完成"——开发期不以"还没写集成 / 接口测试"为由继续加测试;重型层(Testcontainers / E2E)由 java-integration-test 在明示或确认后补齐。
✗ DoD 写"覆盖率 ≥ 80%" → ✓ DoD 写"四维度齐全 + 分支盲区清零 + 等价类清单已显式走完"。前者把覆盖率当目标,催生凑数测试;后者才是测试设计完整性的判定。
版本与范围
- JUnit 5(Jupiter)为主,JDK 8+;JUnit 4 项目的写法差异见
references/03§1 栈基线决策表。 - Spring Boot 项目测试依赖一行
spring-boot-starter-test;非 Spring 项目逐库坐标见references/03§1。断言默认 JUnit 原生,升级条件见上文"断言库策略"。 - 与
java-integration-test的边界:本技能 = 纯单测(@Mock+@InjectMocks,毫秒级,零基础设施,全周期默认);测试自管真实依赖(Testcontainers、迁移、真实事务传播、E2E、WireMock、契约切片@WebMvcTest/@JsonTest)——→ java-integration-test,明示或确认后激活。注解分界:纯单测@Mock(本技能),Spring 测试内@MockitoBean(→ java-integration-test)。 - 不适用:性能测试、前端测试。