单元测试中Mock与Stub的浅析
本部分主要介绍所谓的Test Double的概念,并且对其中容易被混用的Mocks与Stubs的概念进行一个阐述。在初期接触到的时候,很多人会把Mock对象与另一个单元测试中经常用到的Stub对象搞混掉。为了方便更好地理解,这里把所有的所谓的Test Double的概念进行一个说明。我们先来看一个常用的单元测试的用例:
public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";
private MockControl warehouseControl;
private Warehouse warehouseMock;
public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehouseMock = (Warehouse) warehouseControl.getMock();
}
public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);
//setup - expectations
warehouseMock.hasInventory(TALISKER, 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER, 50);
warehouseControl.replay();
//exercise
order.fill(warehouseMock);
//verify
warehouseControl.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);
warehouseMock.hasInventory(TALISKER, 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();
order.fill((Warehouse) warehouseMock);
assertFalse(order.isFilled());
warehouseControl.verify();
}
}
当我们进行单元测试的时候,我们会专注于软件中的一个小点,不过问题就是虽然我们只想进行一个单一模块的测试,但是不得不依赖于其他模块,就好像上面例子中的warehouse。而在我提供的两种不同的测试用例的编写方案中,第一个是使用了真实的warehouse对象,而第二个使用了所谓的mock的warehouse对象,也意味着并不是一个真正的warehouse。使用Mock对象也是一种常用的在测试中避免依赖真正的对象的方法,不过像这种在测试中不使用真正对象的方法也有很多。
我们经常看到的类似的关联的名词会有:stub、mock、fake、dummy。本文中我是打算借鉴Gerard Meszaros的论述,可能并不是所有人都怎么描述,不过我觉得Gerard Meszaros说的不错。Gerard Meszaros是用Test Double
这个术语来称呼这一类用于替换真实对象的模拟对象。Gerard Meszaros具体定义了以下几类double:
-
Dummy : 用于传递给调用者但是永远不会被真实使用的对象,通常它们只是用来填满参数列表。
-
Fake : Fake对象常常与类的实现一起起作用,但是只是为了让其他程序能够正常运行,譬如内存数据库就是一个很好的例子。
-
Stubs : Stubs通常用于在测试中提供封装好的响应,譬如有时候编程设定的并不会对所有的调用都进行响应。Stubs也会记录下调用的记录,譬如一个email gateway就是一个很好的例子,它可以用来记录所有发送的信息或者它发送的信息的数目。简而言之,Stubs一般是对一个真实对象的封装。
-
Mocks : Mocks也就是Fowler这篇文章讨论的重点,即是针对设定好的调用方法与需要响应的参数封装出合适的对象。
在上述这几种doubles中,只有mocks强调行为验证,其他的一般都是强调状态验证。为了更好地描述这种区别,我们会对上面的例子进行一些扩展。一般在真实对象不太好交互或者代码还没有写好的时候,我们会选择使用一个测试的Double。譬如我们需要测试一个发送邮件的程序是不是能够在发送邮件的时候设定正确的顺序,而我们肯定不希望真的发邮件出去,这样会被打死的。因此我们会为我们的email系统来创建一个test double。这里也是用例子来展示mocks与stubs区别的地方:
public interface MailService {
public void send (Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
然后就可以进行状态验证了:
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
当然这是一个非常简单的测试,我们并没有测试它是否发给了正确的人或者发出了正确的内容。而如果使用Mock的话写法就很不一样了:
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
在两个例子中我们都是用了test double来替代真正的mail服务,不同的在于stub是用的状态验证而mock使用的是行为验证。如果要基于stub编写状态验证的方法,需要写一些额外的代码来进行验证。而Mock对象用的是行为验证,并不需要写太多的额外代码。
- Python时间获取及转换
- spark streaming知识总结[优化]
- 让你真正明白spark streaming
- Centos7 firewalld防火墙基本操作
- Spark Sql系统入门4:spark应用程序中使用spark sql
- Flume+Kafka收集Docker容器内分布式日志应用实践
- CentOS7搭建ELK日志集中分析平台
- Centos安装sshfs实现挂载目录
- shell脚本监控磁盘使用率
- Python使用MD5加密字符串
- Spark MLlib之 KMeans聚类算法详解
- Python时间与时间戳转换
- linux配置ssh互信实现免密登陆
- uva--1339 - Ancient Cipher(模拟水体系列)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Java自动化测试(jenkins 21)
- C#刷剑指Offer | 链表的倒数第k个节点
- 二叉树常见算法总结和C++实现
- Qt音视频开发15-mpv事件订阅
- MySQL案例:8.0统计信息不准确?
- Chaos Mesh® X GitHub Actions —— 把混沌工程集成到你的 CI 中
- 链表常见操作总结及C++实现
- 跳表原理及C++实现
- MySQL 8.0 新特性:WriteSet 复制
- leetcode: explore-array-30 有效的数独
- leetcode: explore-array-29 两数之和
- leetcode: explore-array-28 移动零
- leetcode: explore-array-27 加一
- leetcode: explore-array-26 列表取交集 II
- leetcode 找出唯一一个只出现一次的数字