Skip to content

Spring Boot 测试完全指南

集成测试、Controller 测试、Repository 测试的完整方案


测试注解体系

Spring Boot 提供了一套完整的测试注解,基于 JUnit 5:

注解范围用途
@SpringBootTest集成测试加载完整上下文
@WebMvcTestController 层仅加载 Web 层
@DataJpaTestRepository 层仅加载 JPA 配置
@JsonTestJSON 序列化测试 JSON 映射
@MockBean任意 BeanMock Spring Bean
@TestConfiguration测试配置定义测试专用配置

@SpringBootTest

完整集成测试

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DisplayName("用户服务集成测试")
class UserServiceIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("创建用户后应能通过 API 获取")
    void createAndFindUser() {
        userRepository.deleteAll();

        UserRequest request = new UserRequest("张三", "zhangsan@example.com", 25);
        ResponseEntity<UserResponse> response = restTemplate.postForEntity(
            "/api/users", request, UserResponse.class);

        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertNotNull(response.getBody());

        UserResponse found = restTemplate.getForObject(
            "/api/users/" + response.getBody().getId(), UserResponse.class);
        assertEquals("张三", found.getName());
    }
}

WebEnvironment 选项

说明
MOCK(默认)使用 Mock Web 环境
RANDOM_PORT启动随机端口的真实服务器
DEFINED_PORT使用配置的固定端口
NONE不启动 Web 环境

@WebMvcTest

Controller 单元测试

java
@WebMvcTest(UserController.class)
@DisplayName("用户控制器测试")
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private UserService userService;

    @MockBean
    private JwtService jwtService;

    @Test
    @DisplayName("GET /users/{id} 应返回用户信息")
    void getUserById() throws Exception {
        when(userService.findById(1L))
            .thenReturn(Optional.of(new User(1L, "张三", 25)));

        mockMvc.perform(get("/users/1").accept(MediaType.APPLICATION_JSON))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.id").value(1))
               .andExpect(jsonPath("$.name").value("张三"));

        verify(userService).findById(1L);
    }

    @Test
    @DisplayName("GET /users/{id} 用户不存在应返回 404")
    void getUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/users/999"))
               .andExpect(status().isNotFound());
    }

    @Test
    @DisplayName("POST /users 应创建新用户")
    void createUser() throws Exception {
        UserRequest request = new UserRequest("李四", "lisi@example.com", 30);
        when(userService.create(any())).thenReturn(new User(2L, "李四", 30));

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
               .andExpect(status().isCreated())
               .andExpect(jsonPath("$.name").value("李四"));
    }
}

@DataJpaTest

Repository 测试

java
@DataJpaTest
@DisplayName("用户 Repository 测试")
class UserRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("按邮箱查找用户")
    void findByEmail() {
        entityManager.persist(new User("zhangsan@example.com", "张三"));
        entityManager.persist(new User("lisi@example.com", "李四"));
        entityManager.flush();

        Optional<User> found = userRepository.findByEmail("zhangsan@example.com");

        assertTrue(found.isPresent());
        assertEquals("张三", found.get().getName());
    }

    @Test
    @DisplayName("查找年龄范围内的用户")
    void findByAgeBetween() {
        entityManager.persist(new User("a@example.com", "A", 15));
        entityManager.persist(new User("b@example.com", "B", 25));
        entityManager.persist(new User("c@example.com", "C", 35));
        entityManager.flush();

        List<User> found = userRepository.findByAgeBetween(20, 30);

        assertEquals(1, found.size());
        assertEquals("B", found.get(0).getName());
    }
}

@AutoConfigureTestDatabase

java
// 使用内存数据库(默认 H2)
@DataJpaTest

// 使用真实数据库
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)

@MockBean 与服务层测试

java
@SpringBootTest
@DisplayName("订单服务测试")
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean
    private OrderRepository orderRepository;

    @MockBean
    private PaymentGateway paymentGateway;

    @Test
    @DisplayName("支付成功应更新订单状态")
    void processPaymentSuccess() {
        Order order = new Order(1L, "商品A", 100.0, OrderStatus.PENDING);
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
        when(paymentGateway.process(any())).thenReturn(true);

        orderService.processPayment(1L, "txn123");

        assertEquals(OrderStatus.PAID, order.getStatus());
    }
}

@TestConfiguration

替代 Bean

java
@TestConfiguration
static class TestConfig {
    @Bean
    @Primary
    public Clock clock() {
        return Clock.fixed(
            Instant.parse("2024-01-01T00:00:00Z"), 
            ZoneId.of("UTC"));
    }
}

@SpringBootTest
class OrderServiceWithFixedTimeTest {
    @Test
    void testWithFixedTime(@Autowired Clock clock) {
        // Clock 已被替换为固定时间
    }
}

TestContainers(真实数据库)

Maven 依赖

xml
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.19.7</version>
    <scope>test</scope>
</dependency>

使用 PostgreSQL

java
@SpringBootTest
@Testcontainers
class IntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private MockMvc mockMvc;

    @Test
    void fullIntegration() throws Exception {
        mockMvc.perform(get("/health"))
               .andExpect(status().isOk());
    }
}

事务管理

测试回滚

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

    @Autowired
    private UserRepository userRepository;

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

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

Maven 依赖(完整版)

xml
<dependencies>
    <!-- Spring Boot Test(包含 JUnit 5, AssertJ, Mockito) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- TestContainers -->
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>1.19.7</version>
        <scope>test</scope>
    </dependency>

    <!-- REST Assured -->
    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

    <!-- H2 Database -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

MockMvc 常用匹配器

java
mockMvc.perform(get("/path"))
       .andExpect(status().isOk())
       .andExpect(jsonPath("$.name").value("张三"))
       .andExpect(header().exists("Location"))
       .andExpect(cookie().value("session", "abc123"))
       .andDo(print())  // 打印请求响应

相关页面

  • **Java\JUnit5-完全指南** - 返回总览
  • **Java\JUnit5-断言机制** - 断言使用
  • **Java\Mockito-完全指南** - Mock 与 Spring 集成
  • **Java\JUnit5-最佳实践** - 测试策略与架构

#标签 #spring-boot #testing #springboottest #webmvctest #datajpatest #java

Knowledge4J — Java 知识库