SpringBoot单元测试实战:JUnit5+Mockito+MockMvc组合应用详解
2026/7/15 10:39:40 网站建设 项目流程

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结果)。

基本使用三步曲:

  1. 创建Mock对象MyDao daoMock = Mockito.mock(MyDao.class);
  2. 打桩(Stubbing):定义Mock对象的行为。when(daoMock.findById(1L)).thenReturn(new User(“张三”));
  3. 将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?有两种主流方式,推荐第一种:

  1. @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字段 } }
  2. @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. 分层单元测试实战演练

光说不练假把式,我们用一个经典的“用户管理”模块来串联所有知识点。假设我们有UserControllerUserServiceUserRepository三层。

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的映射关系是否正确。基础的savefindById等方法由Spring Data JPA提供,通常不需要测试。

5. 高级技巧与最佳实践

5.1 测试代码的组织与命名

混乱的测试代码比没有测试更可怕。好的命名和组织能让测试意图一目了然。

命名约定:一个流行的模式是[MethodUnderTest]_[Scenario]_[ExpectedBehavior]。例如:

  • getUserById_WithValidId_ReturnsUser
  • createUser_WithDuplicateEmail_ThrowsException
  • updateUser_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类。

步骤:

  1. 引入依赖(如果starter里的Mockito版本够新,可能已包含):
    <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <scope>test</scope> </dependency>
  2. 在测试类上使用@ExtendWith(MockitoExtension.class)
  3. 使用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,或者在重构代码时给你十足的信心,你就会觉得所有投入都是值得的。从今天开始,尝试为你正在开发的新功能先写上测试,你会发现你的代码设计也会在不自觉中变得更好——因为可测试的代码,往往是松耦合、职责清晰的代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询