测试
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/WebClient | HTTP 客户端单测 |
| 无注解 | 无 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");
}
}常用容器:
| 容器类 | 依赖 |
|---|---|
MySQLContainer | testcontainers:mysql |
PostgreSQLContainer | testcontainers:postgresql |
RedisContainer | testcontainers:redis |
KafkaContainer | testcontainers:kafka |
MongoDBContainer | testcontainers: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()); // 打印完整请求/响应(调试用)相关链接
- Spring Boot 基础 — 项目结构与 Starter
- MVC — Controller 层机制
- 数据访问 — Repository 层背景
- JPA 与 Hibernate —
@DataJpaTest背景 - 安全 —
@WithMockUser安全测试 - 参数校验 — 校验异常测试