如何写好单元测试:Mock 脱离数据库,告别 @SpringBootTest 的重型启动

如何写好单元测试:Mock 脱离数据库,告别 @SpringBootTest 的重型启动

作者:Killian(重庆) — 欢迎各位架构猎头、技术布道者联系我,项目实战丰富,代码稳健,Mock测试爱好者。
技术栈:Java 17、JUnit 5、Mockito 5、Spring Boot 3.x(可选)


一、前言

你是否遇到过以下问题:

  • 每次跑测试都要加载整个 Spring 容器,慢如蜗牛?
  • 明明只测一个方法,却启动了 Redis、MySQL、MQ 等服务?
  • 想 Mock 一个 Bean 却被 @Autowired 绑死?

这时候,我们该说:不需要 @SpringBootTest!

本篇文章将系统讲解:

  • 如何编写真正的“单元”测试(Unit Test)
  • 如何使用 Mockito 精准 Mock 依赖,避免启动数据库等外部依赖
  • 如何写出高覆盖率、快反馈、可维护的业务逻辑测试

二、为什么要避免 @SpringBootTest?

问题描述
启动慢@SpringBootTest 会加载整个上下文(Controller、Service、Repository、Config)
依赖重需要配置数据库、缓存、RabbitMQ 等外部环境
不稳定环境不一致容易导致测试 flaky(有时通过,有时失败)
非单元测试实际上是“集成测试”,容易误用

三、正确的方式:使用 Mockito + JUnit 写真正的单元测试

示例背景

我们有一个服务类:

@Service
public class OrderService {private final OrderRepository orderRepository;private final PaymentClient paymentClient;public OrderService(OrderRepository orderRepository, PaymentClient paymentClient) {this.orderRepository = orderRepository;this.paymentClient = paymentClient;}public String pay(String orderId) {Order order = orderRepository.findById(orderId).orElseThrow(() -> new RuntimeException("订单不存在"));if (order.isPaid()) {return "重复支付";}boolean result = paymentClient.callPayGateway(order);if (result) {order.markPaid();orderRepository.save(order);return "支付成功";} else {return "支付失败";}}
}

单元测试写法(脱离容器 + Mock 依赖)

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {@Mock OrderRepository orderRepository;@Mock PaymentClient paymentClient;@InjectMocks OrderService orderService;@Test@DisplayName("支付成功时,订单状态应更新并保存")void testPaySuccess() {Order mockOrder = new Order("123", false);when(orderRepository.findById("123")).thenReturn(Optional.of(mockOrder));when(paymentClient.callPayGateway(mockOrder)).thenReturn(true);String result = orderService.pay("123");assertEquals("支付成功", result);assertTrue(mockOrder.isPaid());verify(orderRepository).save(mockOrder);}@Test@DisplayName("找不到订单时,应抛出异常")void testOrderNotFound() {when(orderRepository.findById("999")).thenReturn(Optional.empty());assertThrows(RuntimeException.class, () -> orderService.pay("999"));}@Test@DisplayName("已支付订单不应重复支付,也不应保存")void testAlreadyPaid() {Order paidOrder = new Order("456", true);when(orderRepository.findById("456")).thenReturn(Optional.of(paidOrder));String result = orderService.pay("456");assertEquals("重复支付", result);verify(orderRepository, never()).save(any());}
}

四、关键技巧:Mock 什么?怎么 Mock?

1. 只 Mock “外部依赖”

  • 数据库 Repository
  • 第三方客户端(如 FeignClient、HttpClient)
  • Redis 操作、MQ 发送器、ES 操作器

2. 不 Mock 的部分

  • 自己写的业务逻辑类(即你要测的类)

3. 使用 Mockito 提供的能力

  • when(...).thenReturn(...):设置返回值
  • verify(...):验证方法是否调用
  • argThat(...):匹配参数条件
  • doThrow(...):模拟异常

五、单元测试 vs 集成测试:职责边界与框架选择

对比表格

