在Spring Boot项目中,测试不是附属品,而是保证代码质量和重构信心的基础。Spring Boot Test作为官方测试模块,整合了JUnit 5、Mockito、AssertJ等常用库,能够以极低的配置成本启动测试上下文。不过很多团队只满足于测试能跑,忽略了切片测试、Mock策略和事务回滚带来的稳定性提升。接下来会从依赖配置开始,逐步拆解Spring Boot Test的关键能力。

一、快速引入Spring Boot Test依赖
Spring Boot官方提供了spring-boot-starter-test这个starter,它一次性聚合了测试所需的常用组件。在Maven项目中,只需在pom.xml的<dependencies>节点内加入以下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这个starter内部包含了JUnit 5、Spring Test、Spring Boot Test、AssertJ、Hamcrest、Mockito、JSONassert和JsonPath等依赖,基本覆盖了单元测试和集成测试的需求。如果使用Gradle,则可以在build.gradle中添加testImplementation 'org.springframework.boot:spring-boot-starter-test'。需要特别注意的是,starter会自动排除JUnit 4的依赖,并强制使用JUnit 5,因此编写测试类时要使用org.junit.jupiter.api.Test而不是旧版的org.junit.Test。
依赖引入后,Spring Boot会自动配置测试相关的基础设施。测试类默认会放在src/test/java目录下,并与主代码保持相同的包结构。这样在测试类中通过@SpringBootTest注解就能直接找到主配置类。如果项目结构比较特殊,也可以使用@ContextConfiguration手动指定配置类,但通常没有必要。
二、理解核心注解与测试切片
Spring Boot Test最常用的入口是@SpringBootTest。这个注解会启动完整的Spring应用上下文,自动扫描所有Bean,尽可能接近生产环境。它的webEnvironment属性可以控制Web环境类型,常用的取值有MOCK(默认,使用MockServletContext)、RANDOM_PORT(启动真实内嵌服务器并随机分配端口)和DEFINED_PORT。当需要测试完整的HTTP链路时,通常把webEnvironment设置为RANDOM_PORT,并结合TestRestTemplate发送请求。
然而,完整上下文启动耗时较长,而且当测试目标只是某一层时,加载所有Bean会引入不必要的依赖。Spring Boot Test为此提供了切片测试注解:@WebMvcTest只加载Controller、ControllerAdvice、Filter等Web层组件,不会加载Service和Repository;@DataJpaTest只加载JPA相关组件,默认使用内嵌数据库并自动开启事务回滚;@JsonTest专注于JSON序列化与反序列化。这些切片测试可以显著缩短测试启动时间,同时让测试意图更加清晰。
下面是一个@WebMvcTest的示例,它只测试UserController,并通过@MockBean隔离UserService:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
given(userService.findById(1L)).willReturn(new User(1L, "Tom"));
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Tom"));
}
}
这里MockMvc不需要启动真实服务器,测试速度快且能精确断言响应状态和JSON内容。但要注意,@WebMvcTest只覆盖了Controller层,Service的真实逻辑并没有被执行,因此还需要配合Service层的单元测试和完整的集成测试。
三、用Mockito编写可靠的单元测试
单元测试的目标是验证单个类的行为,隔离外部依赖。Mockito是Spring Boot Test默认整合的Mock框架,通过@Mock创建虚拟对象,通过@InjectMocks将Mock对象注入到被测试类中。对于Service层来说,通常需要Mock掉Repository或外部API客户端。
比如有一个UserService依赖UserRepository,测试时不需要真实数据库连接。可以这样编写:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldSaveUser() {
User user = new User("Alice");
given(userRepository.save(any(User.class))).willAnswer(invocation -> invocation.getArgument(0));
User saved = userService.createUser("Alice");
assertThat(saved.getName()).isEqualTo("Alice");
verify(userRepository, times(1)).save(any(User.class));
}
}
这里的given...willReturn是Mockito的BDD风格写法,也可以使用when...thenReturn。断言部分使用了AssertJ的assertThat,写法流畅且错误信息可读性高。需要注意的是,any(User.class)中的User类型在泛型擦除后仍然有效,因为Mockito通过参数类型匹配。另外,如果Mock方法的返回值是void,则需要使用willThrow或doThrow来模拟异常。
Mockito还支持验证调用次数、调用顺序、参数捕获等高级特性,但过度使用verify会让测试变得脆弱。建议优先断言方法的输出结果,只有在对副作用(如发送消息、记录日志)有关键要求时才使用verify。
四、集成测试与REST接口验证
当需要验证多个模块协作是否正常时,单元测试就不够了。集成测试会启动完整的Spring上下文,并可能连接真实的数据库或消息队列。Spring Boot Test提供了@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)来启动内嵌服务器,然后注入TestRestTemplate或使用MockMvc进行HTTP调用。
使用TestRestTemplate时,测试代码更像一个外部客户端,能够覆盖Servlet容器、过滤器、拦截器等完整链路。示例如下:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateAndGetUser() {
ResponseEntity<User> createResponse = restTemplate.postForEntity("/users", new User("Bob"), User.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
ResponseEntity<User> getResponse = restTemplate.getForEntity("/users/1", User.class);
assertThat(getResponse.getBody().getName()).isEqualTo("Bob");
}
}
注意ResponseEntity<User>中的尖括号在代码块中已经转义。测试数据如果写入了数据库,默认情况下Spring Test会在测试方法结束后自动回滚事务,不会污染数据。但这个回滚机制要求测试方法使用@Transactional注解,并且数据源支持事务。对于RANDOM_PORT模式,由于HTTP请求在独立线程中执行,事务回滚可能不生效,此时需要手动清理数据或使用@Sql脚本重置。
如果不想启动真实服务器,也可以使用@SpringBootTest配合@AutoConfigureMockMvc,这样同样能测试完整链路但省去网络开销。不过MockMvc不会经过Servlet容器和过滤器,所以涉及安全过滤器链的测试建议使用TestRestTemplate。
五、优化测试数据与断言策略
测试数据的准备方式直接影响测试的可维护性。简单的做法是在每个测试方法前使用@BeforeEach初始化数据,但对于复杂数据集,推荐使用@Sql注解执行SQL脚本。例如在src/test/resources下放置data.sql,然后标注@Sql(scripts = "/data.sql"),这样可以保证每次测试前数据一致。
断言方面,AssertJ提供了丰富的流式API,比如assertThat(user).extracting(User::getName).isEqualTo("Alice"),或者对集合使用containsExactly、hasSize等。相比JUnit自带的assertEquals,AssertJ在比较对象属性时不需要重写toString,失败时输出的差异信息也更直观。
最后,测试覆盖率并不是越高越好,核心业务逻辑和经常出Bug的模块应该优先覆盖。建议在CI流水线中集成JaCoCo生成覆盖率报告,并设定合理的阈值(如行覆盖率80%)。同时,给测试类和方法起清晰的名字,例如shouldReturnUserWhenIdExists,能让测试本身成为文档。Spring Boot Test的整合并不复杂,难的是持续维护测试的稳定性和可读性。

