Spring Boot 测试完全指南
集成测试、Controller 测试、Repository 测试的完整方案
测试注解体系
Spring Boot 提供了一套完整的测试注解,基于 JUnit 5:
| 注解 | 范围 | 用途 |
|---|---|---|
@SpringBootTest | 集成测试 | 加载完整上下文 |
@WebMvcTest | Controller 层 | 仅加载 Web 层 |
@DataJpaTest | Repository 层 | 仅加载 JPA 配置 |
@JsonTest | JSON 序列化 | 测试 JSON 映射 |
@MockBean | 任意 Bean | Mock 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