unit-testing
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUnit Testing Guidelines
单元测试指南
General Principles
通用原则
1. Test Independence
1. 测试独立性
- Each test must be independent and self-contained
- Tests must not rely on execution order
- Use local variables within test methods instead of shared instance variables when possible
- 每个测试必须独立且自包含
- 测试不得依赖执行顺序
- 尽可能在测试方法内使用局部变量,而非共享实例变量
2. Clear Test Names
2. 清晰的测试命名
Follow the pattern:
methodName_scenario_expectedBehaviorExamples: ,
tableExists_positive_tableIsPresentdeleteTimer_negative_timerDescriptorIdIsNull遵循命名模式:
methodName_scenario_expectedBehavior示例:,
tableExists_positive_tableIsPresentdeleteTimer_negative_timerDescriptorIdIsNull3. Test Organization
3. 测试结构组织
Structure every test with: Arrange → Act → Assert
每个测试都需遵循Arrange(准备)→Act(执行)→**Assert(断言)**的结构
Mockito Best Practices
Mockito最佳实践
1. Never Use Lenient Mode
1. 禁用宽松模式
❌ Never add — write precise tests instead.
@MockitoSettings(strictness = Strictness.LENIENT)❌ 切勿添加——应编写精准的测试用例。
@MockitoSettings(strictness = Strictness.LENIENT)2. Avoid Unnecessary Stubbing
2. 避免不必要的存根
Only stub () what is actually used in the test. Use helper methods called only by tests that need them:
when()java
private void setupContextMocks() {
when(context.getFolioModuleMetadata()).thenReturn(moduleMetadata);
when(context.getTenantId()).thenReturn(TENANT_ID);
when(moduleMetadata.getDBSchemaName(TENANT_ID)).thenReturn(SCHEMA_NAME);
}仅对测试中实际用到的内容进行存根()。可使用仅被需要的测试调用的辅助方法:
when()java
private void setupContextMocks() {
when(context.getFolioModuleMetadata()).thenReturn(moduleMetadata);
when(context.getTenantId()).thenReturn(TENANT_ID);
when(moduleMetadata.getDBSchemaName(TENANT_ID)).thenReturn(SCHEMA_NAME);
}3. Remove Redundant Verify Statements
3. 移除冗余的Verify语句
Only verify interactions that are not mocked with :
when()java
verify(connection).close(); // ✅ not mocked
verify(dataSource).getConnection(); // ❌ redundant — already mocked仅对未通过进行存根的交互进行验证:
when()java
verify(connection).close(); // ✅ 未被存根
verify(dataSource).getConnection(); // ❌ 冗余——已被存根4. Prefer Specific Object Stubs Over any()
Matchers
any()4. 优先使用特定对象存根而非any()
匹配器
any()Use specific objects when you control test data:
java
when(mapper.convert(input)).thenReturn(output); // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌Use only when: the service mutates the object unpredictably, multiple different objects may be passed, or when using argument captors.
any()当你能控制测试数据时,使用特定对象:
java
when(mapper.convert(input)).thenReturn(output); // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌仅在以下场景使用:服务会不可预测地修改对象、可能传入多个不同对象,或使用参数捕获器时。
any()5. Use Simple when().thenReturn()
for Basic Returns
when().thenReturn()5. 基础返回值使用简单的when().thenReturn()
when().thenReturn()java
when(service.process(data)).thenReturn(true); // ✅
doAnswer(inv -> true).when(service).process(data); // ❌Use only for computed return values, conditional logic, or side effects.
thenAnswer()java
when(service.process(data)).thenReturn(true); // ✅
doAnswer(inv -> true).when(service).process(data); // ❌仅在需要计算返回值、条件逻辑或副作用时使用。
thenAnswer()6. Stub the Complete Service Flow
6. 对完整服务流程进行存根
Stub every external call in the code path being tested:
java
when(repository.findByKey(key)).thenReturn(Optional.empty());
when(repository.save(entity)).thenReturn(entity);
when(repository.findById(id)).thenReturn(Optional.of(entity));对测试代码路径中的所有外部调用进行存根:
java
when(repository.findByKey(key)).thenReturn(Optional.empty());
when(repository.save(entity)).thenReturn(entity);
when(repository.findById(id)).thenReturn(Optional.of(entity));7. Use verifyNoMoreInteractions Carefully
7. 谨慎使用verifyNoMoreInteractions
Only include mocks that should be fully accounted for:
java
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dataSource, connection, databaseMetaData, resultSet);
}仅包含需要完全追踪的模拟对象:
java
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dataSource, connection, databaseMetaData, resultSet);
}Test Structure
测试结构
1. Class Setup
1. 类初始化
java
@UnitTest
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
private static final String CONSTANT_VALUE = "test-value";
@Mock
private Dependency1 dependency1;
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dependency1);
}
}java
@UnitTest
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
private static final String CONSTANT_VALUE = "test-value";
@Mock
private Dependency1 dependency1;
@AfterEach
void tearDown() {
verifyNoMoreInteractions(dependency1);
}
}2. Individual Test
2. 单个测试用例
java
@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();
}java
@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();
}Verification Patterns
验证模式
1. Successful Operations
1. 成功操作场景
java
@Test
void operation_positive_success() throws Exception {
setupRequiredMocks();
when(repository.find()).thenReturn(entity);
var result = service.operation();
assertThat(result).isNotNull();
verify(connection).close();
}java
@Test
void operation_positive_success() throws Exception {
setupRequiredMocks();
when(repository.find()).thenReturn(entity);
var result = service.operation();
assertThat(result).isNotNull();
verify(connection).close();
}2. Exception Scenarios
2. 异常场景
java
@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);
}java
@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);
}Common Patterns
通用模式
1. Constants for Test Data
1. 测试数据常量
Define at class level for reuse across tests:
java
private static final String MODULE_ID = "mod-foo-1.0.0";
private static final String TENANT_ID = "test-tenant";在类级别定义,以便在多个测试中复用:
java
private static final String MODULE_ID = "mod-foo-1.0.0";
private static final String TENANT_ID = "test-tenant";2. Helper Methods
2. 辅助方法
Extract methods for object creation, assertions, finding objects, and mock setup when the same code appears in 2+ tests.
private staticFor advanced patterns (parameterized tests, deep copy stubbing, fluent API, exact matching, explicit lambda types), see references/patterns.md.
For complete test class examples, see references/examples.md.
当相同代码出现在2个及以上测试中时,提取方法用于对象创建、断言、对象查找和模拟对象初始化。
private static如需了解高级模式(参数化测试、深拷贝存根、流畅API、精确匹配、显式lambda类型),请参阅references/patterns.md。
完整测试类示例,请参阅references/examples.md。
Checklist
检查清单
- No
@MockitoSettings(strictness = Strictness.LENIENT) - No unnecessary stubbing — all statements are used
when() - Only verify methods that are NOT mocked
- Specific object stubs over when test data is controlled
any() - Simple for basic returns
when().thenReturn() - All repository/service interactions in the code path are stubbed
- Copy operations stubbed when service creates internal copies
- Test names follow
methodName_scenario_expectedBehavior - Each test is independent
- Resources (connections, streams) verified to be closed
- Exception tests verify type, message, and cause
- Constants used for reusable test data
- Helper methods eliminate code duplication
- Clear Arrange-Act-Assert sections
- Fluent API used for test data creation
- Parameterized tests use for single parameters
Stream<Type> - Explicit type declarations for lambdas when var inference fails
- Exact matching tests include similar non-matching values
- All error paths from utility methods covered
- Unused imports removed
- 未使用
@MockitoSettings(strictness = Strictness.LENIENT) - 无不必要的存根——所有语句均被使用
when() - 仅对未被存根的方法进行验证
- 当测试数据可控制时,使用特定对象存根而非
any() - 基础返回值使用简单的
when().thenReturn() - 代码路径中的所有仓库/服务交互均已被存根
- 当服务创建内部副本时,存根拷贝操作
- 测试名称遵循模式
methodName_scenario_expectedBehavior - 每个测试都是独立的
- 验证资源(连接、流)已被关闭
- 异常测试验证了异常类型、消息和原因
- 使用常量复用测试数据
- 使用辅助方法消除代码重复
- 清晰的Arrange-Act-Assert分段
- 使用流畅API创建测试数据
- 参数化测试使用处理单个参数
Stream<Type> - 当var推断失败时,为lambda添加显式类型声明
- 精确匹配测试包含相似的非匹配值
- 覆盖工具方法的所有错误路径
- 移除未使用的导入