spring-boot-test

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Spring Boot Testing

Spring Boot 测试

Deep Knowledge: Use
mcp__documentation__fetch_docs
with technology:
spring-boot-test
for comprehensive documentation.
深度参考:使用
mcp__documentation__fetch_docs
并指定技术栈为
spring-boot-test
以获取完整文档。

When NOT to Use This Skill

不适用本技能的场景

  • Pure Unit Tests - Use
    junit
    with Mockito for faster tests without Spring context
  • Integration Tests with Real Database - Use
    spring-boot-integration
    with Testcontainers
  • REST API Client Testing - Use
    rest-assured
    for HTTP testing
  • E2E Web Testing - Use Selenium or Playwright
  • Microservice Contract Testing - Use Spring Cloud Contract
  • 纯单元测试 - 使用
    junit
    搭配Mockito执行无需Spring上下文的快速测试
  • 使用真实数据库的集成测试 - 使用
    spring-boot-integration
    搭配Testcontainers
  • REST API客户端测试 - 使用
    rest-assured
    进行HTTP测试
  • 端到端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)

切片测试(更快速、聚焦)

AnnotationLayerAuto-configured
@WebMvcTest
ControllersMockMvc, Jackson
@DataJpaTest
JPA RepositoriesTestEntityManager, DataSource
@DataMongoTest
MongoDBMongoTemplate
@JsonTest
JSON serializationJacksonTester
@RestClientTest
REST clientsMockRestServiceServer
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"));
    }
}
注解层级自动配置内容
@WebMvcTest
控制器层MockMvc、Jackson
@DataJpaTest
JPA仓库层TestEntityManager、DataSource
@DataMongoTest
MongoDB层MongoTemplate
@JsonTest
JSON序列化层JacksonTester
@RestClientTest
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-PatternWhy It's BadSolution
Using @SpringBootTest for all testsExtremely slowUse slice tests (@WebMvcTest, @DataJpaTest)
Not using @MockBeanTesting real beansMock external dependencies
Hardcoding ports in testsPort conflictsUse @LocalServerPort with RANDOM_PORT
Testing private methodsCoupled to implementationTest through controller/service API
Not isolating test dataTests interfereUse @Transactional or cleanup in @AfterEach
Ignoring @Sql scriptsManual setup duplicationUse @Sql for test data setup
No test profilesPolluting dev/prod configUse @ActiveProfiles("test")
反模式问题所在解决方案
所有测试都使用@SpringBootTest测试速度极慢使用切片测试(@WebMvcTest、@DataJpaTest等)
不使用@MockBean测试真实外部依赖Bean模拟外部依赖
测试中硬编码端口端口冲突使用@LocalServerPort搭配RANDOM_PORT
测试私有方法与实现细节耦合通过控制器/服务API进行测试
不隔离测试数据测试间相互干扰使用@Transactional或在@AfterEach中清理数据
忽略@Sql脚本手动重复设置测试数据使用@Sql加载测试数据
不使用测试配置文件污染开发/生产环境配置使用@ActiveProfiles("test")

Quick Troubleshooting

快速故障排除

ProblemLikely CauseSolution
"Unable to find @SpringBootConfiguration"Main class not foundAdd @SpringBootTest(classes = App.class)
Test very slowUsing @SpringBootTest unnecessarilyUse slice tests (@WebMvcTest, etc.)
"No qualifying bean"Missing @MockBeanAdd @MockBean for dependencies
Port already in useHardcoded portUse webEnvironment = RANDOM_PORT
"Could not autowire"Bean not in test contextCheck component scan or add @Import
Flaky testDatabase state not resetUse @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

参考资料