Install
openclaw skills install increment-code-unit-test-creatUse 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.
openclaw skills install increment-code-unit-test-creat为 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 中的 <maven.compiler.source> 或 <java.version> 属性,确定目标 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缺失处理:
pom.xmlmvn test-compile 验证# 1. 确认是 Maven 项目(存在 pom.xml)
# 2. 检查测试依赖是否存在于 pom.xml
# 3. 确认是 Java 项目(src/main/java 目录存在)
输出: 依赖状态报告(完整 / 缺失需补充)
# 1. 确认默认分支(main 优先,不存在则 master)
git rev-parse --verify main 2>/dev/null || git rev-parse --verify master 2>/dev/null
# 2. 获取增量变更(与默认分支对比)
git diff <default-branch> --name-only --diff-filter=ACMR -- '*.java'
# 3. 排除测试文件
# 过滤掉 src/test/java 路径下的文件
# 4. 获取具体方法级变更
git diff <default-branch> -- src/main/java/包路径/类名.java
# 5. 解析变更类型
# - 新增类
# - 新增方法
# - 修改方法
# - 修改字段/依赖
输出: 增量变更清单(类 -> 方法 -> 变更类型)
# 对每个变更的源文件,检查对应测试类是否存在
# 源文件:src/main/java/com/example/service/UserService.java
# 测试文件:src/test/java/com/example/service/UserServiceTest.java
# 规则:
# - 测试类存在且测试方法已覆盖变更方法 -> 跳过
# - 测试类存在但未覆盖变更方法 -> 补充测试方法
# - 测试类不存在 -> 全量生成
在生成测试前,对目标类执行以下读取:
构造对象原则:
Lombok 特殊处理(必须检查):
@Setter(AccessLevel.NONE) 或 @Setter(AccessLevel.PROTECTED) 注解
electricityAmount.add(serviceAmount)),应设置参与计算的源字段@Accessors(chain = true) —— 此时 setter 返回自身而非 void,可直接链式调用@FieldDefaults —— 可能影响字段访问级别根据 Step 2 的结果选择策略:
@Mock + Mockito 模拟@InjectMocks 注入被测实例跳过规则(满足任一即跳过):
@Data, @Getter, @Setter)toString()、equals()、hashCode()(除非手动实现且含业务逻辑)必须测试(满足任一即生成):
# 1. 编译测试代码
mvn test-compile -q
# 2. 如果编译失败,检查并修复
# - 导入缺失
# - 方法签名不匹配
# - 类型不匹配
# 3. 运行单个测试类验证
mvn test -Dtest=ClassNameTest -q
# 使用 JaCoCo 验证覆盖率
mvn test jacoco:report -q
# 检查覆盖率报告
# target/site/jacoco/index.html
# 目标:方法覆盖率 >= 90%
未达标处理:
源类名:UserService.java
测试类名:UserServiceTest.java
源类名:OrderServiceImpl.java
测试类名:OrderServiceImplTest.java
// 格式:[方法名]_[场景描述]_[期望结果]
@Test
void createOrder_validInput_returnsCreatedOrder() { }
@Test
void createOrder_nullInput_throwsIllegalArgumentException() { }
@Test
void getUser_notFound_returnsEmptyOptional() { }
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Mock
private RedisTemplate<String, Object> 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));
}
}
null 或 0,但字段已设置症状: 测试中通过 setField(value) 设置了值,但 obj.getField() 返回 null 或默认值。
排查步骤:
@Setter(AccessLevel.NONE) —— 如果有,getter 很可能也是计算型的示例:
// 实际代码中:
// @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.<SomeClass>nullable(SomeClass.class)
);
症状: 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
解决:
1. 确认当前目录是否为 Maven 项目根目录
2. 如果是子模块,找到对应的 pom.xml
3. 如果确实不是 Maven 项目,提示用户本 skill 仅支持 Java/Maven
问题:当前目录不是 Git 仓库
解决:
1. 如果用户指定了具体类/包路径,跳过增量分析,直接全量生成
2. 如果用户要求"增量测试"但无 Git,提示用户初始化 git 仓库
问题: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 占位,在测试注释中说明
用户:为 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
用户:为这次的增量代码生成单元测试
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 个测试方法
用户:为 OrderService 生成单元测试
AI: 收到!开始执行单元测试生成流程:
Step 0: 项目扫描...
✅ Maven 项目确认
❌ 缺少 JUnit 5 依赖
❌ 缺少 Mockito 依赖
⚠️ 检测到项目缺少单元测试必需依赖:
需要在 pom.xml 中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
是否自动添加到 pom.xml?(y/n)
用户:y
AI: ✅ 依赖已添加到 pom.xml
✅ mvn test-compile 验证通过
继续生成 OrderService 单元测试...
为确保 JUnit 5 正确运行,检查 pom.xml 中是否配置了 Surefire 插件:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
| 版本 | 日期 | 作者 | 变更 |
|---|---|---|---|
| 1.0.0 | 2026-04-15 | endcy | 初始版本,支持 JUnit 5 + Mockito 增量/全量单元测试生成 |