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

【JUnit实战3_13】第八章:mock 对象模拟技术在细粒度测试中的应用(上)

JUnit in Action, Third Edition

《JUnit in Action》全新第3版封面截图

写在前面
在上一章介绍 Stub 模拟时作者曾反复强调,细粒度的测试还得使用 mock 对象进行模拟,并且还说 Stub 是过去人们对模拟测试的认识还不准确导致的中间产物,可谓吊足了我对 mock 模拟技术的胃口。深入了解后才发现,自己之前从前端和 Postman 那里偷学来的那点 mock 技术还是太肤浅了,至少对于隔离和本地这两个概念的认识很模糊。直到看到作者演示的案例,加上 DeepSeek 的趁热打铁,对于这个 mock 才自认算是入门了。可见叙事能力和选取经典案例的极端重要性。

文章目录

  • 第八章 mock 对象模拟技术在细粒度测试中的应用(上)
    • 8.1 基本概念
    • 8.2 演示案例概况
    • 8.3 模拟1:无重构模拟 transfer 方法

第八章 mock 对象模拟技术在细粒度测试中的应用(上)

本章概要

  • mock 对象简介与用法演示
  • 借助 mock 对象执行多种重构
  • 案例演示:用 mock 对象模拟 HTTP 连接
  • EasyMockJMockMockito 框架的用法及平行对比

Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the Universe trying to produce bigger and better idiots. So far, the Universe is winning.
如今的编程是一场竞赛:软件工程师们在竭尽全力地构建更庞大、更厉害的“傻瓜式”程序,而宇宙则在不遗余力地制造更强大、更厉害的傻瓜。目前看来还是宇宙更胜一筹。

—— Rich Cook

本章较为全面地介绍了 mock 对象模拟技术在单元测试中的基本原理和具体应用。