维度单元测试(Unit Test)集成测试(Integration Test)
启动方式不启动 Spring 容器启动 Spring 容器(或部分)
测试目标业务逻辑、算法正确性Bean 交互、配置、环境集成
Mock 使用必须 Mock 外部依赖通常不 Mock,使用真实组件
性能快,毫秒级慢,秒级
数据源无数据库或 H2 Mock真正连接数据库(如 Docker 启动 MySQL)
断言粒度精确控制方法行为更偏向流程通路与集成稳定性

@DataJpaTest

用于测试 JPA Repository 层(不加载 Service、Controller):

@DataJpaTest
class UserRepositoryTest {@Autowired UserRepository repo;@Test@DisplayName("根据用户名查询用户,应返回结果")void testFindByUsername() {User u = new User("tom", "123");repo.save(u);assertTrue(repo.findByUsername("tom").isPresent());}
}

自动配置内嵌数据库(如 H2),速度适中,适合数据层测试。


@Mapper + MyBatis 的 Mapper 层测试(两种方式)

✅ 方式一:真实数据库 + @MybatisTest
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 保持使用真实数据库配置
class OrderMapperTest {@Autowired OrderMapper orderMapper;@Test@DisplayName("根据订单ID查询,应返回订单信息")void testSelectById() {Order order = orderMapper.selectById("order123");assertNotNull(order);}
}

说明:

  • @MybatisTest 会只加载 MyBatis 相关的配置(不会加载 Service、Controller)
  • 默认使用 H2,可通过 @AutoConfigureTestDatabase 强制保留 MySQL 等真实库
  • 可以测试 XML 映射、注解 SQL、分页插件等
✅ 方式二:Mock Mapper(更适合单元测试)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {@Mock OrderMapper orderMapper;@InjectMocks OrderService orderService;@Test@DisplayName("Mock Mapper 查询订单,应返回正确订单")void testOrderFetch() {Order mockOrder = new Order("order789", false);when(orderMapper.selectById("order789")).thenReturn(mockOrder);Order result = orderService.getOrder("order789");assertEquals("order789", result.getId());}
}

说明:

  • Mapper 在 Service 中作为依赖,Mock 掉即可测试业务逻辑
  • 不需要数据库、不用 @SpringBootTest,速度快、适合 CI

@WebMvcTest

用于测试 Controller 层(不加载业务逻辑):

@WebMvcTest(UserController.class)
class UserControllerTest {@Autowired MockMvc mockMvc;@MockBean UserService userService;@Test@DisplayName("调用 /hello 接口,应返回 hello tom")void testHelloApi() throws Exception {when(userService.getName()).thenReturn("tom");mockMvc.perform(get("/hello")).andExpect(status().isOk()).andExpect(content().string("hello tom"));}
}

优点是启动快,只加载 Web 层相关 Bean,可精准控制 Controller 输入输出。


要点说明
不使用 @SpringBootTest减少启动时间,提高测试速度
用 @ExtendWith(MockitoExtension.class)使用 Mockito 管理依赖注入
用 @InjectMocks注入被测类(业务类)
用 @Mock模拟依赖(Repository、外部接口)
每个测试只验证一件事保证测试原子性和可维护性

六、扩展阅读

  • Mockito 官方文档:https://site.mockito.org
  • JUnit 5 用户指南:https://junit.org/junit5/docs/current/user-guide/
  • 推荐阅读:Martin Fowler《Unit Test vs Integration Test》

七、结语

如果你写单元测试还依赖 @SpringBootTest,那就像每次微波炉加热都要重启电厂。Mock 依赖、聚焦业务、轻量高效,才是测试真正的姿势。

下一次写测试时,请问自己:“我是在测试业务逻辑,还是在启动一个服务器?”

本文由 @killian 原创,转载请注明出处。
☕ 请作者喝杯咖啡,持续更新更深入的干货

