当前位置: 首页 > news >正文

Mockito 原理与实战

一、Mockito 核心原理

1. 动态代理与字节码生成

Mockito 并非使用标准的 Java 动态代理(java.lang.reflect.Proxy),而是基于 CGLIBASM 库,在运行时:

  • 动态生成目标类的子类(对于非 final 类)
  • 或接口的实现类(对于接口)
  • 重写所有方法,替换为“可控制”的行为

✅ 优势:可以 mock 普通类、抽象类、接口,甚至部分方法(spy


2. Mock 的创建过程

UserRepository mockRepo = mock(UserRepository.class);

执行过程:

  1. 生成子类字节码:CGLIB 创建 UserRepository$$EnhancerByMockito 类
  2. 方法拦截:所有方法调用被重定向到 MockHandler
  3. 行为匹配:根据 when(...).thenReturn(...) 的 stubbing 规则返回值或抛异常
  4. 记录调用:用于后续 verify(...) 验证

3. Stubbing(打桩) vs Verification(验证)

阶段目的使用方法
Stubbing预设方法返回值when(mock.method()).thenReturn(value)
Verification验证方法是否被调用verify(mock).method()

4. Mock vs Spy

类型行为适用场景
mock(Class)全新虚拟对象,所有方法默认返回 null/0/false完全隔离依赖
spy(Object)真实对象,但可部分 mock 方法测试部分逻辑,保留其他真实行为

二、Mockito 核心用法详解

示例:UserServiceTest.java

// 引入 JUnit 5 的测试类
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
// 引入 Mockito 注解和核心类
import org.mockito.*;
// 引入断言库(AssertJ)
import static org.assertj.core.api.Assertions.*;
// 引入 Mockito 的 BDD 风格语法(given/when/then)
import static org.mockito.BDDMockito.*;// 使用 JUnit 5 的扩展,自动处理 Mockito 注解
class UserServiceTest {// @Mock:创建一个 UserRepository 的 mock 对象// 所有方法默认返回 null(对于对象)、0(int)、false(boolean)等@Mockprivate UserMapper userMapper;// @InjectMocks:创建 UserService 实例,并自动将上面的 @Mock 注入到其字段中// 相当于:userService = new UserService(userMapper);@InjectMocksprivate UserService userService;// @BeforeEach:在每个测试方法执行前运行@BeforeEachvoid setUp() {// 初始化所有 @Mock 和 @InjectMocks 注解// 必须调用,否则 mock 不会生效MockitoAnnotations.openMocks(this);}// 测试:创建用户是否成功@Testvoid shouldCreateUserSuccessfully() {// Given:准备测试数据和预设行为(Arrange)// 创建一个待插入的用户对象User user = new User("Alice", "alice@example.com");// 设置 ID 为 null,表示尚未保存user.setId(1L);// 打桩(Stubbing):当调用 userMapper.insert(any(User.class)) 时// any(User.class) 表示匹配任意 User 类型的参数// thenAnswer 允许我们编写更复杂的逻辑,比如模拟主键回填when(userMapper.insert(any(User.class))).thenAnswer(invocation -> {// 获取传入的参数(即要插入的 user 对象)User u = invocation.getArgument(0);// 模拟数据库生成主键的行为,设置 ID = 1u.setId(1L);// 返回 1 表示插入成功(影响行数)return 1;});// When:执行被测方法(Act)// 调用 userService 的 createUser 方法User result = userService.createUser("Alice", "alice@example.com");// Then:验证结果(Assert)// 断言:返回的用户 ID 应该是 1assertThat(result.getId()).isEqualTo(1L);// 验证:userMapper.insert 方法是否被调用了一次// argThat(...) 用于自定义参数匹配器// 这里验证传入的 User 对象的 name 字段是 "Alice"verify(userMapper).insert(argThat(u -> u.getName().equals("Alice")));}// 测试:当用户不存在时应抛出异常@Testvoid shouldThrowExceptionWhenUserNotFound() {// Given:预设行为// 当调用 findById(999L) 时返回 null(表示数据库查不到)given(userMapper.findById(999L)).willReturn(null);// When & Then:执行并验证异常(AssertJ 风格)// 断言:调用 getUserById(999L) 会抛出 UserNotFoundExceptionassertThatThrownBy(() -> userService.getUserById(999L))// 并且异常类型是 UserNotFoundException.isInstanceOf(UserNotFoundException.class)// 并且异常消息包含 "999".hasMessageContaining("999");}// 测试:获取所有用户@Testvoid shouldReturnAllUsers() {// Given:预设 findAll() 返回一个包含一个用户的列表User user1 = new User("Bob", "bob@example.com");user1.setId(2L);// 将预设行为绑定到 mock 对象given(userMapper.findAll()).willReturn(java.util.Arrays.asList(user1));// When:调用服务方法List<User> result = userService.getAllUsers();// Then:验证结果assertThat(result).hasSize(1);           // 列表大小为 1assertThat(result.get(0).getName()).isEqualTo("Bob"); // 第一个用户名字是 Bob}// 测试:更新用户@Testvoid shouldUpdateUserSuccessfully() {// Given:预设 findById 返回一个用户,update 返回影响行数 1User existingUser = new User("OldName", "old@example.com");existingUser.setId(1L);given(userMapper.findById(1L)).willReturn(existingUser);given(userMapper.update(any(User.class))).willReturn(1);// When:调用更新方法User updated = userService.updateUser(1L, "NewName", "new@example.com");// Then:验证assertThat(updated.getName()).isEqualTo("NewName");// 验证 update 方法被调用了一次verify(userMapper).update(argThat(u ->u.getName().equals("NewName") &&u.getEmail().equals("new@example.com")));}// 测试:删除用户@Testvoid shouldDeleteUser() {// Given:预设 findById 返回用户,deleteById 返回 1given(userMapper.findById(1L)).willReturn(new User("ToDelete", "del@example.com"));given(userMapper.deleteById(1L)).willReturn(1);// When:执行删除userService.deleteUser(1L);// Then:验证 deleteById 被调用了一次,参数是 1Lverify(userMapper).deleteById(eq(1L)); // eq(1L) 明确匹配 1L}// 测试:验证方法调用次数@Testvoid shouldVerifyCallCount() {// Givengiven(userMapper.findAll()).willReturn(java.util.Collections.emptyList());// WhenuserService.getAllUsers();userService.getAllUsers(); // 调用两次// Then:验证 findAll() 被调用了 2 次verify(userMapper, times(2)).findAll();// 其他调用次数验证:// verify(userMapper, never()).deleteById(999L);       // 从未调用// verify(userMapper, atLeastOnce()).findAll();       // 至少调用一次// verify(userMapper, atMost(3)).findAll();           // 最多调用 3 次}// 测试:抛出异常@Testvoid shouldThrowExceptionOnSave() {// Given:当 insert 被调用时抛出 RuntimeExceptiondoThrow(new RuntimeException("DB Error")).when(userMapper).insert(any(User.class));// When & ThenassertThatThrownBy(() -> userService.createUser("Fail", "fail@example.com")).hasMessageContaining("DB Error");}// 测试:Spy(部分模拟)@Testvoid shouldUseSpy() {// 创建一个真实 ArrayList,并用 spy 包装List<String> list = spy(new java.util.ArrayList<String>());// 预设 get(0) 返回 "mocked"when(list.get(0)).thenReturn("mocked");// add() 仍执行真实逻辑list.add("real");// 验证assertThat(list.get(0)).isEqualTo("mocked"); // 被 mockassertThat(list.get(1)).isEqualTo("real");   // 真实行为assertThat(list).hasSize(2);                 // 真实 size}
}

三、高级用法与最佳实践

1. 参数匹配器(Argument Matchers)

匹配器说明
any()任意对象
anyString()任意字符串
eq("value")精确匹配
argThat(x -> x > 5)自定义条件
isNull()null 值
same(obj)同一个引用

⚠️ 注意:不能混用具体值和匹配器
when(repo.findById(1L, anyString())) → 错误
when(repo.findById(eq(1L), anyString())) → 正确


2. BDD 风格写法(推荐)

// Given
given(userMapper.findById(1L)).willReturn(user);// When
User result = userService.getUserById(1L);// Then
then(userMapper).should().findById(1L);

3. Mockito 扩展(JUnit 5)

@ExtendWith(MockitoExtension.class)
class UserServiceTest {@Mock UserMapper mapper;@InjectMocks UserService service;// 不需要 @BeforeEach 中的 openMocks()
}

四、常见问题与陷阱

问题解决方案
NullPointerException忘记 MockitoAnnotations.openMocks(this);
UnnecessaryStubbing存在未使用的 when(...)
Wanted but not invokedverify 的方法未被调用
Mockito cannot mock this class类是 final / 无参构造函数私有

文章转载自:

http://EnjWwu0H.xjkfb.cn
http://4GJjIFBJ.xjkfb.cn
http://LJvfoewq.xjkfb.cn
http://tnrtYB08.xjkfb.cn
http://SrT2wFza.xjkfb.cn
http://Oe9iL98J.xjkfb.cn
http://wgIaMWgo.xjkfb.cn
http://XtFdHYnT.xjkfb.cn
http://tHnNblW1.xjkfb.cn
http://h21dWPDY.xjkfb.cn
http://EgLmyBkv.xjkfb.cn
http://vscUstfd.xjkfb.cn
http://3fzE1wCg.xjkfb.cn
http://GAQDDHfU.xjkfb.cn
http://ID2I4oy8.xjkfb.cn
http://eLe4DBYw.xjkfb.cn
http://PE9rZlNc.xjkfb.cn
http://8yEfd59G.xjkfb.cn
http://GsWiYBV3.xjkfb.cn
http://vbLYYU0U.xjkfb.cn
http://gLBWfKO7.xjkfb.cn
http://YAoJdWxv.xjkfb.cn
http://LNPdOUll.xjkfb.cn
http://an4bIrDJ.xjkfb.cn
http://tWO879KE.xjkfb.cn
http://4lEVH9Be.xjkfb.cn
http://QfeaKJ3b.xjkfb.cn
http://SAiGomTu.xjkfb.cn
http://Id85FgSu.xjkfb.cn
http://XSo0jxzn.xjkfb.cn
http://www.dtcms.com/a/375307.html

相关文章:

  • Django项目架构
  • SpringBoot整合通用ClamAV文件扫描病毒
  • 提权分析报告 —— 基于DriftingBlues: 4靶场
  • 设计模式-原则概述
  • LAMPSecurity: CTF8靶场渗透
  • python网络爬取个人学习指南-(五)
  • CSS 基础概念
  • 在企业内部分发 iOS App 时如何生成并使用 manifest.plist
  • AJAX入门-AJAX 概念和 axios 使用
  • 框架-MyBatis|Plus-1
  • Spring Boot 2.7 启动流程详解
  • springboot框架使用websocket实现一个聊天室的细节
  • Kubernetes集群部署Jenkins指南
  • 027、全球数据库市场深度分析:技术革命下的产业格局重塑
  • 贪心算法与动态规划:数学原理、实现与优化
  • Oracle APEX 利用卡片实现翻转(方法二)
  • 记一次 electron 添加 检测 终端编码,解决终端打印中文乱码问题
  • 从生活照料到精神关怀,七彩喜打造全场景养老服务体系
  • 2025-09-08升级问题记录: 升级SDK从Android11到Android12
  • BizDevOps 是什么?如何建设企业 BizDevOps 体系
  • 一、ARM异常等级及切换
  • 【项目复现】MOOSE-Chem 用于重新发现未见化学科学假说的大型语言模型
  • mybatis plus 使用wrapper输出SQL
  • PgSQL中优化术语HOT详解
  • Python 绘制 2025年 9~11月 P/1999 RO28 (LONEOS) 彗星路径
  • Spring Cloud Stream深度实战:发布订阅模式解决微服务通信难题
  • 【菜狗每日记录】深度轨迹聚类算法、GRU门控神经网络—20250909
  • OpenCV 实战:多角度模板匹配实现图像目标精准定位
  • C#/.NET/.NET Core技术前沿周刊 | 第 53 期(2025年9.1-9.7)
  • 基于Java+Vue开发的家政服务系统源码适配H5小程序APP