spring-boot-test
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSpring Boot Testing
Spring Boot 测试
Deep Knowledge: Usewith technology:mcp__documentation__fetch_docsfor comprehensive documentation.spring-boot-test
深度参考:使用并指定技术栈为mcp__documentation__fetch_docs以获取完整文档。spring-boot-test
When NOT to Use This Skill
不适用本技能的场景
- Pure Unit Tests - Use with Mockito for faster tests without Spring context
junit - Integration Tests with Real Database - Use with Testcontainers
spring-boot-integration - REST API Client Testing - Use for HTTP testing
rest-assured - E2E Web Testing - Use Selenium or Playwright
- Microservice Contract Testing - Use Spring Cloud Contract
- 纯单元测试 - 使用搭配Mockito执行无需Spring上下文的快速测试
junit - 使用真实数据库的集成测试 - 使用搭配Testcontainers
spring-boot-integration - REST API客户端测试 - 使用进行HTTP测试
rest-assured - 端到端Web测试 - 使用Selenium或Playwright
- 微服务契约测试 - 使用Spring Cloud Contract
Dependencies
依赖配置
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>Includes: JUnit 5, Mockito, AssertJ, Hamcrest, JSONPath, Spring Test
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>包含组件:JUnit 5、Mockito、AssertJ、Hamcrest、JSONPath、Spring Test
Test Annotations
测试注解
@SpringBootTest - Full Context
@SpringBootTest - 完整上下文
java
@SpringBootTest
class ApplicationTest {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertThat(userService).isNotNull();
}
}
// With web environment
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class WebApplicationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void healthCheck() {
ResponseEntity<String> response = restTemplate
.getForEntity("/actuator/health", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}java
@SpringBootTest
class ApplicationTest {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertThat(userService).isNotNull();
}
}
// 配置Web环境
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class WebApplicationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void healthCheck() {
ResponseEntity<String> response = restTemplate
.getForEntity("/actuator/health", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}Slice Tests (Faster, Focused)
切片测试(更快速、聚焦)
| Annotation | Layer | Auto-configured |
|---|---|---|
| Controllers | MockMvc, Jackson |
| JPA Repositories | TestEntityManager, DataSource |
| MongoDB | MongoTemplate |
| JSON serialization | JacksonTester |
| REST clients | MockRestServiceServer |
java
// Controller test - only loads web layer
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_ReturnsUser() throws Exception {
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
// Repository test - uses embedded database
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void findByEmail_ReturnsUser() {
User user = new User("test@example.com", "Test User");
entityManager.persistAndFlush(user);
Optional<User> found = repository.findByEmail("test@example.com");
assertThat(found).isPresent()
.hasValueSatisfying(u -> assertThat(u.getName()).isEqualTo("Test User"));
}
}| 注解 | 层级 | 自动配置内容 |
|---|---|---|
| 控制器层 | MockMvc、Jackson |
| JPA仓库层 | TestEntityManager、DataSource |
| MongoDB层 | MongoTemplate |
| JSON序列化层 | JacksonTester |
| REST客户端层 | MockRestServiceServer |
java
// 控制器测试 - 仅加载Web层
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_ReturnsUser() throws Exception {
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
// 仓库测试 - 使用嵌入式数据库
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void findByEmail_ReturnsUser() {
User user = new User("test@example.com", "Test User");
entityManager.persistAndFlush(user);
Optional<User> found = repository.findByEmail("test@example.com");
assertThat(found).isPresent()
.hasValueSatisfying(u -> assertThat(u.getName()).isEqualTo("Test User"));
}
}MockMvc
MockMvc
Basic Requests
基础请求示例
java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
// GET request
@Test
void getUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
));
mockMvc.perform(get("/api/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("Alice"));
}
// POST request with JSON body
@Test
void createUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
User createdUser = new User(1L, "John", "john@example.com");
when(userService.create(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "john@example.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1));
}
// PUT request
@Test
void updateUser() throws Exception {
mockMvc.perform(put("/api/users/{id}", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Updated Name"}
"""))
.andExpect(status().isOk());
verify(userService).update(eq(1L), any());
}
// DELETE request
@Test
void deleteUser() throws Exception {
mockMvc.perform(delete("/api/users/{id}", 1))
.andExpect(status().isNoContent());
verify(userService).delete(1L);
}
}java
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
// GET请求
@Test
void getUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
));
mockMvc.perform(get("/api/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("Alice"));
}
// 带JSON请求体的POST请求
@Test
void createUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("John", "john@example.com");
User createdUser = new User(1L, "John", "john@example.com");
when(userService.create(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "john@example.com"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1));
}
// PUT请求
@Test
void updateUser() throws Exception {
mockMvc.perform(put("/api/users/{id}", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Updated Name"}
"""))
.andExpect(status().isOk());
verify(userService).update(eq(1L), any());
}
// DELETE请求
@Test
void deleteUser() throws Exception {
mockMvc.perform(delete("/api/users/{id}", 1))
.andExpect(status().isNoContent());
verify(userService).delete(1L);
}
}With Authentication
带身份验证的测试
java
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
// Using @WithMockUser
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_WithAdminRole_Succeeds() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_WithUserRole_Forbidden() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
// Using JWT token
@Test
void withJwtToken() throws Exception {
String token = jwtTokenProvider.createToken("admin", List.of("ROLE_ADMIN"));
mockMvc.perform(get("/api/admin/dashboard")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}java
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
// 使用@WithMockUser
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_WithAdminRole_Succeeds() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_WithUserRole_Forbidden() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
// 使用JWT令牌
@Test
void withJwtToken() throws Exception {
String token = jwtTokenProvider.createToken("admin", List.of("ROLE_ADMIN"));
mockMvc.perform(get("/api/admin/dashboard")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}Mocking
模拟对象
@MockBean
@MockBean
java
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean // Replaces bean in context with mock
private PaymentGateway paymentGateway;
@MockBean
private InventoryService inventoryService;
@Test
void placeOrder_WhenPaymentSucceeds_CreatesOrder() {
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
Order order = orderService.placeOrder(new OrderRequest(...));
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
verify(paymentGateway).charge(any());
}
@Test
void placeOrder_WhenPaymentFails_ThrowsException() {
when(paymentGateway.charge(any()))
.thenThrow(new PaymentException("Card declined"));
assertThatThrownBy(() -> orderService.placeOrder(new OrderRequest(...)))
.isInstanceOf(PaymentException.class)
.hasMessage("Card declined");
}
}java
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean // 用模拟对象替换上下文中的真实Bean
private PaymentGateway paymentGateway;
@MockBean
private InventoryService inventoryService;
@Test
void placeOrder_WhenPaymentSucceeds_CreatesOrder() {
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
Order order = orderService.placeOrder(new OrderRequest(...));
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
verify(paymentGateway).charge(any());
}
@Test
void placeOrder_WhenPaymentFails_ThrowsException() {
when(paymentGateway.charge(any()))
.thenThrow(new PaymentException("Card declined"));
assertThatThrownBy(() -> orderService.placeOrder(new OrderRequest(...)))
.isInstanceOf(PaymentException.class)
.hasMessage("Card declined");
}
}@SpyBean
@SpyBean
java
@SpringBootTest
class NotificationServiceTest {
@Autowired
private NotificationService notificationService;
@SpyBean // Wraps real bean, allows partial mocking
private EmailSender emailSender;
@Test
void sendNotification_CallsEmailSender() {
notificationService.notify(user, "Hello");
verify(emailSender).send(eq(user.getEmail()), any());
}
@Test
void sendNotification_WhenEmailFails_LogsError() {
doThrow(new EmailException("SMTP error"))
.when(emailSender).send(any(), any());
// Method should handle exception gracefully
assertThatCode(() -> notificationService.notify(user, "Hello"))
.doesNotThrowAnyException();
}
}java
@SpringBootTest
class NotificationServiceTest {
@Autowired
private NotificationService notificationService;
@SpyBean // 包装真实Bean,支持部分模拟
private EmailSender emailSender;
@Test
void sendNotification_CallsEmailSender() {
notificationService.notify(user, "Hello");
verify(emailSender).send(eq(user.getEmail()), any());
}
@Test
void sendNotification_WhenEmailFails_LogsError() {
doThrow(new EmailException("SMTP error"))
.when(emailSender).send(any(), any());
// 方法应优雅处理异常
assertThatCode(() -> notificationService.notify(user, "Hello"))
.doesNotThrowAnyException();
}
}Test Configuration
测试配置
Test Properties
测试属性
java
@SpringBootTest
@TestPropertySource(properties = {
"app.feature.enabled=true",
"app.external.url=http://localhost:8080"
})
class FeatureTest { }
// Or use test profile
@SpringBootTest
@ActiveProfiles("test")
class ProfileTest { }java
@SpringBootTest
@TestPropertySource(properties = {
"app.feature.enabled=true",
"app.external.url=http://localhost:8080"
})
class FeatureTest { }
// 或使用测试配置文件
@SpringBootTest
@ActiveProfiles("test")
class ProfileTest { }application-test.yml
application-test.yml
yaml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
app:
external:
url: http://localhost:${wiremock.server.port}yaml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
app:
external:
url: http://localhost:${wiremock.server.port}Custom Test Configuration
自定义测试配置
java
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(
Instant.parse("2025-01-15T10:00:00Z"),
ZoneId.of("UTC")
);
}
@Bean
@Primary
public PaymentGateway testPaymentGateway() {
return new FakePaymentGateway();
}
}
@SpringBootTest
@Import(TestConfig.class)
class TimeBasedFeatureTest { }java
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(
Instant.parse("2025-01-15T10:00:00Z"),
ZoneId.of("UTC")
);
}
@Bean
@Primary
public PaymentGateway testPaymentGateway() {
return new FakePaymentGateway();
}
}
@SpringBootTest
@Import(TestConfig.class)
class TimeBasedFeatureTest { }AssertJ Assertions
AssertJ断言
java
// Basic assertions
assertThat(user.getName()).isEqualTo("John");
assertThat(user.getAge()).isGreaterThan(18);
assertThat(user.getEmail()).contains("@").endsWith(".com");
// Collection assertions
assertThat(users)
.hasSize(3)
.extracting(User::getName)
.containsExactly("Alice", "Bob", "Charlie");
// Exception assertions
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null");
// Optional assertions
assertThat(repository.findById(1L))
.isPresent()
.hasValueSatisfying(user ->
assertThat(user.getName()).isEqualTo("John")
);
// Soft assertions (collect all failures)
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getEmail()).contains("@");
softly.assertThat(user.getAge()).isPositive();
});java
// 基础断言
assertThat(user.getName()).isEqualTo("John");
assertThat(user.getAge()).isGreaterThan(18);
assertThat(user.getEmail()).contains("@").endsWith(".com");
// 集合断言
assertThat(users)
.hasSize(3)
.extracting(User::getName)
.containsExactly("Alice", "Bob", "Charlie");
// 异常断言
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null");
// Optional断言
assertThat(repository.findById(1L))
.isPresent()
.hasValueSatisfying(user ->
assertThat(user.getName()).isEqualTo("John")
);
// 软断言(收集所有失败项)
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getEmail()).contains("@");
softly.assertThat(user.getAge()).isPositive();
});Test Data Builders
测试数据构建器
java
public class UserTestBuilder {
private Long id = 1L;
private String name = "Test User";
private String email = "test@example.com";
private UserRole role = UserRole.USER;
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withId(Long id) {
this.id = id;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestBuilder withRole(UserRole role) {
this.role = role;
return this;
}
public UserTestBuilder asAdmin() {
this.role = UserRole.ADMIN;
return this;
}
public User build() {
return new User(id, name, email, role);
}
}
// Usage
User admin = aUser().withName("Admin").asAdmin().build();
User regularUser = aUser().build();java
public class UserTestBuilder {
private Long id = 1L;
private String name = "Test User";
private String email = "test@example.com";
private UserRole role = UserRole.USER;
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withId(Long id) {
this.id = id;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestBuilder withRole(UserRole role) {
this.role = role;
return this;
}
public UserTestBuilder asAdmin() {
this.role = UserRole.ADMIN;
return this;
}
public User build() {
return new User(id, name, email, role);
}
}
// 使用示例
User admin = aUser().withName("Admin").asAdmin().build();
User regularUser = aUser().build();Anti-Patterns
反模式
| Anti-Pattern | Why It's Bad | Solution |
|---|---|---|
| Using @SpringBootTest for all tests | Extremely slow | Use slice tests (@WebMvcTest, @DataJpaTest) |
| Not using @MockBean | Testing real beans | Mock external dependencies |
| Hardcoding ports in tests | Port conflicts | Use @LocalServerPort with RANDOM_PORT |
| Testing private methods | Coupled to implementation | Test through controller/service API |
| Not isolating test data | Tests interfere | Use @Transactional or cleanup in @AfterEach |
| Ignoring @Sql scripts | Manual setup duplication | Use @Sql for test data setup |
| No test profiles | Polluting dev/prod config | Use @ActiveProfiles("test") |
| 反模式 | 问题所在 | 解决方案 |
|---|---|---|
| 所有测试都使用@SpringBootTest | 测试速度极慢 | 使用切片测试(@WebMvcTest、@DataJpaTest等) |
| 不使用@MockBean | 测试真实外部依赖Bean | 模拟外部依赖 |
| 测试中硬编码端口 | 端口冲突 | 使用@LocalServerPort搭配RANDOM_PORT |
| 测试私有方法 | 与实现细节耦合 | 通过控制器/服务API进行测试 |
| 不隔离测试数据 | 测试间相互干扰 | 使用@Transactional或在@AfterEach中清理数据 |
| 忽略@Sql脚本 | 手动重复设置测试数据 | 使用@Sql加载测试数据 |
| 不使用测试配置文件 | 污染开发/生产环境配置 | 使用@ActiveProfiles("test") |
Quick Troubleshooting
快速故障排除
| Problem | Likely Cause | Solution |
|---|---|---|
| "Unable to find @SpringBootConfiguration" | Main class not found | Add @SpringBootTest(classes = App.class) |
| Test very slow | Using @SpringBootTest unnecessarily | Use slice tests (@WebMvcTest, etc.) |
| "No qualifying bean" | Missing @MockBean | Add @MockBean for dependencies |
| Port already in use | Hardcoded port | Use webEnvironment = RANDOM_PORT |
| "Could not autowire" | Bean not in test context | Check component scan or add @Import |
| Flaky test | Database state not reset | Use @Transactional or @DirtiesContext |
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| "Unable to find @SpringBootConfiguration" | 未找到主启动类 | 添加@SpringBootTest(classes = App.class)指定主类 |
| 测试速度极慢 | 不必要地使用@SpringBootTest | 使用切片测试(@WebMvcTest等) |
| "No qualifying bean" | 缺少@MockBean | 为依赖项添加@MockBean |
| 端口已被占用 | 硬编码端口 | 使用webEnvironment = RANDOM_PORT |
| "Could not autowire" | Bean不在测试上下文中 | 检查组件扫描范围或添加@Import |
| 测试不稳定 | 数据库状态未重置 | 使用@Transactional或@DirtiesContext |
Reference
参考资料
- Quick Reference: Annotations
- Spring Boot Testing Guide
- See also: for Testcontainers
spring-boot-integration
- 快速参考:注解
- Spring Boot 测试官方指南
- 另请参阅:使用Testcontainers的集成测试请查看
spring-boot-integration