💡 彩蛋时间:如果你看到了这里,说明你是那种喜欢动手实战的人。那我悄悄分享一个开发圈流传的工具试用入口,貌似跟高效调试很有关系,地址也挺特别的:

🔗 入口

据说注册还能解锁一些隐藏功能,懂的都懂(别外传 😂)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/83854.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【DNS】在 Windows 下修改 `hosts` 文件

在 Windows 下修改 hosts 文件,一般用于本地 DNS 覆盖。操作步骤如下(以 Windows 10/11 为例): 1. 以管理员权限打开记事本 点击 开始 → 输入 “记事本”在“记事本”图标上右键 → 选择 以管理员身份运行 如果提示“是否允许此…

共享内存实现进程通信

目录 system V共享内存 共享内存示意图 共享内存函数 shmget函数 shmat函数 shmdt函数 shmctl函数 代码示例 shm头文件 构造函数 获取key值 创建者的构造方式 GetShmHelper 函数 GetShmUseCreate 函数 使用者的构造方式 GetShmForUse 函数 分离附加操作 DetachShm 函数 AttachS…

6月15日星期日早报简报微语报早读

6月15日星期日,农历五月二十,早报#微语早读。 1、证监会拟修订期货公司分类评价:明确扣分标准,优化加分标准; 2、国家考古遗址公园再添10家,全国已评定65家; 3、北京多所高校禁用罗马仕充电宝…

破解关键领域软件测试“三重难题”:安全、复杂性、保密性

在国家关键领域,软件系统正成为核心战斗力的一部分。相比通用软件,关键领域软件在 安全性、复杂性、实时性、保密性 等方面要求极高。如何保障安全合规前提下提升测试效率,确保系统稳定,已成为软件质量保障的核心挑战。 关键领域…

记录一次 Oracle DG 异常停库问题解决过程

记录一次 Oracle DG 异常停库问题解决过程 某医院有以下架构的双节点 Oracle 集群: 节点1:172.16.20.2 节点2:172.16.20.3 SCAN IP:172.16.20.1 DG:172.16.20.1206月12日,医院信息科用户反映无法连接 DG 服务器。 登录 DG 服务…

MySQL使用EXPLAIN命令查看SQL的执行计划

1‌、EXPLAIN 的语法 MySQL 中的 EXPLAIN 命令是用于分析 SQL 查询执行计划的关键工具,它能帮助开发者理解查询的执行方式并找出性能瓶颈‌‌。 语法格式: EXPLAIN <sql语句> 【示例】查询学生表关联班级表的执行计划。 (1)创建班级信息表和学生信息表,并创建索…

Go语言2个协程交替打印

WaitGroup 无缓冲channel waitgroup 用来控制2个协程 Add() 、Done()、Wait() channel用来实现信号的传递和信号的打印 ch1: 用来记录打印的信号 ch2:用来实现信号的传递&#xff0c;实现2个协程的顺序打印 package mainimport ("fmt""sync" )func ma…

微信小程序 路由跳转

