A professional guide to writing high-quality unit tests, based on the JUnit 5 and Mockito frameworks.
Testing Principles
- Test Independence: Each test case should run independently, without depending on the execution order or results of other tests
- Single Responsibility: Each test verifies only one specific behavior or scenario
- Clear Naming: Test method names should clearly describe the test scenario; recommended format:
方法名_测试场景_期望结果 - Comprehensive Coverage: Includes three dimensions: normal scenarios, boundary conditions, and exceptional situations
Test Structure (AAA Pattern)
Each test case should follow the Arrange-Act-Assert three-part structure:
@Test
void calculateTotal_withValidItems_returnsCorrectSum() {
// Arrange - 准备测试数据和依赖
List<Item> items = Arrays.asList(
new Item("A", 10.0),
new Item("B", 20.0)
);
Calculator calculator = new Calculator();
// Act - 执行被测试的方法
double result = calculator.calculateTotal(items);
// Assert - 验证结果是否符合期望
assertEquals(30.0, result, 0.001);
}
Common Assertions
JUnit 5 Assertions
assertEquals(expected, actual)- Verify equalityassertNotNull(actual)- Verify not nullassertTrue(condition)/assertFalse(condition)- Verify boolean conditionsassertThrows(Exception.class, () -> {...})- Verify exception thrownassertAll()- Batch assertions, all executedassertTimeout(duration, () -> {...})- Verify execution time
AssertJ Fluent Assertions (Recommended)
assertThat(result)
.isNotNull()
.isInstanceOf(User.class)
.extracting(User::getName)
.isEqualTo("张三");
Mock Usage Guide
Basic Mock
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void getUser_whenExists_returnsUser() {
// 定义Mock行为
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User("张三")));
// 执行测试
User user = userService.getUser(1L);
// 验证结果
assertEquals("张三", user.getName());
// 验证方法调用
verify(userRepository).findById(1L);
}
}
Mock Precautions
- Use
@Mockannotation to mark dependencies - Use
@InjectMocksto automatically inject Mock objects - Use
when().thenReturn()to define Mock return values - Use
verify()to verify whether a method was called - Use
any()、eq()and other matchers to flexibly match parameters
Boundary Condition Testing Checklist
Boundary conditions that must be tested:
- Null value (null): The case where an input parameter is null
- Empty collection: Empty list, empty Map, empty string
- Single-element collection: A collection with only one element
- Large amount of data: Performance and correctness in large data volume scenarios
- Boundary values: Maximum value, minimum value, 0, -1, etc.
- Special characters: Spaces, newline characters, special symbols
- Concurrent scenarios: Multi-threaded concurrent access
Test Coverage Strategy
Method-level Coverage
Each public method should include at least the following tests:
- Normal case tests
- Exception case tests (invalid parameters, abnormal state, etc.)
- Boundary condition tests
Branch Coverage
- Every branch of if/else should be covered by tests
- Every case of switch/case should be tested
- Boundary conditions of loops (0 times, 1 time, multiple times)
Best Practices
- Test First: Adopt TDD (Test-Driven Development) when possible
- Fast Execution: Unit tests should execute quickly and avoid depending on external resources
- Repeatable: Test results should be stable and repeatable, not dependent on the execution environment
- Automation: Integrate into the CI/CD process for automatic execution
- Regular Refactoring: Test code also needs refactoring and maintenance
Anti-patterns (Avoid)
❌ Testing private methods (should test public interfaces) ❌ Testing third-party libraries (should be mocked out) ❌ Testing getters/setters (unnecessary) ❌ Over-mocking (leads to fragile tests) ❌ Ignoring exception testing