单元测试的艺术:JUnit 5 + Mockito 最佳实践

David Ng | 2026-08-26T23:02:12 | Java

从测试命名、Mock 策略到参数化测试,分享编写高质量 Java 单元测试的十条实践经验。

# 单元测试的艺术:JUnit 5 + Mockito 最佳实践 ## 原则一:测试命名遵循 Given-When-Then ```java @Test @DisplayName("已存在的用户名注册时应抛出异常") void givenExistingUsername_whenRegister_thenThrowException() { // Given when(userRepository.existsByUsername("alice")).thenReturn(true); // When & Then assertThrows(BusinessException.class, () -> userService.register(new RegisterRequest("alice", "password123")) ); } ``` ## 原则二:只 Mock 外部依赖 ```java @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private OrderRepository orderRepository; // Mock 数据库 @Mock private PaymentClient paymentClient; // Mock 外部 API @InjectMocks private OrderService orderService; // 被测对象用真实实现 @Test void shouldCalculateTotalCorrectly() { // 不要 Mock 被测对象内部的计算逻辑 Order order = new Order(List.of( new OrderItem("商品A", 2, new BigDecimal("100")), new OrderItem("商品B", 1, new BigDecimal("200")) )); when(orderRepository.findById(1L)).thenReturn(Optional.of(order)); BigDecimal total = orderService.calculateTotal(1L); assertEquals(new BigDecimal("400"), total); } } ``` ## 原则三:参数化测试 ```java @ParameterizedTest @CsvSource({ "admin@example.com, true", "user@test.org, true", "invalid-email, false", "'', false", "a@b, false" }) void shouldValidateEmail(String email, boolean expected) { assertEquals(expected, validator.isValidEmail(email)); } @ParameterizedTest @MethodSource("providePasswords") void shouldValidatePasswordStrength(String password, PasswordStrength expected) { assertEquals(expected, validator.checkStrength(password)); } static Stream providePasswords() { return Stream.of( Arguments.of("123", PasswordStrength.WEAK), Arguments.of("abc12345", PasswordStrength.MEDIUM), Arguments.of("Abc123!@#xyz", PasswordStrength.STRONG) ); } ``` ## 原则四:验证行为而非实现 ```java // 好:验证业务结果 verify(emailService).sendWelcomeEmail("alice@example.com"); assertEquals(UserStatus.ACTIVE, savedUser.getStatus()); // 差:验证方法调用次数(过度耦合实现细节) verify(userRepository, times(1)).save(any()); verify(userRepository, times(1)).findByUsername(any()); ``` ## 原则五:测试边界条件 ```java @Test void shouldHandleEmptyList() { assertEquals(BigDecimal.ZERO, calculator.sum(List.of())); } @Test void shouldHandleNullInput() { assertThrows(IllegalArgumentException.class, () -> calculator.sum(null) ); } @Test void shouldHandleMaxValue() { List items = List.of(BigDecimal.valueOf(Long.MAX_VALUE)); assertDoesNotThrow(() -> calculator.sum(items)); } ``` ## 覆盖率不是目标 追求 100% 覆盖率是错误的。重点覆盖:核心业务逻辑、边界条件、错误处理路径。getter/setter、简单委托方法不需要测试。

← Back to Blog