路由方式 官方参考文档 wx.switchTab 实现底部导航栏 1.配置信息 app.json"tabBar": {"custom": true,"list": [{"pagePath": "pages/home/index","text": "首页"},{"pagePath": "p…

[Java 基础]正则表达式

正则表达式是一种强大的文本模式匹配工具&#xff0c;它使用一种特殊的语法来描述要搜索或操作的字符串模式。在 Java 中&#xff0c;我们可以使用 java.util.regex包提供的类来处理正则表达式。 :::color3 正则表达式不止 Java 语言提供了相应的功能&#xff0c;很多其他语言…

ArcGIS安装出现1606错误解决办法

问题背景&#xff1a; 由于最近Arcgis10.2打是有些功能不正常退出&#xff0c;比如arctoolbox中的&#xff0c;table to excel 功能&#xff0c;只要一点击&#xff0c;arcgis就报错退出&#xff0c;平常在使用过程中&#xff0c;也经常出现一些莫名其妙的崩溃现象&#xff0c…

wpf 解决DataGridTemplateColumn中width绑定失效问题

感谢酪酪烤奶 提供的Solution 文章目录 感谢酪酪烤奶 提供的Solution使用示例示例代码分析各类交互流程 WPF DataGrid 列宽绑定机制分析整体架构数据流分析1. ViewModel到Slider的绑定2. ViewModel到DataGrid列的绑定a. 绑定代理(BindingProxy)b. 列宽绑定c. 数据流 关键机制详…

语音转文本ASR、文本转语音TTS

ASR Automatic Speech Recognition&#xff0c;语音转文本。 技术难点&#xff1a; 声学多样性 口音、方言、语速、背景噪声会影响识别准确性&#xff1b;多人对话场景&#xff08;如会议录音&#xff09;需要区分说话人并分离语音。 语言模型适配 专业术语或网络新词需要动…

通用embedding模型和通用reranker模型,观测调研

调研Qwen3-Embedding和Qwen3-Reranker 现在有一个的问答库&#xff0c;包括150个QA-pair&#xff0c;用10个query去同时检索问答库的300个questionanswer Embedding模型&#xff0c;query-question的匹配分数 普遍高于 query-answer的匹配分数。比如对于10个query&#xff0c…

基于YOLOv8+Deepface的人脸检测与识别系统

摘要 人脸检测与识别系统是一个集成了先进计算机视觉技术的应用&#xff0c;通过深度学习模型实现人脸检测、识别和管理功能。系统采用双模式架构&#xff1a; ​​注册模式​​&#xff1a;检测新人脸并添加到数据库​​删除模式​​&#xff1a;识别数据库中的人脸并移除匹…

Grdle版本与Android Gradle Plugin版本, Android Studio对应关系

Grdle版本与Android Gradle Plugin版本&#xff0c; Android Studio对应关系 各个 Android Gradle 插件版本所需的 Gradle 版本&#xff1a; https://developer.android.com/build/releases/gradle-plugin?hlzh-cn Maven上发布的Android Gradle Plugin&#xff08;AGP&#x…

用c语言实现简易c语言扫雷游戏

void test(void) {int input 0;do{menu();printf("请选择&#xff1a; >");scanf("%d", &input);switch (input){menu();case 1:printf("扫雷\n");game();break;case 2:printf("退出游戏\n");break;default:printf("输入…

系统辨识的研究生水平读书报告期末作业参考

这是一份关于系统辨识的研究生水平读书报告&#xff0c;内容系统完整、逻辑性强&#xff0c;并深入探讨了理论、方法与实际应用。报告字数超过6000字 从理论到实践&#xff1a;系统辨识的核心思想、方法论与前沿挑战 摘要 系统辨识作为连接理论模型与客观世界的桥梁&#xff…

开源、免费、美观的 Vue 后台管理系统模板

随着前端技术的不断发展&#xff0c;Vue.js 凭借其轻量、高效、易上手的特性&#xff0c;成为国内外开发者最喜爱的前端框架之一。在构建后台管理系统时&#xff0c;Vue 提供了以下优势&#xff1a; 响应式数据绑定&#xff1a;让页面和数据保持同步&#xff0c;开发效率高。 …

适合 Acrobat DC 文件类型解析

文件类型 (File Type)ProgID (Continuous)ProgID (Classic)主要用途.pdfAcroExch.Document.DCAcroExch.Document.20XX (版本特定)Adobe PDF文档格式&#xff0c;用于存储文档内容和格式.pdfxmlAcroExch.pdfxmlAcroExch.pdfxmlPDF与XML结合的格式&#xff0c;可能用于结构化数据…

C/C++数据结构之漫谈

概述 在当今的数字化时代&#xff0c;无论是刷短视频、社交聊天&#xff0c;还是使用导航软件、网络购物&#xff0c;背后都离不开计算机技术的支持。但你是否想过&#xff1a;为什么同样的功能&#xff0c;有的软件运行得飞快&#xff0c;有的却严重卡顿&#xff0c;半天没有响…