1. 为什么需要掌握Spring测试框架
刚接触Spring测试时,我常遇到这样的困境:Controller层手动Postman测试、Service层用main方法验证、DAO层直接跑JUnit。这种割裂的测试方式导致每次代码变更都要重复劳动,更可怕的是各层之间的交互逻辑完全无法覆盖。直到某次上线后出现接口连环报错,才让我真正意识到自动化测试的价值。
Spring Test提供了一套完整的测试解决方案:
- 容器环境测试:支持真实Spring容器启动(@SpringBootTest)
- 分层测试:支持Controller层Mock测试(@WebMvcTest)
- 数据访问测试:支持带事务回滚的DAO测试(@DataJpaTest)
- 集成测试:支持RestTemplate调用验证(TestRestTemplate)
2. 测试环境搭建实战
2.1 基础依赖配置
在pom.xml中需要包含以下核心依赖(以Spring Boot 2.7.x为例):
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
注意:排除junit-vintage是为了避免JUnit4和JUnit5冲突,Spring Boot 2.4+默认使用JUnit5
2.2 测试目录结构规范
推荐采用Maven标准结构:
code复制src
├── main
│ └── java
└── test
└── java
├── controllers // Controller测试类
├── services // Service测试类
├── repositories // DAO测试类
└── integration // 集成测试类
3. 分层测试实战详解
3.1 Controller层测试:@WebMvcTest
测试REST接口时最实用的注解组合:
java复制@WebMvcTest(UserController.class)
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Test
void getUserById() throws Exception {
given(userService.findById(1L))
.willReturn(new User(1L, "test"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username").value("test"));
}
}
关键技巧:
- 使用@MockBean替代真实Service
- MockMvc.perform()构建请求
- andExpect()链式断言响应
3.2 Service层测试:@SpringBootTest
对于复杂业务逻辑测试:
java复制@SpringBootTest
@Transactional
class UserServiceTest {
@Autowired
UserService userService;
@Test
void createUserWithDuplicateName() {
userService.createUser("admin");
assertThrows(DuplicateUsernameException.class,
() -> userService.createUser("admin"));
}
}
重要:@Transactional保证每个测试方法执行后自动回滚,避免测试数据污染
3.3 DAO层测试:@DataJpaTest
测试JPA Repository的推荐方式:
java复制@DataJpaTest
class UserRepositoryTest {
@Autowired
TestEntityManager entityManager;
@Autowired
UserRepository userRepository;
@Test
void findByUsername() {
entityManager.persist(new User(null, "test"));
User user = userRepository.findByUsername("test");
assertThat(user).isNotNull();
}
}
4. 高级测试技巧
4.1 测试切片(Slice Test)
Spring Boot提供的测试切片注解:
| 注解 | 用途 | 加载组件 |
|---|---|---|
| @WebMvcTest | MVC控制器测试 | @Controller, @RestController |
| @DataJpaTest | JPA仓库测试 | @Entity, Repository |
| @JsonTest | JSON序列化测试 | Jackson ObjectMapper |
| @RestClientTest | REST客户端测试 | RestTemplateBuilder |
4.2 自定义测试配置
通过@TestConfiguration覆盖特定Bean:
java复制@TestConfiguration
static class MockConfig {
@Bean
@Primary
PaymentService mockPaymentService() {
return mock(PaymentService.class);
}
}
@SpringBootTest
@Import(MockConfig.class)
class OrderServiceTest {
// 测试将使用mock的PaymentService
}
5. 常见问题排查
5.1 上下文缓存问题
症状:多个测试类运行时出现Bean冲突
解决方案:
java复制@SpringBootTest
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD)
class DirtyContextTest {
// 每个测试方法后重建上下文
}
5.2 事务不回滚问题
检查项:
- 确保使用@Transactional
- 确认数据库引擎支持事务(如MyISAM不支持)
- 检查测试方法是否抛出了未捕获异常
5.3 Mock失效问题
典型原因:
- 错误使用@Mock代替@MockBean
- 在@Before中初始化Mock导致覆盖
- 测试类没有加载对应切面(如@WebMvcTest不会加载@Service)
6. 测试代码优化实践
6.1 参数化测试
JUnit5提供的强大功能:
java复制@ParameterizedTest
@CsvSource({
"1, admin",
"2, tester"
})
void testUserExists(Long id, String name) {
given(userRepo.findById(id))
.willReturn(Optional.of(new User(id, name)));
// 测试逻辑
}
6.2 测试容器支持
使用Testcontainers进行集成测试:
java复制@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
class UserRepositoryIT {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:13");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
// 测试方法...
}
7. 测试覆盖率提升策略
7.1 JaCoCo配置示例
在pom.xml中添加:
xml复制<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
执行测试后会在target/site/jacoco目录生成覆盖率报告
7.2 边界测试案例设计
以用户注册为例:
java复制@TestFactory
Stream<DynamicTest> testRegisterUser() {
return Stream.of(
// 正常案例
dynamicTest("valid user", () -> {
User user = new User("valid", "P@ssw0rd");
assertDoesNotThrow(() -> service.register(user));
}),
// 边界案例
dynamicTest("short password", () -> {
User user = new User("test", "123");
assertThrows(InvalidPasswordException.class,
() -> service.register(user));
})
);
}
8. 测试金字塔实践建议
健康的测试比例应该是:
code复制 UI Tests (10%)
/ \
/ \
Service Tests (20%)
\ /
\ /
Unit Tests (70%)
具体到Spring项目:
- 单元测试:直接测试Service/Util类(不用@SpringBootTest)
- 集成测试:@DataJpaTest验证数据库交互
- API测试:@WebMvcTest验证Controller
- E2E测试:@SpringBootTest完整启动应用
9. 持续集成中的测试优化
9.1 测试分组执行
通过JUnit5的@Tag分类:
java复制@Tag("fast")
class FastTests { /* 快速测试 */ }
@Tag("slow")
class SlowTests { /* 耗时测试 */ }
然后在maven-surefire-plugin中配置:
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>fast</groups>
</configuration>
</plugin>
9.2 并行测试配置
在junit-platform.properties中添加:
properties复制junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
10. 实战经验总结
- 测试数据管理:推荐使用@Sql初始化数据而非编程插入
- 耗时测试优化:@MockBean替代真实外部服务调用
- 断言选择:AssertJ比JUnit断言更易读
- 测试命名:应该用should_When格式(如shouldThrowExceptionWhenUsernameExists)
- 测试隔离:每个测试方法必须独立,不能依赖执行顺序
最后分享一个真实案例:某次发版后出现登录性能问题,通过编写@WebMvcTest + MockMvc.perform()的基准测试,我们快速定位到是密码加密算法迭代导致的性能下降。这正是自动化测试价值的完美体现——它不仅是质量保障手段,更是高效的问题诊断工具。