无论是 Stub 桩模拟还是 mock 对象模拟,其本质都是为了实现测试环境与真实环境的 隔离(isolation;区别在于它们实现的隔离程度不同:stub 桩的粒度更粗,常用于模拟远程 Web 服务器、文件系统、数据库等;而 mock 对象实现的隔离粒度更细,让单元测试可以精确到针对 具体某个方法 开展。

相关背景:mock 对象模拟的概念最早由 Tim MackinnonSteve FreemanPhilip CraigXP2000 极限编程国际大会1 上被首次提出。

8.1 基本概念

测试环境与真实环境相隔离的最大好处在于:被测系统即便依赖了其他对象,也不会受到任何因调用了它们的方法所产生的副作用的影响。

时刻保持测试用例的简单、轻量、小巧 是第一重要的。

单元测试套件的意义:让后续扩展及重构更有底气。

mock 模拟与 Stub 模拟的差异:

对比维度mock 对象Stub 桩模拟
隔离级别方法级(细粒度)系统级、模块级(粗粒度)
业务逻辑实现完全不涉及原逻辑,只是个 空壳完全保留原逻辑,与生产环境一致
预设行为完全无预设,须手动设置提前预设,运行后无法变更
测试模式初始化 mock ➡️ 设置期望 ➡️ 执行测试 ➡️ 验证断言初始化 Stub ➡️ 执行测试 ➡️ 验证断言

8.2 演示案例概况

本章重点研究两个案例:简化的银行转账场景,以及第七章介绍的远程 URL 连接场景。

银行转账场景的核心设计如下图所示:

Fig8.1

相关实现如下:

  1. AccountService 服务实现类:包含一个经办人 manager 依赖,以及待测方法 transfer()
public class AccountService {private AccountManager accountManager;public void setAccountManager(AccountManager manager) {this.accountManager = manager;}/*** A transfer method which transfers the amount of money* from the account with the senderId to the account of* beneficiaryId.*/public void transfer(String senderId, String beneficiaryId, long amount) {Account sender = accountManager.findAccountForUser(senderId);Account beneficiary = accountManager.findAccountForUser(beneficiaryId);sender.debit(amount);beneficiary.credit(amount);this.accountManager.updateAccount(sender);this.accountManager.updateAccount(beneficiary);}
}
  1. 经办人接口 AccountManager:转账逻辑主要涉及两个接口实现:转账前的帐户查询、转账后的帐户更新。由于本例不考虑更新失败导致的事务回滚操作,帐户更新对转账核心逻辑就没有任何贡献,因此可以不用实现:
public interface AccountManager {Account findAccountForUser(String userId);void updateAccount(Account account);
}
  1. Account 帐户实体类:仅包含帐户 id 和余额两个成员属性,以及涉及转账的两个核心操作(支出、收入):
/*** Account POJO to hold the bank account object.*/
public class Account {private String accountId;private long balance;public Account(String accountId, long initialBalance) {this.accountId = accountId;this.balance = initialBalance;}public void debit(long amount) {this.balance -= amount;}public void credit(long amount) {this.balance += amount;}public long getBalance() {return this.balance;}
}

8.3 模拟1:无重构模拟 transfer 方法

先从最简单的 mock 模拟开始演示。仔细观察转账方法 transfer(),其服务类已经通过依赖注入的方式引用了 accountManager,并调用了它的两个接口。在不考虑帐户更新失败导致的事务回滚的情况下,只需要模拟 findAccountForUser() 的实现即可。于是有了如下的模拟对象 MockAccountManager

public class MockAccountManager implements AccountManager {private Map<String, Account> accounts = new HashMap<String, Account>();public void addAccount(String userId, Account account) {this.accounts.put(userId, account);}public Account findAccountForUser(String userId) {return this.accounts.get(userId);}public void updateAccount(Account account) {// do nothing}
}

可以看到,帐户更新方法可以不用任何模拟逻辑;新增的 addAccount() 方法也只是为了方便测试过程中的初始化。这样测试用例就能完全控制 MockAccountManager 的所有状态了:

public class TestAccountService {@Testvoid testTransferOk() {// 1. 初始化 mock 对象MockAccountManager mockManager = new MockAccountManager();// 2. 设置期望值Account senderAccount = new Account("1", 200);Account beneficiaryAccount = new Account("2", 100);mockManager.addAccount("1", senderAccount);mockManager.addAccount("2", beneficiaryAccount);AccountService service = new AccountService();service.setAccountManager(mockManager);// 3. 执行测试service.transfer("1", "2", 50);// 4. 验证断言assertEquals(150, senderAccount.getBalance());assertEquals(150, beneficiaryAccount.getBalance());}
}

上述代码中——

  • mock 对象的模拟逻辑和真实环境下的具体逻辑毫不相关,只是实现了同一个 AccountManager 接口而已;
  • mock 对象的所有模拟逻辑都是围绕 怎样让测试用例完全控制 mock 对象的必要状态 展开的,包括新增的 HashMap<String, Account> 型成员变量,以及 addAccount() 方法的添加;
  • updateAccount() 由于对转账核心逻辑没有实质性贡献,模拟时直接留白即可。

关于 mock 模拟的两则 JUnit 最佳实践

  • 永远不要在 mock 对象中编写任何真实业务逻辑;
  • 测试仅针对可能出错的业务逻辑(忽略 updateAccount())。

第一次看到这里时,心中是非常疑惑的:既然 mock 对象的所有逻辑都是为了方便测试用例的全权控制专门模拟出来的,那它们就和真实环境完全脱钩了,即便后期切到真实场景报错了,这些模拟逻辑也依然会通过测试。这样的单元测试又有什么实际意义呢?要模拟转账,一不考虑数据库的查询逻辑,二不考虑更新失败后的回滚逻辑,这样的测试还能叫模拟转账吗?

如果你也跟我有同样的困惑,说明对前面提到的 隔离 二字的理解仍停留在表面:mock 对象模拟的最大价值,恰恰在于依靠这些模拟逻辑真正实现了 本地逻辑外部逻辑完全隔离

  • findAccountForUser()transfer() 方法自己的逻辑吗?
    • 答案是 否定的。那是 accountManager 引入的外来逻辑;
  • 同理,updateAccount()transfer() 自己的逻辑吗?
    • 答案也是 否定的。那也是 accountManager 引入的另一个外来逻辑。

查询、更新帐户是否顺利,本质上同我们真正关心的 transfer() 方法自带的业务逻辑 没有任何交集,那都是 accountManager 在具体实现时才需要考虑的问题。那么,transfer() 考虑的到底是哪些问题呢?无非是——

  1. 是否通过 accountManager.findAccountForUser() 的调用得到指定的帐户对象;
  2. 是否通过转账人的 debit() 方法扣减了正确的金额;
  3. 是否通过收款人的 credit() 方法收入了正确的金额;
  4. 是否利用 accountManager.updateAccount() 方法更新了转账后的帐户信息。

其中,1 和 4 通过 mock 对象已经通过验证了,因为对其设置的期望值就是按这些要求来的。2 和 3 的验证需要测试用例末尾的两个断言来决定,通过比较转账后的余额是否是设置的期望值就知道了。这样,transfer() 的固有逻辑就全部通过了,一旦真实转账出现 Bug 时,可以很明确地排除是转账逻辑本身导致的问题,只可能是由 accountManager 引入的外部逻辑有问题。如果 accountManager 的两个接口方法也按这个思路进行模拟,则可以进一步缩小排查范围,第一时间找出 Bug 的位置。

解决了最核心的困惑,后面的案例理解起来就轻松多了。

注意到 transfer() 方法没有需要重构的地方,accountManager 也通过依赖注入实现了数据库持久层和转账逻辑之间的解耦,上述模拟不涉及重构原逻辑环节。下面通过另一个方法演示需要重构源码的情况。


  1. 三人在大会上发表了著名论文 Endo-Testing: Unit Testing with Mock Objects。自此,mock objects 逐渐成为软件测试的标准实践,并催生了一系列模拟框架的发展,例如 JMockEasyMockMockito 等。 ↩︎

http://www.dtcms.com/a/528113.html

相关文章:

  • STM32项目分享:简易自动门设计
  • 小白怎样建设公司网站奔奔网站建设
  • YouTube评论情感分析项目84%正确率:基于BERT的实战复现与原理解析
  • 【Shell】Shell变量
  • 华为OD机考:计算正方形数量(Python C/C++ JAVA JS GO)
  • 基于 STM32 的语音识别智能垃圾桶设计与实现
  • 【基础复习3】决策树
  • 网站设计公司驻马店市住房和城乡建设局网站首页
  • Microsoft AI Genius | 用智能 Microsoft Copilot 副驾驶® 构建高韧性 DevOps 流程
  • wordpress网站布置电子商务网站建设的心得
  • nicegui 无框模式最小化关闭例子
  • 【气动技术】气动控制元件及其选型计算
  • LCL滤波器传递函数及波特图绘制
  • 银河麒麟v10 sp1更改data目录挂载
  • 在安卓中基于OpenGL ES实现随风飘荡3D动画效果
  • Java坐标转换技术详解
  • AWS Systems Manager:批量服务器管理的隐藏利器
  • 如何分析对手网站关键词网页版游戏平台
  • 招聘网站建设初衷远程数据库 wordpress
  • 驱动隔离芯片:电子系统的安全与效能守护者
  • 【经验】Word/WPS|用邮件合并批量填写表格或教案,单个Word导出成多个文件
  • Git工作流
  • 简单企业网站青岛天元建设集团网站
  • C#/.NET 微服务架构:从入门到精通的完整学习路线
  • 从 MySQL 过渡到 PostgreSQL 学习计划(暂定)
  • JAVA算法练习题day53
  • 在 C# .NETCore 中使用 RabbitMQ 实现发布、订阅示例
  • 【MySQL-笔记】数据库MySQL的安装与卸载
  • 网站没有域名wordpress修改鼠标
  • LeetCode 刷题【133. 克隆图】