Junit到Springboot单元测试

第一部分 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文件的配置示例:

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>

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的测试代码示例:

java 复制代码
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的测试代码示例:

typescript 复制代码
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 测试方法运行成功!");
    }
}

五、实际案例:持久层与业务层测试

假设我们有一个UserDAOUserService,以下是它们的实现代码:

5.1 持久层代码

csharp 复制代码
public class UserDAOImpl {
    public void insert() {
        System.out.println("INSERT INTO USER VALUES(...)");
    }
}

5.2 业务层代码

java 复制代码
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 测试代码

typescript 复制代码
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 时,就无需再重复添加了。

代码对比
  • 早期写法 (已过时)

    less 复制代码
    // 在 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 里的业务逻辑(比如计算价格、校验参数)。

代码示例

less 复制代码
// 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)

less 复制代码
// 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 注入的场景
性能 极快 (毫秒级) 较快 (秒级,取决于加载多少组件)

🚀 四、最佳实践建议

  1. Service 层 :优先使用 @ExtendWith(MockitoExtension.class) + @Mock 。不要动不动就 @SpringBootTest,那样太慢了。
  2. Controller 层 :使用 @WebMvcTest + @MockBean
  3. Repository 层 :使用 @DataJpaTest (它会自动配置内存数据库 H2)。
  4. 全链路测试 :只有当你需要测试"整个应用能不能跑起来"或者"配置类是否正确"时,才使用 @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

xml 复制代码
<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

  • 文件内容

    ini 复制代码
    mock-maker=inline
3. 编写测试

配置好后,你就可以像 Mock 普通类一样 Mock final 类了,代码完全不用变:

kotlin 复制代码
// 假设 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()),真实方法会立即执行,可能导致空指针异常。
scss 复制代码
@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 对象。

scss 复制代码
@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)

💡 避坑指南

  1. 初始化陷阱 :使用 @Spy 时,字段必须手动初始化(如 = new UserService()),否则 Mockito 无法创建 Spy 对象,会报 NullPointerException
  2. Final 方法 :和 Mock 一样,Spy 也无法 Spy final 方法。调用 final 方法时,永远执行真实代码,无法拦截。
  3. 自调用问题 :在 Spring 中,如果一个 Bean 的方法 A 调用了同一个类的方法 B(this.methodB()),即使你 Spy 了方法 B,A 调用 B 时走的也是真实逻辑,Spy 的拦截可能失效(因为 Spring AOP 代理机制)。

总结建议 :在单元测试中,优先使用 @Mock ,因为它更干净、更安全。只有当你真的需要保留真实逻辑,或者为了省事不想 Mock 所有依赖时,才使用 @Spy

相关推荐
爱勇宝13 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒18 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码19 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo19 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志20 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..20 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects20 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰20 小时前
一些基础和问题解决
后端
陈随易20 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN20 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp