测试

返回 Spring Boot 基础

Spring Boot 通过 spring-boot-starter-test 集成 JUnit 5、Mockito、AssertJ、MockMvc、Testcontainers 等主流测试库,提供分层测试能力。


测试分层策略

┌──────────────────────────────────────────────┐
│  E2E / 集成测试  @SpringBootTest             │  启动完整上下文
│  ├─ MockMvc 测试(Web 层)                   │
│  ├─ @DataJpaTest(Repository 层)            │  启动部分上下文
│  └─ @WebMvcTest(Controller 层)             │
├──────────────────────────────────────────────┤
│  单元测试  (无 Spring Context)               │  最快,纯 JUnit + Mockito
└──────────────────────────────────────────────┘
注解加载范围适用场景
@SpringBootTest完整 ApplicationContext集成测试、端到端流程
@WebMvcTest仅 Web 层(Controller/Filter/Advice)Controller 单测
@DataJpaTest仅 JPA 相关(内存库)Repository 单测
@DataRedisTest仅 Redis 相关Redis 操作单测
@RestClientTest仅 RestTemplate/WebClientHTTP 客户端单测
无注解无 Spring Context纯业务逻辑单测

单元测试(Service 层)

// 无需 Spring Context,启动最快
class UserServiceTest {
 
    @InjectMocks
    private UserService userService;
 
    @Mock
    private UserRepository userRepo;
 
    @Mock
    private PasswordEncoder passwordEncoder;
 
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
 
    @Test
    void createUser_whenUsernameExists_shouldThrow() {
        // Arrange
        given(userRepo.existsByUsername("alice")).willReturn(true);
 
        // Act & Assert
        assertThatThrownBy(() -> userService.create("alice", "pass"))
            .isInstanceOf(DuplicateUsernameException.class);
    }
 
    @Test
    void createUser_shouldEncodePassword() {
        // Arrange
        given(userRepo.existsByUsername("alice")).willReturn(false);
        given(passwordEncoder.encode("pass")).willReturn("hashed");
        given(userRepo.save(any())).willAnswer(inv -> inv.getArgument(0));
 
        // Act
        User user = userService.create("alice", "pass");
 
        // Assert
        assertThat(user.getPassword()).isEqualTo("hashed");
        then(userRepo).should().save(any(User.class));
    }
}

Controller 测试(@WebMvcTest)

只加载 Web 层,Service 层需 Mock:

@WebMvcTest(UserController.class)
class UserControllerTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @MockBean                               // 注入 Mock 到 Spring 上下文
    private UserService userService;
 
    @Autowired
    private ObjectMapper objectMapper;
 
    @Test
    void getUser_whenExists_shouldReturn200() throws Exception {
        // Arrange
        given(userService.findById(1L))
            .willReturn(new UserResponse(1L, "alice", "alice@example.com"));
 
        // Act & Assert
        mockMvc.perform(get("/api/users/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.username").value("alice"))
            .andExpect(jsonPath("$.email").value("alice@example.com"));
    }
 
    @Test
    void createUser_withInvalidBody_shouldReturn400() throws Exception {
        var req = new CreateUserRequest("", "short", "bad-email");
 
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(req)))
            .andExpect(status().isBadRequest());
    }
}

Repository 测试(@DataJpaTest)

自动使用内嵌数据库(H2),每个测试后回滚:

@DataJpaTest
class UserRepositoryTest {
 
    @Autowired
    private UserRepository userRepo;
 
    @Test
    void findByUsername_whenExists_shouldReturn() {
        // Arrange
        userRepo.save(new User("alice", "hashed", "alice@example.com"));
 
        // Act
        Optional<User> result = userRepo.findByUsername("alice");
 
        // Assert
        assertThat(result).isPresent();
        assertThat(result.get().getEmail()).isEqualTo("alice@example.com");
    }
 
    @Test
    void existsByUsername_whenNotExists_shouldReturnFalse() {
        assertThat(userRepo.existsByUsername("nobody")).isFalse();
    }
}

若需使用真实数据库(MySQL 等),配合 Testcontainers(见下文)替换内嵌库。


集成测试(@SpringBootTest)

启动完整上下文,可选择 WebEnvironment

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class UserIntegrationTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Autowired
    private UserRepository userRepo;
 
    @BeforeEach
    void setUp() {
        userRepo.deleteAll();
    }
 
    @Test
    @Sql("/sql/init-users.sql")             // 执行 SQL 脚本初始化数据
    void fullFlow_createAndGetUser() throws Exception {
        var req = new CreateUserRequest("bob", "password123", "bob@example.com");
 
        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(req)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.id").isNumber());
    }
}

Testcontainers(真实中间件)

@SpringBootTest
@Testcontainers
class RedisIntegrationTest {
 
    @Container
    static RedisContainer redis = new RedisContainer(
        DockerImageName.parse("redis:7-alpine"));
 
    @DynamicPropertySource
    static void redisProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", redis::getFirstMappedPort);
    }
 
    @Autowired
    private StringRedisTemplate redisTemplate;
 
    @Test
    void setAndGet() {
        redisTemplate.opsForValue().set("key", "value");
        assertThat(redisTemplate.opsForValue().get("key")).isEqualTo("value");
    }
}

常用容器:

容器类依赖
MySQLContainertestcontainers:mysql
PostgreSQLContainertestcontainers:postgresql
RedisContainertestcontainers:redis
KafkaContainertestcontainers:kafka
MongoDBContainertestcontainers:mongodb

安全相关测试

@WebMvcTest(UserController.class)
class UserControllerSecurityTest {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void protectedEndpoint_withoutAuth_shouldReturn401() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isUnauthorized());
    }
 
    @Test
    @WithMockUser(roles = "ADMIN")          // 模拟已登录用户
    void adminEndpoint_withAdminRole_shouldReturn200() throws Exception {
        mockMvc.perform(get("/api/admin/stats"))
            .andExpect(status().isOk());
    }
 
    @Test
    @WithMockUser(roles = "USER")
    void adminEndpoint_withUserRole_shouldReturn403() throws Exception {
        mockMvc.perform(get("/api/admin/stats"))
            .andExpect(status().isForbidden());
    }
}

测试配置

# src/test/resources/application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb;MODE=MySQL
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
 
logging:
  level:
    root: WARN
    com.example: DEBUG
// 激活测试 Profile
@SpringBootTest
@ActiveProfiles("test")
class MyTest { ... }

常用断言速查

// AssertJ(推荐)
assertThat(result).isNotNull();
assertThat(list).hasSize(3).contains("a");
assertThat(value).isEqualTo(42);
assertThatThrownBy(() -> service.call()).isInstanceOf(IllegalArgumentException.class);
 
// MockMvc 响应断言
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.total").value(10))
.andExpect(jsonPath("$.items[0].name").value("foo"))
.andDo(print());                            // 打印完整请求/响应(调试用)

相关链接