关于 unit-testing
面向使用 Java 和 Mockito 编写测试的开发者,提供单元测试规范,涵盖测试结构、命名、桩设定、验证及常见检查。
主要功能
- 01独立且自包含的测试
- 02安排—执行—断言结构
- 03Mockito 桩设定与验证指南
- 04命名和测试数据规范
适用场景
- 01为服务方法编写单元测试
- 02检查测试类中的 Mockito 用法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ folio-org/ folio-eureka-ai-dev --skill unit-testing安装后在 Claude Code 中输入/unit-testing确认已加载。
附带文件references/examples.mdreferences/patterns.md
详细介绍
单元测试指南
一般原则
1. 测试独立性
- 每个测试都必须独立且自包含
- 测试不得依赖执行顺序
- 尽可能在测试方法中使用局部变量,而不是共享实例变量
2. 清晰的测试名称
遵循以下模式:methodName_scenario_expectedBehavior
示例:tableExists_positive_tableIsPresent、deleteTimer_negative_timerDescriptorIdIsNull
3. 测试组织
每个测试都按以下结构编写:准备 → 执行 → 断言
Mockito 最佳实践
1. 绝不使用宽松模式
❌ 绝不要添加 @MockitoSettings(strictness = Strictness.LENIENT) —— 应编写精确的测试。
2. 避免不必要的桩
只为测试中实际使用的内容创建桩(when())。仅在需要它们的测试中调用辅助方法:
private void setupContextMocks() {
when(context.getFolioModuleMetadata()).thenReturn(moduleMetadata);
when(context.getTenantId()).thenReturn(TENANT_ID);
when(moduleMetadata.getDBSchemaName(TENANT_ID)).thenReturn(SCHEMA_NAME);
}
3. 移除冗余的 verify 语句
只验证未通过 when() 模拟的交互:
verify(connection).close(); // ✅ not mocked
verify(dataSource).getConnection(); // ❌ redundant — already mocked
4. 优先使用具体对象桩,而不是 any() 匹配器
当测试数据由你控制时,使用具体对象:
when(mapper.convert(input)).thenReturn(output); // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌
仅在以下情况下使用 any():服务会不可预测地修改对象、可能传入多个不同对象,或使用参数捕获器时。
5. 基本返回值优先使用简单的 when().thenReturn()
when(service.process(data)).thenReturn(true); // ✅
doAnswer(inv -> true).when(service).process(data); // ❌
仅在返回值需要计算、存在条件逻辑或需要副作用时使用 thenAnswer()。
6. 为完整的服务流程创建桩
为被测代码路径中的每个外部调用创建桩:
when(repository.findByKey(key)).thenReturn(Optional.empty());
when(repository.save(entity)).thenReturn(entity);
when(repository.findById(id)).thenReturn(Optional.of(entity));
7. 谨慎使用 verifyNoMoreInteractions
仅包含其交互应已全部核对的模拟对象:
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dataSource, connection, databaseMetaData, resultSet);
}
测试结构
1. 类设置
@UnitTest
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
private static final String CONSTANT_VALUE = "test-value";
@Mock
private Dependency1 dependency1;
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dependency1);
}
}
2. 单个测试
@Test
void methodName_scenario_expectedBehavior() throws Exception {
// Arrange
setupCommonMocks();
var service = new MyService(dependency1);
when(dependency1.doSomething()).thenReturn(expectedValue);
// Act
var result = service.methodUnderTest();
// Assert
assertThat(result).isEqualTo(expectedValue);
verify(dependency1).close();
}
验证模式
1. 成功操作
@Test
void operation_positive_success() throws Exception {
setupRequiredMocks();
when(repository.find()).thenReturn(entity);
var result = service.operation();
assertThat(result).isNotNull();
verify(connection).close();
}
2. 异常场景
@Test
void operation_negative_throwsException() throws Exception {
var expectedException = new SQLException("Connection failed");
when(dataSource.getConnection()).thenThrow(expectedException);
assertThatThrownBy(() -> service.operation())
.isInstanceOf(DataRetrievalFailureException.class)
.hasMessageContaining("Failed to perform operation")
.hasCause(expectedException);
}
常见模式
1. 测试数据常量
在类级别定义,以便在多个测试中重复使用:
private static final String MODULE_ID = "mod-foo-1.0.0";
private static final String TENANT_ID = "test-tenant";
2. 辅助方法
当相同代码出现在 2 个或更多测试中时,将对象创建、断言、查找对象和模拟设置提取为 private static 方法。
有关高级模式(参数化测试、深拷贝桩、流式 API、精确匹配、显式 Lambda 类型),请参阅 references/patterns.md。
完整的测试类示例请参阅 references/examples.md。
检查清单
- 没有
@MockitoSettings(strictness = Strictness.LENIENT) - 没有不必要的桩——所有
when()语句都已使用 - 只验证未模拟的方法
- 测试数据可控时,使用具体对象桩,而不是
any() - 基本返回值使用简单的
when().thenReturn() - 代码路径中的所有仓库/服务交互都已创建桩
- 服务创建内部副本时,为复制操作创建桩
- 测试名称遵循
methodName_scenario_expectedBehavior - 每个测试都相互独立
- 验证资源(连接、流)已关闭
- 异常测试验证类型、消息和原因
- 可重复使用的测试数据使用常量
- 辅助方法消除了代码重复
- 清晰划分准备、执行和断言部分
- 使用流式 API 创建测试数据
- 单个参数的参数化测试使用
Stream<Type> -
var推断失败时,为 Lambda 显式声明类型 - 精确匹配测试包含相似但不匹配的值
- 覆盖工具方法的所有错误路径
- 移除未使用的导入