Skip to content

JUnit 5 最佳实践与测试设计模式

编写高质量、可维护单元测试的规范与模式


测试分层策略

金字塔模型

            ┌─────────┐
            │   E2E   │  ← 少量端到端测试
           ┌┴─────────┴┐
          ┌┴───────────┴┐
         ┌┴─────────────┴┐
        │   集成测试     │  ← 中等数量(@SpringBootTest)
       ┌┴───────────────┴┐
      ┌┴─────────────────┴┐
     │      单元测试       │  ← 大量单元测试(纯逻辑)
    ┌┴───────────────────┴┘
         JUnit 5 + Mockito

分层测试示例

java
// 1. 单元测试:纯逻辑,不依赖 Spring
@ExtendWith(MockitoExtension.class)
class CalculatorUnitTest {
    @Test
    void add_twoPositiveNumbers() {
        assertEquals(5, new Calculator().add(2, 3));
    }
}

// 2. 集成测试:数据库、缓存等真实依赖
@DataJpaTest
class UserRepositoryIntegrationTest {
    @Test
    void saveAndFindByEmail() { /* 使用真实 H2 */ }
}

// 3. 端到端测试:完整 HTTP 请求
@SpringBootTest
@AutoConfigureMockMvc
class UserApiE2ETest {
    @Test
    void fullCRUD() { /* 真实 HTTP */ }
}

测试命名规范

推荐风格

java
// 方法名风格
@Test
void add_positiveNumbers_returnsSum() { }

@Test
void findById_nonExistingUser_throwsException() { }

// @DisplayName 中文命名(最清晰)
@DisplayName("🧮 计算器")
class CalculatorTest {

    @Test
    @DisplayName("➕ 两个正数相加应返回正确和")
    void addTwoPositiveNumbers() { }

    @Test
    @DisplayName("➗ 除数为零时应抛出 ArithmeticException")
    void divideByZeroShouldThrow() { }
}

AAA 模式(Arrange-Act-Assert)

标准结构

java
@Test
@DisplayName("创建订单应发送确认邮件")
void createOrderShouldSendConfirmationEmail() {
    // Arrange(准备)
    OrderRequest request = new OrderRequest("商品A", 2, 100.0);
    when(userRepository.findById(1L)).thenReturn(Optional.of(user));
    when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

    // Act(执行)
    OrderResult result = orderService.createOrder(request, 1L);

    // Assert(断言)
    assertTrue(result.isSuccess());
    assertEquals(100L, result.getOrderId());

    // Verify(验证)
    verify(emailService).sendConfirmation(any());
}

简化版(准备很简单时)

java
@Test
void addNumbers() {
    Calculator calc = new Calculator();  // Arrange
    int result = calc.add(2, 3);       // Act
    assertEquals(5, result);           // Assert
}

测试隔离原则

每个测试独立运行

java
@DataJpaTest
@Transactional  // 自动回滚,保持测试隔离
class UserRepositoryTest {

    @Test
    void test1() {
        repo.save(new User("A"));
        assertEquals(1, repo.count());
    }

    @Test
    void test2() {
        // 始终从干净状态开始,不受 test1 影响
        assertEquals(0, repo.count());
    }
}

每个测试只验证一件事

java
// ✅ 好:拆分多个测试
@Test
@DisplayName("创建用户应返回带 ID 的用户对象")
void createUserReturnsUserWithId() {
    User user = service.create(request);
    assertNotNull(user.getId());
}

@Test
@DisplayName("创建用户应发送欢迎邮件")
void createUserSendsWelcomeEmail() {
    service.create(request);
    verify(emailService).sendWelcomeEmail(any());
}

测试替身(Test Doubles)

类型目的Mockito 方式
Dummy填充参数列表不配置返回值
Fake简化实现new InMemoryUserRepository()
Stub提供预设响应when().thenReturn()
Spy部分真实行为@Spy
Mock验证交互verify()

示例

java
// Fake - 简化实现
class InMemoryUserRepository implements UserRepository {
    private final Map<Long, User> db = new ConcurrentHashMap<>();
}

// Stub - 提供预设响应
@Test
void findUserById_stubResponse() {
    when(userRepo.findById(1L)).thenReturn(Optional.of(new User("张三")));
    User user = service.findUser(1L);
    assertNotNull(user);
}

// Mock - 验证交互
@Test
void createUser_verifyEmailSent() {
    service.createUser("test@example.com");
    verify(emailService).sendWelcomeEmail("test@example.com");
}

异常测试模式

java
@Test
@DisplayName("除数为零应抛出 ArithmeticException")
void divideByZeroThrowsException() {
    ArithmeticException ex = assertThrows(
        ArithmeticException.class,
        () -> calculator.divide(10, 0)
    );
    assertEquals("Division by zero", ex.getMessage());
}

私有方法测试策略

策略 1:不要测试私有方法(推荐)

java
public class OrderService {
    public void placeOrder(OrderRequest request) {
        validateRequest(request);        // private - 不测
        Order order = createOrder(request); // private - 不测
        sendNotification(order);          // public - 测试
    }
}

策略 2:重构为独立类(可测试)

java
// 重构成独立的验证器类
class OrderValidator {
    public ValidationResult validate(OrderRequest request) { }
}

class OrderValidatorTest {
    @Test
    void emptyProductName_shouldFail() {
        ValidationResult result = new OrderValidator()
            .validate(new OrderRequest("", 1));
        assertFalse(result.isValid());
    }
}

测试性能优化

并行执行

properties
# junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=dynamic

测试超时控制

java
@Test
@Timeout(value = 1, unit = TimeUnit.SECONDS)
void shouldCompleteInOneSecond() { }

常见反模式

反模式问题解决方案
Thread.sleep(1000)不稳定、慢Awaitility 等待条件
测试依赖数据库状态顺序敏感每个测试自包含
assertTrue(list.isEmpty())错误消息差assertThat(list).isEmpty()
Mock 所有东西不真实用 Fake 替代部分 Mock
过度测试私有方法脆弱测试公开行为
全局共享状态互相干扰使用 @BeforeEach 重置

测试类模板

java
@DisplayName("Xxx 模块测试")
@ExtendWith(MockitoExtension.class)
class XxxServiceTest {

    @Mock
    private XxxRepository xxxRepository;

    @InjectMocks
    private XxxService xxxService;

    @Nested
    @DisplayName("methodName 方法")
    class MethodNameTests {

        @Test
        @DisplayName("场景描述")
        void scenario() { }
    }
}

工具速查

工具用途
AssertJ流式断言库
MockitoMock 框架
TestContainers真实数据库测试
REST AssuredREST API 测试 DSL
WireMockHTTP Mock 服务器
ArchUnit架构规则测试
JaCoCo代码覆盖率
Pitest突变测试
Awaitility异步测试等待

相关页面

  • **Java\JUnit5-完全指南** - 返回总览
  • **Java\JUnit5-注解体系** - 注解参考
  • **Java\JUnit5-断言机制** - 断言使用
  • **Java\Mockito-完全指南** - Mock 最佳实践
  • **Java\SpringBoot-测试专题** - Spring 集成测试

#标签 #junit #best-practices #testing #design-patterns #tdd #aaa

Knowledge4J — Java 知识库