increment-code-unit-test-creat
by @endcy
Use when generating JUnit 5 + Mockito unit tests for Java/Maven source code, especially for incremental changes on a git branch. Activates when user asks to...
clawhub install increment-code-unit-test-creat📖 About This Skill
name: increment-code-unit-test-creator description: Use when generating JUnit 5 + Mockito unit tests for Java/Maven source code, especially for incremental changes on a git branch. Activates when user asks to create unit tests, generate test coverage, add tests for changed code, or write tests for Java classes. Supports full class test generation and incremental diff-based test updates against main/master branch.
Increment Code Unit Test Creator - 增量代码单元测试生成器
> 为 Java/Maven 项目生成 JUnit 5 + Mockito 单元测试,覆盖增量代码变更,目标覆盖率 90%~100%
核心特性
增量代码感知:
main(或 master)分支获取增量变更全量测试生成:
全 Mock 策略:
源码深度读取:
仅测源码:
*Test.java、*Spec.java、*TestCase.java 等文件激活条件
当用户提到以下关键词时激活:
与全项目测试生成的区别:
依赖检查
必需工具
| 工具 | 用途 | 检查方式 |
|------|------|----------|
| Git | 获取增量代码差异 | git --version |
| Maven | 解析依赖、运行测试 | mvn --version |
必需测试依赖
在生成测试前,检查项目 pom.xml 是否包含以下依赖:
第一步:检测 JDK 版本
读取 pom.xml 中的 或 属性,确定目标 JDK 版本:
| JDK 版本 | junit-jupiter | mockito-core | mockito-junit-jupiter | |----------|---------------|--------------|----------------------| | Java 8 | 5.9.x | 4.11.0 | 4.11.0 | | Java 11+ | 5.9.x | 5.5.0 | 5.5.0 |
> 重要: Mockito 5.x 编译后的 class 文件版本为 55.0 (Java 11+),在 Java 8 项目中使用会导致编译失败。必须根据 JDK 版本选择对应的 Mockito 版本。
第二步:检查依赖是否存在
| 依赖 | 版本(按 JDK) | 作用域 |
|------|---------------|--------|
| org.junit.jupiter:junit-jupiter | Java 8→5.9.3 / Java 11+→5.9.3 | test |
| org.mockito:mockito-core | Java 8→4.11.0 / Java 11+→5.5.0 | test |
| org.mockito:mockito-junit-jupiter | Java 8→4.11.0 / Java 11+→5.5.0 | test |
可选但推荐:
org.mockito:mockito-inline — 支持 static/final 方法 mock缺失处理:
1. 提示用户项目缺少 JUnit 5 / Mockito 依赖
2. 展示需要添加的 Maven 依赖片段(见 references/dependency-config.md)
3. 询问用户是否自动添加到 pom.xml
4. 用户确认后自动添加并执行 mvn test-compile 验证
完整工作流程
Step 0: 项目扫描与依赖检查
# 1. 确认是 Maven 项目(存在 pom.xml)
2. 检查测试依赖是否存在于 pom.xml
3. 确认是 Java 项目(src/main/java 目录存在)
输出: 依赖状态报告(完整 / 缺失需补充)
Step 1: 增量代码分析(Git 仓库模式)
# 1. 确认默认分支(main 优先,不存在则 master)
git rev-parse --verify main 2>/dev/null || git rev-parse --verify master 2>/dev/null2. 获取增量变更(与默认分支对比)
git diff --name-only --diff-filter=ACMR -- '*.java'3. 排除测试文件
过滤掉 src/test/java 路径下的文件
4. 获取具体方法级变更
git diff -- src/main/java/包路径/类名.java5. 解析变更类型
- 新增类
- 新增方法
- 修改方法
- 修改字段/依赖
输出: 增量变更清单(类 -> 方法 -> 变更类型)
Step 2: 测试类存在性检查
# 对每个变更的源文件,检查对应测试类是否存在
源文件:src/main/java/com/example/service/UserService.java
测试文件:src/test/java/com/example/service/UserServiceTest.java
规则:
- 测试类存在且测试方法已覆盖变更方法 -> 跳过
- 测试类存在但未覆盖变更方法 -> 补充测试方法
- 测试类不存在 -> 全量生成
Step 3: 源码深度读取
在生成测试前,对目标类执行以下读取:
1. 读取目标类源码:了解字段、依赖注入、方法签名 2. 读取依赖类/接口:了解注入依赖的类型和方法签名 3. 读取依赖方法的实现:了解返回类型、可能的异常 4. 读取相关 Entity/DTO/VO:构造真实测试对象
构造对象原则:
Lombok 特殊处理(必须检查):
@Setter(AccessLevel.NONE) 或 @Setter(AccessLevel.PROTECTED) 注解electricityAmount.add(serviceAmount)),应设置参与计算的源字段
@Accessors(chain = true) —— 此时 setter 返回自身而非 void,可直接链式调用@FieldDefaults —— 可能影响字段访问级别Step 4: 测试生成 / 补充
根据 Step 2 的结果选择策略:
#### 策略 A:全量生成(测试类不存在)
1. 为类中每个 public 方法生成至少一个测试方法
2. 覆盖以下场景:
- 正常路径(Happy Path)
- 边界条件(null、空集合、空字符串、极值)
- 异常路径(抛出异常的情况)
- 分支覆盖(if/else 各分支)
3. 所有外部依赖使用 @Mock + Mockito 模拟
4. 使用 @InjectMocks 注入被测实例
5. 目标覆盖率 90%~100%
#### 策略 B:增量补充(测试类已存在)
1. 识别增量代码中未被现有测试覆盖的方法 2. 为每个未覆盖方法生成新测试方法 3. 不修改已有通过的测试(除非变更破坏了原有逻辑) 4. 验证新增测试方法与已有测试不冲突
跳过规则(满足任一即跳过):
@Data, @Getter, @Setter)toString()、equals()、hashCode()(除非手动实现且含业务逻辑)必须测试(满足任一即生成):
Step 5: 测试编译验证
# 1. 编译测试代码
mvn test-compile -q2. 如果编译失败,检查并修复
- 导入缺失
- 方法签名不匹配
- 类型不匹配
3. 运行单个测试类验证
mvn test -Dtest=ClassNameTest -q
Step 6: 覆盖率验证(可选)
# 使用 JaCoCo 验证覆盖率
mvn test jacoco:report -q检查覆盖率报告
target/site/jacoco/index.html
目标:方法覆盖率 >= 90%
未达标处理: 1. 识别未覆盖的方法/分支 2. 补充对应测试方法 3. 重新运行覆盖率检查 4. 重复直到达标
测试生成规则
测试类命名
源类名:UserService.java
测试类名:UserServiceTest.java源类名:OrderServiceImpl.java
测试类名:OrderServiceImplTest.java
测试方法命名
// 格式:[方法名]_[场景描述]_[期望结果]
@Test
void createOrder_validInput_returnsCreatedOrder() { }@Test
void createOrder_nullInput_throwsIllegalArgumentException() { }
@Test
void getUser_notFound_returnsEmptyOptional() { }
Mock 策略
@ExtendWith(MockitoExtension.class)
class UserServiceTest { @Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private RedisTemplate redisTemplate;
@InjectMocks
private UserService userService;
// 所有外部依赖都 @Mock,不写真实调用
}
测试方法覆盖清单
对每个被测 public 方法,至少覆盖:
| 场景 | 优先级 | |------|--------| | 正常输入,正常返回 | P0 | | 输入为 null | P0 | | 输入为空集合/空字符串 | P1 | | 依赖抛出异常 | P1 | | 分支条件各路径 | P0 | | 边界值(0、-1、MAX) | P1 | | 并发/线程安全(如适用) | P2 |
测试模板
详见 references/test-template.md
核心结构
package com.example.service;import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService 单元测试")
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@BeforeEach
void setUp() {
// 通用 mock 设置(如有)
}
@Test
@DisplayName("创建用户 - 正常路径")
void createUser_validInput_returnsUser() {
// Given
UserDTO dto = new UserDTO();
dto.setName("test");
dto.setEmail("test@example.com");
when(userRepository.save(any(User.class))).thenReturn(new User("test"));
// When
User result = userService.createUser(dto);
// Then
assertNotNull(result);
assertEquals("test", result.getName());
verify(userRepository).save(any(User.class));
}
}
常见问题排查
Getter 返回 null 或 0,但字段已设置
症状: 测试中通过 setField(value) 设置了值,但 obj.getField() 返回 null 或默认值。
排查步骤:
1. 检查该 getter 是否为 Lombok 自动生成(简单返回字段值)还是 手动实现的计算型方法
2. 检查字段是否有 @Setter(AccessLevel.NONE) —— 如果有,getter 很可能也是计算型的
3. 读取 getter 的实现,找到它实际计算的源字段
4. 设置参与计算的源字段而非目标字段
示例:
// 实际代码中:
// @Setter(AccessLevel.NONE)
// private BigDecimal chargeMoneyAmount;
// public BigDecimal getChargeMoneyAmount() {
// return electricityAmount.add(serviceAmount); // 计算型 getter
// }// ❌ 错误:设置目标字段无效,因为 getter 不读它
field.set(order, BigDecimal.valueOf(6.0));
// ✅ 正确:设置参与计算的源字段
order.setElectricityAmount(BigDecimal.valueOf(6.0));
order.setServiceAmount(BigDecimal.ZERO);
any() 匹配器导致 NullPointerException
症状: when(mock.method(any(SomeClass.class))).thenReturn(...) 在 stub 注册阶段抛出 NPE。
原因: 某些接口(特别是外部模块的接口)的方法在接收 null 参数时会直接抛出异常,而 any() 在 stub 注册阶段会先以 null 参数试探性调用。
解决: 使用 doReturn() + nullable(Class) 匹配器:
doReturn(result).when(mockService).method(
ArgumentMatchers.nullable(SomeClass.class)
);
Stubbing 参数数量不匹配
症状: PotentialStubbingProblem 异常,提示实际调用的参数数量与 stub 不一致。
解决:
when() 参数数量与实际调用一致nullable() 匹配器覆盖所有参数@MockitoSettings(LENIENT) vs lenient().when()
原则: 优先使用 lenient().when() 针对个别 stub,避免全局 LENIENT。
// ✅ 推荐:仅对特定 stub 放宽
lenient().when(redisUtils.set(anyString(), any())).thenReturn(true);// ⚠️ 谨慎使用:全局 lenient 会隐藏其他 stubbing 问题
@MockitoSettings(strictness = Strictness.LENIENT)
只有当多个 stub 存在参数数量/类型差异且难以精确匹配时,才使用全局 LENIENT。
错误处理
pom.xml 找不到
问题:项目中不存在 pom.xml解决:
1. 确认当前目录是否为 Maven 项目根目录
2. 如果是子模块,找到对应的 pom.xml
3. 如果确实不是 Maven 项目,提示用户本 skill 仅支持 Java/Maven
Git 仓库不存在
问题:当前目录不是 Git 仓库解决:
1. 如果用户指定了具体类/包路径,跳过增量分析,直接全量生成
2. 如果用户要求"增量测试"但无 Git,提示用户初始化 git 仓库
main 和 master 分支都不存在
问题:git rev-parse --verify main 和 master 都失败解决:
1. 列出所有远程分支:git branch -r
2. 询问用户应对比哪个分支
3. 或使用 git diff HEAD~1 作为替代(最近一次提交)
测试编译失败
问题:mvn test-compile 失败解决:
1. 检查生成的测试代码导入是否正确
2. 检查方法签名是否与源类匹配
3. 检查 Mockito when/thenReturn 的返回类型
4. 修复后重新编译
覆盖率未达标
问题:JaCoCo 报告显示覆盖率低于 90%解决:
1. 读取 JaCoCo 报告,识别未覆盖的方法/分支
2. 为每个未覆盖点生成补充测试
3. 重新运行覆盖率检查
4. 若仍有无法覆盖的分支(如防御性代码),在报告中说明原因
依赖类/方法不存在
问题:源码引用的依赖类在类路径中找不到解决:
1. 检查是否为外部依赖(需 mvn dependency:resolve)
2. 检查是否为同项目其他模块(需先编译依赖模块)
3. 对找不到的依赖,使用 mock 占位,在测试注释中说明
使用示例
示例 1:为指定类生成全量单元测试
用户:为 UserService 生成单元测试AI: 收到!开始执行单元测试生成流程:
Step 0: 项目扫描...
✅ Maven 项目确认
✅ JUnit 5 + Mockito 依赖存在
Step 1: 目标类分析...
📄 com.example.service.UserService (120 行, 8 个 public 方法)
Step 2: 测试类存在性检查...
❌ UserServiceTest.java 不存在 -> 全量生成
Step 3: 源码深度读取...
📖 UserService.java - 8 个 public 方法
📖 UserRepository.java - 依赖接口
📖 User.java - Entity 定义
📖 UserDTO.java - 参数对象
Step 4: 全量测试生成...
✅ 生成 12 个测试方法(覆盖 8 个 public 方法)
📝 UserServiceTest.java (350 行)
Step 5: 编译验证...
✅ mvn test-compile 通过
✅ mvn test -Dtest=UserServiceTest 通过(12/12 passed)
Step 6: 覆盖率验证...
✅ 方法覆盖率:95%(19/20 方法已覆盖)
✅ 分支覆盖率:88%
✅ 完成!测试类已生成:src/test/java/com/example/service/UserServiceTest.java
示例 2:增量代码测试补充
用户:为这次的增量代码生成单元测试AI: 收到!开始执行增量测试流程:
Step 0: 项目扫描...
✅ Maven 项目确认
✅ 测试依赖存在
Step 1: 增量代码分析(对比 main 分支)...
📊 发现 3 个变更文件:
- UserService.java (+2 个方法)
- OrderController.java (修改 1 个方法)
- PaymentHelper.java (新增类)
Step 2: 测试类存在性检查...
✅ UserServiceTest.java 存在 -> 检查覆盖
✅ OrderControllerTest.java 存在 -> 检查覆盖
❌ PaymentHelperTest.java 不存在 -> 全量生成
Step 3: 增量方法覆盖分析...
UserServiceTest:
❌ verifyEmail() - 未覆盖 -> 补充
❌ batchUpdate() - 未覆盖 -> 补充
OrderControllerTest:
❌ updateOrderStatus() - 已修改,现有测试不覆盖新逻辑 -> 补充
PaymentHelperTest:
-> 全量生成(5 个 public 方法)
Step 4: 测试生成...
✅ UserServiceTest.java 补充 2 个测试方法
✅ OrderControllerTest.java 补充 1 个测试方法
✅ PaymentHelperTest.java 全量生成 8 个测试方法
Step 5: 编译验证...
✅ 全部通过
Step 6: 覆盖率验证...
✅ 变更方法覆盖率:100%
✅ 完成!共补充/生成 11 个测试方法
示例 3:缺少测试依赖
用户:为 OrderService 生成单元测试AI: 收到!开始执行单元测试生成流程:
Step 0: 项目扫描...
✅ Maven 项目确认
❌ 缺少 JUnit 5 依赖
❌ 缺少 Mockito 依赖
⚠️ 检测到项目缺少单元测试必需依赖:
需要在 pom.xml 中添加以下依赖:
org.junit.jupiter
junit-jupiter
5.9.3
test
org.mockito
mockito-core
5.5.0
test
org.mockito
mockito-junit-jupiter
5.5.0
test
是否自动添加到 pom.xml?(y/n)
用户:y
AI: ✅ 依赖已添加到 pom.xml
✅ mvn test-compile 验证通过
继续生成 OrderService 单元测试...
配置项
Maven Surefire 插件配置
为确保 JUnit 5 正确运行,检查 pom.xml 中是否配置了 Surefire 插件:
org.apache.maven.plugins
maven-surefire-plugin
3.1.2
JaCoCo 覆盖率插件(可选)
org.jacoco
jacoco-maven-plugin
0.8.11
prepare-agent
report
test
report
版本
| 版本 | 日期 | 作者 | 变更 | |------|------|------|------| | 1.0.0 | 2026-04-15 | endcy | 初始版本,支持 JUnit 5 + Mockito 增量/全量单元测试生成 |