第一部分 junit与springboot的前世今生
一、junit4与junit5及springboot中的使用
在现代软件开发中,单元测试是确保代码质量的重要环节。Spring Boot框架通过整合JUnit,为开发者提供了便捷的单元测试支持。
1.1 Spring Boot中JUnit版本的变化
在Spring Boot 2.0之前,框架默认使用JUnit 4作为测试平台。然而,从Spring Boot 2.0开始,JUnit 5成为默认的测试框架。以下是Spring Boot不同版本中JUnit版本的对比:
| Spring Boot版本 | 默认JUnit版本 |
|---|---|
| 1.x | JUnit 4 |
| 2.x | JUnit 5 |
例如,Spring Boot 2.2.0使用JUnit 5.5.2版本。开发者可以通过POM文件确认具体版本。
1.2 POM文件配置
在Spring Boot项目中,单元测试的依赖通过spring-boot-starter-test启动器引入。以下是POM文件的配置示例:
<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>
1.3 排除JUnit Vintage Engine
junit-vintage-engine是JUnit 3和JUnit 4的运行支持平台。默认情况下,Spring Boot测试启动器会排除该依赖,以鼓励开发者使用JUnit 5。如果需要使用JUnit 4,可以移除<exclusions>标签。
二、JUnit 4与JUnit 5的对比
以下是JUnit 4和JUnit 5的主要差异:
| 特性 | JUnit 4 | JUnit 5 |
|---|---|---|
| 注解 | @RunWith、@Test | @SpringBootTest、@Test |
| 默认启动类支持 | 需要手动指定启动类 | 自动检测启动类 |
| 测试方法支持 | 需要@Test注解 | 需要@Test注解 |
| 扩展支持 | 有限 | 更强大的扩展机制 |
三、JUnit 4测试代码示例
以下是一个基于JUnit 4的测试代码示例:
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.junit.Test;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootExceptionAndJourneyApplication.class)
public class UserServiceTest {
@Test
public void testAddUser() {
System.out.println("JUnit 4 测试方法运行成功!");
}
}
四、JUnit 5测试代码示例
以下是一个基于JUnit 5的测试代码示例:
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
@SpringBootTest
public class UserServiceTest {
@Test
public void testAddUser() {
System.out.println("JUnit 5 测试方法运行成功!");
}
}
五、实际案例:持久层与业务层测试
假设我们有一个UserDAO和UserService,以下是它们的实现代码:
5.1 持久层代码
public class UserDAOImpl {
public void insert() {
System.out.println("INSERT INTO USER VALUES(...)");
}
}
5.2 业务层代码
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl {
private final UserDAOImpl userDAO;
public UserServiceImpl(UserDAOImpl userDAO) {
this.userDAO = userDAO;
}
public void addUser() {
userDAO.insert();
}
}
5.3 测试代码
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserServiceImpl userService;
@Test
public void testAddUser() {
userService.addUser();
}
}
六、常见问题与解答
以下是关于Spring Boot整合JUnit单元测试的常见问题:
| 问题 | 答案 |
|---|---|
| 1. Spring Boot默认使用哪个JUnit版本? | Spring Boot 2.x默认使用JUnit 5。 |
| 2. 如何在Spring Boot中使用JUnit 4? | 需要移除POM文件中junit-vintage-engine的排除配置。 |
| 3. JUnit 5是否需要指定启动类? | 默认情况下,JUnit 5会自动检测启动类。如果项目中有多个启动类,需要通过@SpringBootTest(classes = ...)指定。 |
4. JUnit 5的测试方法是否需要@RunWith注解? | 不需要,JUnit 5使用@SpringBootTest注解即可。 |
| 5. 如何确认JUnit版本? | 通过POM文件中的依赖版本确认,例如junit-jupiter-api的版本号。 |
七、流程图:Spring Boot整合JUnit单元测试的步骤
创建Spring Boot项目添加spring-boot-starter-test依赖确认JUnit版本编写测试代码运行测试
第一部分总结
我们现在直接使用junit5在springboot项目中使用即可。使用方式为添加注解@SpringBootTest(会自己找启动类,有多个则通过classes = ...指定)就可以让容器启动来测试。
疑问1:不是说5的话boot项目需要ext注解吗?
AI解答:
@ExtendWith 确实是 JUnit 5 的核心注解。
不过,在 Spring Boot 项目里,你通常不需要手动添加它。
🤔 为什么会有这个误解?
你的理解是对的:JUnit 5 确实使用 @ExtendWith 来集成 Spring 的功能,它取代了 JUnit 4 的 @RunWith。
在早期版本的 Spring Boot (2.1.x 之前),确实需要手动在测试类上写上 @ExtendWith(SpringExtension.class)。
✅ 现在的实际情况是怎样的?
从 Spring Boot 2.1.x 版本开始,官方为了让开发更便捷,做了一个“贴心”的优化。
@SpringBootTest 这个我们最常用的注解,其内部源码已经自动包含了 @ExtendWith(SpringExtension.class)。
你可以把它理解为:@SpringBootTest 是一个“大礼包”,里面已经帮你把 @ExtendWith 这个“工具”放进去了。所以,当你使用 @SpringBootTest 时,就无需再重复添加了。
代码对比
-
早期写法 (已过时)
// 在 Spring Boot 2.1 之前,需要这样写 @ExtendWith(SpringExtension.class) // 手动启用 Spring 扩展 @SpringBootTest public class OrderServiceTest { // ... } -
现在写法 (推荐) // 现在,这样写就足够了! @SpringBootTest public class OrderServiceTest { // ... }
💡 什么时候才需要手动写 @ExtendWith?
只有在一些不使用 @SpringBootTest 的底层测试场景中,你才可能需要手动添加。
例如,如果你只用 @ContextConfiguration 来加载一个特定的 Spring 配置文件进行测试,这时就需要手动通过 @ExtendWith(SpringExtension.class) 来启用 Spring 的扩展支持。
但对于绝大多数 Spring Boot 的集成测试来说,放心地只使用 @SpringBootTest 吧!
疑问2:有必要每次启动整个容器吗?请看第二部分
第二部分 springboot项目怎么进行mock
很多开发者在刚开始写测试时,都会纠结是“启动整个 Spring 容器”还是“只测一个类”。
简单来说, “优雅”的测试 = 选对测试类型 + 用好 Mock 隔离
🤔 一、Mock 是啥?解决啥问题?
1. 通俗解释:什么是 Mock?
想象一下,你是一名汽车引擎设计师(你在测试 UserService),你需要测试引擎(UserService)的运转是否良好。
-
如果不使用 Mock:你必须把引擎装到整辆车里,连上油箱、排气管、轮胎,甚至要把车开到路上(启动 Spring 容器、连接真实数据库、连接真实 Redis)。这非常慢,而且如果车打不着火,你不知道是引擎坏了,还是油箱漏了,还是轮胎没气。
-
使用 Mock:你在实验室里,给引擎接上一个模拟油箱(Mock Repository)和一个模拟排气管(Mock EmailService)。
- 你可以控制“模拟油箱”里有多少油(Stubbing:预设返回值)。
- 你可以观察引擎是否真的向“模拟排气管”排气了(Verification:验证调用)。
- 重点:你只测试引擎本身,不关心外面的世界。
2. Mock 解决了什么问题?
- 速度极快:不需要启动 Spring 容器,不需要连接数据库(IO 操作最耗时)。单元测试通常是毫秒级的。
- 隔离性强:如果测试失败了,肯定是你的
Service逻辑写错了,而不是因为数据库连不上,或者网络波动。 - 覆盖极端情况:你可以轻松模拟“数据库挂了”或者“查不到数据”的场景,而在真实环境中很难故意制造这些故障。
✨ 二、如何“优雅”地执行 Boot 项目单元测试?
在 Spring Boot 中,优雅的核心在于 “各司其职” 。不要把所有测试都写成 @SpringBootTest(启动全容器),那样太慢了。
我们需要区分两种测试策略:
1. 纯单元测试 (Unit Test) —— 推荐用于 Service 层
特点:完全不启动 Spring 容器,纯 Java 代码运行。
工具:JUnit 5 + Mockito (@Mock, @InjectMocks)。
场景:测试 UserService 里的业务逻辑(比如计算价格、校验参数)。
代码示例:
// 1. 不需要 @SpringBootTest,不需要启动容器!
// 使用 Mockito 的扩展来初始化 Mock 对象
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
// 2. @Mock: 创建一个假的 UserRepository,它是空的,需要你喂数据
@Mock
private UserRepository userRepository;
// 3. @InjectMocks: 创建 UserService 实例,并把上面的 userRepository 塞进去
@InjectMocks
private UserService userService;
@Test
void shouldFindUserById() {
// --- Arrange (准备) ---
User mockUser = new User(1L, "Alice");
// 告诉 Mock 对象:当有人调用 findById(1L) 时,返回 mockUser
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// --- Act (执行) ---
User result = userService.findById(1L);
// --- Assert (断言) ---
assertThat(result.getName()).isEqualTo("Alice");
// --- Verify (验证) ---
// 验证 userRepository.findById 是否真的被调用了一次
verify(userRepository, times(1)).findById(1L);
}
}
优雅点:速度飞快,完全隔离。
2. 切片测试 / 集成测试 (Slice Test) —— 推荐用于 Controller 或 Repository
特点:只启动 Spring 容器的一部分(比如只启动 Web 层,或者只启动 JPA 层)。
工具:@WebMvcTest (控制器), @DataJpaTest (数据库), @MockBean。
场景:测试 UserController 的接口映射是否正确,或者测试 SQL 语句是否正确。
代码示例 (测试 Controller) :
// 1. @WebMvcTest: 只启动 Web 层相关的 Bean (Controller, Converter 等),不启动 Service
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // Spring 提供的模拟 HTTP 客户端
// 2. @MockBean: 这是 Spring 的注解!
// 它会去 Spring 容器里,把 UserService 替换成一个 Mock 对象
@MockBean
private UserService userService;
@Test
void shouldReturnUserJson() throws Exception {
// --- Arrange ---
// 模拟 Service 层返回数据
when(userService.findById(1L)).thenReturn(new User(1L, "Alice"));
// --- Act & Assert ---
// 发送一个模拟的 GET 请求
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk()) // 期望状态码 200
.andExpect(jsonPath("$.name").value("Alice")); // 期望返回 JSON 中有 name: Alice
}
}
优雅点:比 @SpringBootTest 快,但又能测试 Spring 的注解(如 @RestController, @RequestMapping)是否生效。
📊 三、总结:Mock 注解对比表
这是最容易混淆的地方,请注意区分:
表格
| 特性 | @Mock(Mockito) | @MockBean(Spring Boot) |
|---|---|---|
| 所属库 | Mockito | Spring Boot Test |
| 是否启动 Spring | 否 (纯单元测试) | 是 (集成测试/切片测试) |
| 作用范围 | 仅在测试类内部有效 | 会替换 Spring 容器中的 Bean |
| 使用场景 | 测试 Service 业务逻辑 | 测试 Controller, 或者需要 Spring 注入的场景 |
| 性能 | 极快 (毫秒级) | 较快 (秒级,取决于加载多少组件) |
🚀 四、最佳实践建议
- Service 层:优先使用
@ExtendWith(MockitoExtension.class)+@Mock。不要动不动就@SpringBootTest,那样太慢了。 - Controller 层:使用
@WebMvcTest+@MockBean。 - Repository 层:使用
@DataJpaTest(它会自动配置内存数据库 H2)。 - 全链路测试:只有当你需要测试“整个应用能不能跑起来”或者“配置类是否正确”时,才使用
@SpringBootTest。
这样分层测试,你的项目构建速度会非常快,而且逻辑清晰,维护起来也很优雅。
第二部分总结
使用mock可以最小化范围测试,而不是启动整个容器。一般测试的都是service层,直接使用@ExtendWith(MockitoExtension.class) + @Mock。不要动不动就 @SpringBootTest,那样太慢了
第三部分 Mock中的常见问题
一、mock原理
就是 “伪造” 依赖接口 / 对象 / 函数的返回结果,让程序在没有真实后端、真实服务时也能正常跑、正常测。原理为通过动态代理、字节码增强或请求拦截等方式,劫持目标方法 / 接口调用,跳过真实逻辑执行并直接返回预设伪造数据,从而实现依赖隔离与行为模拟。
✨ 疑问:final类怎么模拟呢?
在 Java 中,final 关键字的设计初衷就是为了防止继承(类)或重写(方法)。而 Mockito 的核心原理恰恰是生成子类(动态代理)来拦截方法调用。 所以,默认情况下,Mockito 无法 Mock final 类或 final 方法。如果你强行去 mock,通常会报 Cannot mock/spy class ... final class 的错误。
🚀 方案:使用 mockito-inline(推荐,现代做法)
这是目前最主流的做法。从 Mockito 2.x 后期版本开始,官方提供了一个扩展模块 mockito-inline,它利用 Java Instrumentation API 在运行时修改字节码,从而支持 Mock final 类。
适用场景:Spring Boot 2.x (较新版本) 或 Spring Boot 3.x,且你不想引入沉重的 PowerMock。
1. 添加依赖
虽然 Spring Boot 的 spring-boot-starter-test 已经包含了 mockito-core,但你需要额外引入 mockito-inline。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.x.x</version> <!-- 版本号通常与 mockito-core 保持一致 -->
<scope>test</scope>
</dependency>
2. 开启配置(关键步骤)
仅仅加依赖是不够的,你必须告诉 Mockito 使用这个“内联”模式。
在 src/test/resources 目录下创建一个文件夹 mockito-extensions,并在其中创建一个文件 org.mockito.plugins.MockMaker。
-
文件路径:
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker -
文件内容:
mock-maker=inline
3. 编写测试
配置好后,你就可以像 Mock 普通类一样 Mock final 类了,代码完全不用变:
// 假设 FinalService 是一个 final 类
final class FinalService {
public String sayHello() { return "Hello"; }
}
@ExtendWith(MockitoExtension.class)
class FinalServiceTest {
@Mock
private FinalService finalService; // 直接 @Mock,不会报错!
@Test
void testFinalClass() {
when(finalService.sayHello()).thenReturn("Mocked Hello");
assertEquals("Mocked Hello", finalService.sayHello());
}
}
二、🤔 Spy 是啥?解决什么问题?
1. 核心概念:部分模拟
-
Mock(完全模拟) :创建一个空壳对象。所有方法默认都不执行真实代码,直接返回 null 或 0。你必须手动定义每一个方法的行为。
-
Spy(部分模拟) :包装一个真实的对象。
- 默认情况下,它会执行真实的代码。
- 只有当你明确告诉它“这个方法要拦截”时,它才会返回假数据。
2. 解决什么问题?
- 场景一:遗留代码或复杂对象。当你有一个类,方法很多,你只想 Mock 其中一个很难测的方法(比如调用了外部 API),而其他方法逻辑很复杂你不想重写,这时用 Spy 最省事。
- 场景二:验证真实调用。你想确保某个方法被调用了,同时还想验证它执行后的真实副作用。
🛠️ 怎么用?(核心语法)
在 Spring Boot 项目中,我们通常分两种情况使用 Spy:纯单元测试 和 Spring 容器集成测试。
1. 纯单元测试(使用 @Spy)
这是 Mockito 的原生用法,用于测试普通的 Java 类。
关键点:使用 Spy 时,存根语法(Stubbing)必须换!
-
Mock 用:
when(mock.method()).thenReturn(...) -
Spy 用:
doReturn(...).when(spy).method()- 为什么?因为 Spy 默认执行真实方法,如果用
when(spy.method()),真实方法会立即执行,可能导致空指针异常。
- 为什么?因为 Spy 默认执行真实方法,如果用
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
// 1. 必须初始化真实对象!不能写 @Spy private UserService userService; (这样会报空指针)
@Spy
private UserService userService = new UserService();
@Mock
private UserRepository userRepository;
@Test
void testSpyUsage() {
// --- Arrange ---
// 假设 UserService 有个方法 calculateTax() 很复杂,我们想 Mock 它
// 注意语法:doReturn(...).when(spy).method()
doReturn(100.0).when(userService).calculateTax();
// --- Act ---
// 调用其他未 Mock 的方法,会执行真实逻辑
// 调用 calculateTax,会返回 100.0
double tax = userService.calculateTax();
// --- Assert ---
assertEquals(100.0, tax);
// --- Verify ---
// 验证真实方法是否被调用
verify(userService, times(1)).calculateTax();
}
}
2. Spring 集成测试(使用 @SpyBean)
当你使用 @SpringBootTest 时,普通的 @Spy 无法替换 Spring 容器里的 Bean。这时要用 Spring Boot 提供的 @SpyBean。
作用:把 Spring 容器里原本的 Bean 替换成一个 Spy 对象。
@SpringBootTest
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService; // 真实的 Service
// 1. @SpyBean:替换容器里的 UserService,但保留真实逻辑
@SpyBean
private UserService userService;
@Test
void testOrderWithSpyBean() {
// --- Arrange ---
// 拦截 getUserLevel 方法,返回 "VIP"
doReturn("VIP").when(userService).getUserLevel(anyLong());
// --- Act ---
// 调用 orderService,它会调用 userService.getUserLevel
// 此时 getUserLevel 返回 "VIP",但 userService 的其他方法(如 saveUser)仍走真实数据库逻辑(如果配置了的话)
orderService.createOrder(1L);
// --- Verify ---
// 验证 getUserLevel 确实被调用了
verify(userService).getUserLevel(1L);
}
}
⚖️ Mock vs Spy:怎么选?
为了让你更清晰地做决定,我整理了这个对比表:
表格
| 维度 | @Mock (完全模拟) | @Spy / @SpyBean (部分模拟) |
|---|---|---|
| 真实代码执行 | 绝不执行 | 默认执行 (除非被拦截) |
| 初始化要求 | 不需要实例化 | 必须有真实实例 (new Object()) |
| 存根语法 | when(mock.method())... | doReturn(...).when(spy)... |
| 风险 | 低(完全隔离) | 中(真实代码可能抛异常或依赖数据库) |
| 适用场景 | 依赖对象(Repository, Client) | 被测对象本身(想测部分逻辑)、遗留代码 |
⚖️ Spy vs InjectMocks
| 维度 | @Spy | @InjectMocks |
|---|---|---|
| 核心职责 | 部分模拟。包装一个真实对象,保留真实逻辑,但允许拦截特定方法。 | 依赖注入。创建被测对象,并自动把 @Mock 或 @Spy 塞进去。 |
| 代码行为 | 默认执行真实代码。 | 负责初始化对象(通过构造函数或字段注入)。 |
| 语法陷阱 | 必须手动初始化实例(= new UserService()),否则报错。 | 不需要手动初始化,Mockito 会自动帮你 new 出来。 |
| 常用搭配 | 用于被测对象本身(当你不想 Mock 所有方法时)。 | 用于被测对象(当你想完全隔离,只测逻辑流转时)。 |
| 存根语法 | 必须用 doReturn(...).when(spy)... | (它本身不存根,它注入的对象如果是 Mock,则用 when...thenReturn) |
💡 避坑指南
- 初始化陷阱:使用
@Spy时,字段必须手动初始化(如= new UserService()),否则 Mockito 无法创建 Spy 对象,会报NullPointerException。 - Final 方法:和 Mock 一样,Spy 也无法 Spy
final方法。调用final方法时,永远执行真实代码,无法拦截。 - 自调用问题:在 Spring 中,如果一个 Bean 的方法 A 调用了同一个类的方法 B(
this.methodB()),即使你 Spy 了方法 B,A 调用 B 时走的也是真实逻辑,Spy 的拦截可能失效(因为 Spring AOP 代理机制)。
总结建议:在单元测试中,优先使用 @Mock,因为它更干净、更安全。只有当你真的需要保留真实逻辑,或者为了省事不想 Mock 所有依赖时,才使用 @Spy。