unit-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Unit 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_expectedBehavior
Examples:
tableExists_positive_tableIsPresent
,
deleteTimer_negative_timerDescriptorIdIsNull
遵循命名模式:
methodName_scenario_expectedBehavior
示例:
tableExists_positive_tableIsPresent
,
deleteTimer_negative_timerDescriptorIdIsNull

3. Test Organization

3. 测试结构组织

Structure every test with: ArrangeActAssert
每个测试都需遵循Arrange(准备)Act(执行)→**Assert(断言)**的结构

Mockito Best Practices

Mockito最佳实践

1. Never Use Lenient Mode

1. 禁用宽松模式

❌ Never add
@MockitoSettings(strictness = Strictness.LENIENT)
— write precise tests instead.
❌ 切勿添加
@MockitoSettings(strictness = Strictness.LENIENT)
——应编写精准的测试用例。

2. Avoid Unnecessary Stubbing

2. 避免不必要的存根

Only stub (
when()
) what is actually used in the test. Use helper methods called only by tests that need them:
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

4. 优先使用特定对象存根而非
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
any()
only when: the service mutates the object unpredictably, multiple different objects may be passed, or when using argument captors.
当你能控制测试数据时,使用特定对象:
java
when(mapper.convert(input)).thenReturn(output);           // ✅
when(mapper.convert(any(InputType.class))).thenReturn(output); // ❌
仅在以下场景使用
any()
:服务会不可预测地修改对象、可能传入多个不同对象,或使用参数捕获器时。

5. Use Simple
when().thenReturn()
for Basic Returns

5. 基础返回值使用简单的
when().thenReturn()

java
when(service.process(data)).thenReturn(true);       // ✅
doAnswer(inv -> true).when(service).process(data);  // ❌
Use
thenAnswer()
only for computed return values, conditional logic, or side effects.
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
private static
methods for object creation, assertions, finding objects, and mock setup when the same code appears in 2+ tests.
For 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
    when()
    statements are used
  • Only verify methods that are NOT mocked
  • Specific object stubs over
    any()
    when test data is controlled
  • Simple
    when().thenReturn()
    for basic returns
  • 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
    Stream<Type>
    for single parameters
  • 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添加显式类型声明
  • 精确匹配测试包含相似的非匹配值
  • 覆盖工具方法的所有错误路径
  • 移除未使用的导入