SpringBoot单元测试实战:JUnit5+Mockito+MockMvc组合应用详解
1. 项目概述与核心价值
在SpringBoot项目里写单元测试,这事儿听起来简单,但真动起手来,不少朋友都会卡在几个地方:Controller层的HTTP请求怎么测?Service层依赖的Dao或者第三方服务怎么隔离?测出来的结果到底靠不靠谱?我自己带团队做项目,最怕的就是代码上线后因为一个边界条件没测到,半夜被报警电话叫醒。所以,今天咱们不聊那些“单元测试很重要”的大道理,直接上干货,手把手带你用JUnit5、MockMvc和Mockito这套当前Java生态里最主流的组合拳,把SpringBoot项目的单元测试写得既扎实又高效。
这套组合的核心价值在于“各司其职,精准打击”。JUnit5是测试框架的骨架,提供了组织、运行测试的基础能力;MockMvc专门用来模拟HTTP请求,测试Controller层的行为,让你不用启动整个Web容器就能验证接口逻辑;而Mockito则是“造假”大师,能轻松创建和操控模拟对象(Mock),把那些难搞的外部依赖(比如数据库、消息队列、第三方API)隔离开,让你能专心测试当前代码的逻辑。把它们仨用好了,你的测试用例就会像手术刀一样精准,覆盖率高,运行速度快,而且维护成本低。接下来,我们就从环境搭建开始,一步步拆解每个工具的核心用法和那些容易踩坑的细节。
2. 环境准备与项目基础配置
2.1 依赖引入与版本选择
第一步,咱们得把“武器”配齐。在SpringBoot项目里,通常我们使用Maven或Gradle来管理依赖。我这里以Maven为例,在 pom.xml 中添加必要的依赖。SpringBoot的 spring-boot-starter-test 起步依赖已经为我们整合了JUnit5、Mockito、AssertJ、Hamcrest等一整套测试工具,是首选。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这里有个关键点: 务必确认你的SpringBoot版本与 spring-boot-starter-test 中集成的JUnit5版本兼容 。Spring Boot 2.2.x及以上版本默认就集成了JUnit5。你可以通过查看依赖树( mvn dependency:tree )来确认。如果因为历史原因项目还在用JUnit4,迁移到JUnit5是值得投入的,因为JUnit5在参数化测试、扩展模型等方面强太多了。
注意 :
spring-boot-starter-test默认会引入一个老版本的Mockito(如3.x)。如果你想使用Mockito 4或5的一些新特性(比如对静态方法、构造方法模拟的更好支持),可以显式地排除旧版本并引入新版本。但要注意,Mockito 5.x需要Java 11+。对于大多数项目,使用starter提供的版本已经足够稳定。
2.2 测试类结构与基础注解
在JUnit5中,测试类的结构非常清晰。一个标准的测试类大概长这样:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
// 关键注解:表明这是一个Spring Boot测试,会加载应用程序上下文。
// 如果只测某个切片(如Web层、数据层),可以用更轻量的注解,后面会讲。
@SpringBootTest
// JUnit5通过扩展机制集成Spring,这个注解是桥梁。
@ExtendWith(SpringExtension.class)
public class MyServiceTest {
@Test
void testSomething() {
// 你的测试逻辑
}
}
这里重点说下 @SpringBootTest 。这个注解功能强大,但代价是它会加载完整的应用上下文,启动所有Bean,速度比较慢。 它更适合做集成测试 。对于纯粹的、隔离的单元测试,我们其实有更轻量级的选择,这也是很多新手容易混淆的地方。比如,如果你只想测试Web层(Controller),可以用 @WebMvcTest ;只想测试数据层(Repository),可以用 @DataJpaTest 。它们只加载相关的上下文切片,速度飞快。我们今天的重点虽然是“单元测试”,但会覆盖从Controller到Service的测试场景,所以会根据情况选择不同的注解。
3. 核心组件深度解析与实战应用
3.1 JUnit5:现代测试框架的基石
JUnit5和JUnit4看起来像,但内核完全不同。它由三个子模块组成:JUnit Platform(在JVM上启动测试的基础服务)、JUnit Jupiter(新的编程模型和扩展模型)、JUnit Vintage(用于兼容运行JUnit4/3的测试)。我们日常编码主要和JUnit Jupiter打交道。
几个必知的核心注解:
@Test:标记一个方法是测试方法。和JUnit4的@Test不同,它来自org.junit.jupiter.api.Test包。@BeforeEach/@AfterEach:在每个@Test方法 之前/之后 执行。用来做测试数据的准备和清理,替代JUnit4的@Before/@After。@BeforeAll/@AfterAll:在所有@Test方法 之前/之后 执行 一次 。方法必须是static的。适合做耗时的全局初始化,比如建临时数据库。@DisplayName:为测试类或方法设置一个易读的名称,会在测试报告中显示,非常有用。@Disabled:临时禁用某个测试,相当于JUnit4的@Ignore。
一个容易被忽略但极其强大的功能:参数化测试。 它能用不同的输入数据多次运行同一个测试逻辑,完美测试边界条件。比如测试一个计算器除法方法,你需要测试正数、负数、除零等情况。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ParameterizedTestExample {
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15})
void testIsOdd(int number) {
assertTrue(number % 2 != 0);
}
}
除了 @ValueSource ,还有 @CsvSource (用CSV格式提供多参数)、 @MethodSource (从一个工厂方法获取流式数据)等,功能非常灵活。
3.2 Mockito:依赖隔离的艺术
单元测试的核心原则之一是“隔离”。你的Service方法依赖了Dao,测试Service时,我们不应该真的去连数据库,因为那会引入不确定性(网络、数据状态)和慢速。Mockito的作用就是创建一个Dao的“替身”(Mock对象),并规定好这个替身在各种情况下的行为(当调用A方法时,返回B结果)。
基本使用三步曲:
- 创建Mock对象 :
MyDao daoMock = Mockito.mock(MyDao.class); - 打桩(Stubbing) :定义Mock对象的行为。
when(daoMock.findById(1L)).thenReturn(new User(“张三”)); - 将Mock对象注入被测试类 :通常通过构造器或Setter注入。在Spring测试中,更常用
@MockBean注解(后面会讲)。
进阶技巧:验证交互。 有时候,我们不仅关心方法返回什么,还关心方法是否被调用了、调用了多少次、以什么参数调用的。这就是Mockito的验证(Verification)功能。
@Test
void testUserUpdate() {
UserService userService = new UserService(userDaoMock, emailServiceMock);
User user = new User(1L, “李四”);
userService.updateUserName(1L, “李四”);
// 验证 userDaoMock 的 save 方法被调用了一次,并且参数是 user 对象
verify(userDaoMock, times(1)).save(user);
// 验证 emailServiceMock 的 sendEmail 方法从未被调用
verify(emailServiceMock, never()).sendEmail(any());
}
实操心得:关于 @Mock 和 @MockBean 的选择。
@Mock:是纯Mockito的注解,需要在测试类上添加@ExtendWith(MockitoExtension.class)才能生效。它创建一个普通的Mockito mock对象, Spring容器不知道它的存在 。@MockBean:是Spring Boot Test提供的注解。它会在Spring的测试应用上下文中注册一个Mockito mock对象,并替换掉上下文中同类型的Bean。 当你测试的类是通过@Autowired从Spring容器注入依赖时,必须用@MockBean。
简单记:如果你用new手动创建被测试对象,用 @Mock + @ExtendWith(MockitoExtension.class) ,更轻量。如果你测试Spring管理的Bean(比如加了 @Service 的类),用 @MockBean + @SpringBootTest 或其它切片测试注解。
3.3 MockMvc:Web层的精准测试工具
Controller层测试的传统方式是启动一个内嵌的Servlet容器(如Tomcat),然后发真实的HTTP请求。这太慢了,而且是集成测试。MockMvc可以模拟Servlet容器,直接调用Controller的方法,并检查返回的模型(Model)、视图(View)或JSON响应。
如何初始化MockMvc? 有两种主流方式,推荐第一种:
-
@WebMvcTest+@AutoConfigureMockMvc(推荐) :@WebMvcTest是一个切片测试注解,它只加载Web层相关的Bean(Controller, ControllerAdvice, Filter等),不会加载Service、Repository。速度极快。它会自动配置一个MockMvc实例。@WebMvcTest(UserController.class) // 只加载UserController这一个Controller @AutoConfigureMockMvc(addFilters = false) // 自动配置MockMvc,addFilters=false可禁用安全过滤器方便测试 class UserControllerTest { @Autowired private MockMvc mockMvc; @MockBean // 因为Controller依赖UserService,我们需要Mock它 private UserService userService; @Test void getUserById() throws Exception { // 打桩 when(userService.getUserById(1L)).thenReturn(new User(1L, “测试用户”)); // 执行请求并断言 mockMvc.perform(get(“/api/users/1”) // 模拟GET请求 .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) // 断言HTTP状态码是200 .andExpect(jsonPath(“$.name”).value(“测试用户”)); // 断言JSON响应体中的name字段 } } -
在
@SpringBootTest中手动构建 :如果你用了@SpringBootTest(加载了完整上下文),可以手动注入一个WebApplicationContext来构建MockMvc。这种方式更重,通常在你需要测试Web层与其它层(如安全)的集成时才用。
MockMvc的核心API:
mockMvc.perform():构建一个请求。里面可以链式调用.contentType()、.content()(传请求体JSON)、.param()(传表单参数)、.header()等方法。.andExpect():对结果进行断言。这是最常用的部分。status().isOk():状态码200。jsonPath(“$.field”).value(“expected”):使用JsonPath断言JSON响应。content().string(“expected”):断言响应内容字符串。view().name(“home”):断言视图名。
.andDo():执行一些额外的操作,比如打印请求/响应详情到控制台(MockMvcResultHandlers.print()),这在调试时非常有用。
4. 分层单元测试实战演练
光说不练假把式,我们用一个经典的“用户管理”模块来串联所有知识点。假设我们有 UserController 、 UserService 和 UserRepository 三层。
4.1 Service层单元测试:Mockito的主场
Service层包含核心业务逻辑,是单元测试的重点。我们的目标是隔离数据库(Repository)和任何外部服务。
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
// 使用Mockito扩展,这样@Mock和@InjectMocks才能生效
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepositoryMock; // 模拟Repository
@Mock
private EmailService emailServiceMock; // 模拟一个外部邮件服务
@InjectMocks
private UserService userService; // Mockito会自动将上面的mock注入到这里
@Test
void getUserById_Success() {
// 1. 准备数据 & 打桩
Long userId = 1L;
User expectedUser = new User(userId, “张三”, “zhangsan@example.com”);
when(userRepositoryMock.findById(userId)).thenReturn(Optional.of(expectedUser));
// 2. 执行测试方法
User actualUser = userService.getUserById(userId);
// 3. 断言结果
assertNotNull(actualUser);
assertEquals(“张三”, actualUser.getName());
assertEquals(userId, actualUser.getId());
// 4. 验证交互(可选)
verify(userRepositoryMock, times(1)).findById(userId);
verify(emailServiceMock, never()).sendEmail(any()); // 这个方法不应该被调用
}
@Test
void getUserById_NotFound() {
Long userId = 999L;
when(userRepositoryMock.findById(userId)).thenReturn(Optional.empty());
// 断言会抛出特定的异常
assertThrows(UserNotFoundException.class, () -> {
userService.getUserById(userId);
});
verify(userRepositoryMock).findById(userId);
}
@Test
void createUser_Success() {
UserDto userDto = new UserDto(“李四”, “lisi@example.com”);
User savedUser = new User(1L, userDto.getName(), userDto.getEmail());
// 模拟保存操作,any(User.class)表示匹配任何User对象参数
when(userRepositoryMock.save(any(User.class))).thenReturn(savedUser);
// 模拟发送邮件成功
doNothing().when(emailServiceMock).sendWelcomeEmail(anyString());
User result = userService.createUser(userDto);
assertNotNull(result.getId());
assertEquals(userDto.getName(), result.getName());
// 验证保存和发送邮件都被调用了
verify(userRepositoryMock).save(any(User.class));
verify(emailServiceMock).sendWelcomeEmail(userDto.getEmail());
}
}
关键点解析:
@InjectMocks:它会尝试用构造器注入、setter注入或字段注入的方式,将本类中@Mock(或@Spy)注解创建的mock对象,注入到被标注的实例中。非常方便。any():这是一个参数匹配器(Argument Matcher),表示“任何此类型的参数”。当你不太关心调用时传入的具体对象,只关心方法被调用这个事实时,就用它。类似的还有eq()(等于某个值)、anyString()等。doNothing().when(mock).method():用于对返回类型是void的方法进行打桩。when(mock.method()).thenReturn(...)的语法对void方法不适用。
4.2 Controller层单元测试:MockMvc的舞台
Controller层测试关注HTTP协议层面的输入输出是否正确,业务逻辑依赖的Service层我们用MockBean隔离。
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(UserController.class) // 关键:只加载Web层和UserController
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper; // Spring Boot会自动配置,用于JSON序列化/反序列化
@MockBean
private UserService userService;
@Test
void getAllUsers_ShouldReturnUserList() throws Exception {
// 准备模拟数据
List<User> userList = Arrays.asList(
new User(1L, “用户一”, “user1@test.com”),
new User(2L, “用户二”, “user2@test.com”)
);
when(userService.getAllUsers()).thenReturn(userList);
mockMvc.perform(get(“/api/users”)
.accept(MediaType.APPLICATION_JSON))
.andDo(print()) // 打印请求响应详情,调试神器
.andExpect(status().isOk())
.andExpect(jsonPath(“$”, hasSize(2))) // 断言返回的JSON数组长度为2
.andExpect(jsonPath(“$[0].name”).value(“用户一”))
.andExpect(jsonPath(“$[1].email”).value(“user2@test.com”));
}
@Test
void createUser_WithValidInput_ShouldReturnCreated() throws Exception {
UserDto userDto = new UserDto(“新用户”, “new@test.com”);
User createdUser = new User(10L, userDto.getName(), userDto.getEmail());
when(userService.createUser(any(UserDto.class))).thenReturn(createdUser);
// 将对象转换为JSON字符串作为请求体
String requestBody = objectMapper.writeValueAsString(userDto);
mockMvc.perform(post(“/api/users”)
.contentType(MediaType.APPLICATION_JSON) // 声明请求体是JSON
.content(requestBody)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated()) // 断言HTTP 201 Created
.andExpect(header().string(“Location”, “/api/users/10”)) // 断言Location头
.andExpect(jsonPath(“$.id”).value(10));
}
@Test
void createUser_WithInvalidInput_ShouldReturnBadRequest() throws Exception {
// 测试一个name为空的无效请求
UserDto invalidUserDto = new UserDto(“”, “invalid-email”);
String requestBody = objectMapper.writeValueAsString(invalidUserDto);
mockMvc.perform(post(“/api/users”)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isBadRequest()); // 假设Controller用了@Valid,会返回400
// 可以进一步断言返回的错误信息结构
}
}
注意事项:
@WebMvcTest默认会启用Spring Security。如果你的Controller有安全限制,测试时需要配置用户凭证,或者像上面例子一样在@AutoConfigureMockMvc里设置addFilters = false来禁用安全过滤器链(仅用于测试)。ObjectMapper是Jackson库的核心,用来处理JSON。Spring Boot自动配置了一个,在测试中直接@Autowired注入使用即可。jsonPath语法非常强大,类似于XPath for JSON。$表示根节点,$[0]表示数组第一个元素,$.field表示字段。
4.3 Repository层测试:与真实数据库的轻量集成
严格来说,Repository(或Dao)层的测试属于集成测试范畴,因为它需要和真实的数据库交互。但Spring Boot提供了 @DataJpaTest 注解来优雅地处理这个问题。
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import javax.persistence.EntityManager;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest // 关键:只加载JPA相关的配置和Bean,使用一个内存数据库(如H2)
class UserRepositoryTest {
@Autowired
private TestEntityManager testEntityManager; // 用于测试的EntityManager,方便操作
@Autowired
private UserRepository userRepository; // 这是我们要测试的真实Repository
@Test
void findByName_ShouldReturnUser() {
// 使用TestEntityManager准备数据,不经过Repository
User savedUser = testEntityManager.persistFlushFind(new User(null, “王五”, “wangwu@test.com”));
// 调用真实的Repository方法
User foundUser = userRepository.findByName(“王五”).orElse(null);
// 断言
assertThat(foundUser).isNotNull();
assertThat(foundUser.getEmail()).isEqualTo(savedUser.getEmail());
assertThat(foundUser.getId()).isEqualTo(savedUser.getId());
}
@Test
void findByEmail_WhenNotExist_ShouldReturnEmpty() {
Optional<User> found = userRepository.findByEmail(“nonexist@test.com”);
assertThat(found).isEmpty(); // 使用AssertJ的流式断言,可读性更好
}
}
核心要点:
@DataJpaTest会自动配置一个内存数据库(默认是H2),并只初始化@Entity类和Spring Data JPA的Repository。它 不会 加载@Service、@Controller等其他Bean,所以速度很快。- 它默认在每个测试方法后回滚事务,确保测试之间数据隔离。
TestEntityManager是专门为测试设计的,提供了persistFlushFind这类便捷方法,能立即持久化并刷新,然后查找,确保数据已写入“数据库”(内存中)。- 对于Repository层的测试,我们主要验证自定义的查询方法(如
findByName)是否编写正确,以及JPA的映射关系是否正确。基础的save、findById等方法由Spring Data JPA提供,通常不需要测试。
5. 高级技巧与最佳实践
5.1 测试代码的组织与命名
混乱的测试代码比没有测试更可怕。好的命名和组织能让测试意图一目了然。
命名约定: 一个流行的模式是 [MethodUnderTest]_[Scenario]_[ExpectedBehavior] 。例如:
getUserById_WithValidId_ReturnsUsercreateUser_WithDuplicateEmail_ThrowsExceptionupdateUser_WhenUserNotFound_ThrowsNotFoundException
测试类结构: 使用 @Nested 注解对测试进行逻辑分组,让测试报告更清晰。
@DisplayName(“UserService 测试套件”)
class UserServiceTest {
@Nested
@DisplayName(“当查询用户时”)
class WhenGettingUser {
@Test
@DisplayName(“给定有效ID,应返回用户”)
void withValidId_shouldReturnUser() { ... }
@Test
@DisplayName(“给定无效ID,应抛出异常”)
void withInvalidId_shouldThrowException() { ... }
}
@Nested
@DisplayName(“当创建用户时”)
class WhenCreatingUser {
// ... 相关测试
}
}
5.2 静态方法、构造方法及final类的模拟
有时候,你不得不面对一些遗留代码,里面调用了静态工具类(如 DateUtils.format() )或第三方库的final类。早期的Mockito(3.x之前)对此无能为力。但现在,通过 mockito-inline 模块,可以模拟静态方法、构造方法甚至final类。
步骤:
- 引入依赖(如果starter里的Mockito版本够新,可能已包含):
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <scope>test</scope> </dependency> - 在测试类上使用
@ExtendWith(MockitoExtension.class)。 - 使用
Mockito.mockStatic()来模拟静态方法。
import org.mockito.MockedStatic;
import static org.mockito.Mockito.*;
@Test
void testWithStaticMethod() {
// 模拟一个静态工具类
try (MockedStatic<MyStaticUtils> mockedStatic = mockStatic(MyStaticUtils.class)) {
// 打桩静态方法
mockedStatic.when(() -> MyStaticUtils.generateId()).thenReturn(“fixed-id-123”);
// 调用被测代码,其内部会调用MyStaticUtils.generateId()
String result = myService.doSomething();
assertEquals(“result-with-fixed-id-123”, result);
// 可以验证静态方法是否被调用
mockedStatic.verify(() -> MyStaticUtils.generateId());
} // try-with-resources 块结束,静态模拟会自动关闭
}
重要提醒 :模拟静态方法或构造器是“终极武器”,意味着你的代码设计可能出现了问题(过度耦合、职责不清)。在编写新代码时,应优先考虑通过依赖注入来传递这些依赖,让代码更可测。这只在改造难以变动的旧代码时使用。
5.3 测试覆盖率与持续集成
写测试不是为了自我感动,而是为了保障质量。我们需要工具来量化测试效果。
Jacoco(Java Code Coverage) 是最流行的代码覆盖率工具之一。它可以和Maven/Gradle以及CI/CD工具(如Jenkins, GitLab CI)无缝集成。
Maven配置示例:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.10</version> <!-- 使用最新版本 -->
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- 可选的:设置覆盖率检查规则,在CI中失败构建 -->
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum> <!-- 要求行覆盖率至少80% -->
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
运行 mvn clean test 后,Jacoco会生成报告,通常位于 target/site/jacoco/index.html ,用浏览器打开可以看到详细的覆盖率分析(行覆盖、分支覆盖等)。
最佳实践建议:
- 不要盲目追求100%覆盖率 。覆盖率达到70%-90%通常已经很好。重点覆盖核心业务逻辑、复杂分支和边界条件。
- 将测试作为CI/CD流水线的必过环节 。每次代码提交或合并请求,都必须通过所有测试,并且覆盖率不能低于预设阈值。
- 区分单元测试和集成测试 。单元测试要快(秒级),可以频繁运行。集成测试、端到端测试可以放在单独的Maven profile或CI的不同阶段运行。
6. 常见问题排查与调试技巧
即使按照最佳实践来,写测试时还是会遇到各种奇怪的问题。这里记录几个我踩过的坑和解决方法。
问题1: @MockBean 注入失败,测试运行时提示 NoSuchBeanDefinitionException 。
- 可能原因 :你用了
@WebMvcTest(SomeController.class),但你的Controller依赖了一个没有被@MockBean模拟的Service,而这个Service又依赖了其他Repository或Component。因为@WebMvcTest只加载Web层相关的Bean,这些下游依赖不会被加载,导致创建Controller Bean失败。 - 解决方案 :确保Controller所有直接依赖的Bean都被
@MockBean模拟了。或者,如果这个测试确实需要更完整的上下文,考虑使用@SpringBootTest配合@AutoConfigureMockMvc,但要做好速度变慢的心理准备。
问题2:Mockito打桩不生效,实际调用了真实方法。
- 可能原因1 :你mock的对象并不是被测试对象实际使用的那个。在使用
@InjectMocks时,确保被测试类是通过构造器或setter注入依赖的。如果被测试类内部用new关键字创建了依赖对象,Mockito是无法注入mock的。 - 可能原因2 :你在对final方法、静态方法或private方法打桩。默认情况下Mockito不能mock这些。对于final类和final方法,需要添加
mockito-inline依赖。 - 排查技巧 :在测试中加一行
verify(mockObj, times(1)).someMethod(...);。如果验证失败,说明mock对象根本没被调用,问题出在依赖注入或对象创建上。
问题3:使用 @DataJpaTest 时,找不到自定义的查询方法(如 findByName )。
- 可能原因 :你的查询方法名可能不符合Spring Data JPA的命名约定,或者JPQL/HQL写错了。
- 解决方案 :首先检查方法名是否遵循“findBy…”、“readBy…”、“queryBy…”、“countBy…”等前缀。如果是
@Query注解的JPQL,在测试环境下运行一下,看控制台是否打印了SQL创建语句,检查生成的SQL是否正确。一个很实用的调试技巧是在application-test.properties中开启SQL日志:spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true logging.level.org.hibernate.SQL=DEBUG logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
问题4:MockMvc测试时,请求体中的日期(LocalDateTime)序列化/反序列化出错。
- 场景 :你的
UserDto里有一个LocalDateTime createTime字段,测试时发现Jackson无法将JSON字符串反序列化成对象。 - 解决方案 :确保你的项目中配置了Jackson对Java 8时间类型的支持。通常Spring Boot会自动配置,但如果出现问题,可以检查是否引入了
jackson-datatype-jsr310依赖,并且在ObjectMapper中注册了JavaTimeModule。在测试中,你可以直接使用Spring自动注入的ObjectMapper,它通常是配置好的。
问题5:测试本身通过,但一运行就报 java.lang.OutOfMemoryError: Metaspace 或 PermGen space。
- 可能原因 :每个
@SpringBootTest或@WebMvcTest注解的测试类,Spring都会为其创建一个独立的应用程序上下文(ApplicationContext)。如果测试类很多,且没有重用上下文,会导致元空间(Metaspace)被大量加载的类占满。 - 解决方案 :Spring Test框架本身支持上下文缓存。确保你的测试类结构相似(使用相同的配置)。你可以通过
@DirtiesContext注解来控制上下文是否需要清理后重建。对于大多数情况,Spring能智能地缓存和重用上下文。如果问题严重,可以考虑将一些重量级的集成测试拆分到单独的模块或使用@TestPropertySource来让测试共享更相似的配置。
写单元测试是一个需要持续练习和反思的技能。一开始可能会觉得繁琐,但当你发现它帮你提前拦截了数个线上bug,或者在重构代码时给你十足的信心,你就会觉得所有投入都是值得的。从今天开始,尝试为你正在开发的新功能先写上测试,你会发现你的代码设计也会在不自觉中变得更好——因为可测试的代码,往往是松耦合、职责清晰的代码。
更多推荐



所有评论(0)