这篇文章简介一下,如何通过 mock framework,来辅助我们更便利地模拟目标对象的依赖对象,而不必手工敲堆只为了这次测试而存在的辅助类型。
而模拟目标对象的部分,常见的有 stub object, mock object, fake object,本文也会简单介绍一下三者的不同点,并且通过实例,帮助读者快速的 pick up 实战经验。
安装与范例说明
本文的范例,使用 VS2013 为开发工具,mock framework 则是使用 Rhino.Mocks,通过 IoC 的方式,由构造函数来传入 stub/mock/fake object。
注:在 Microsoft Fakes 里面也有内建的 stub object,但是是类似 fake object 的方式产生,而非 Rhino.Mocks, moq 这类 mock framework 是使用动态产生 stub/mock object的方式。Isolating Code under Test with Microsoft Fakes
- 效益:顾客入场时,帮助店员统计出门票收入,确认是否核帐正确
- 角色:Pub 店员
- 目的:根据顾客与相关条件,算出对应的门票收入总值
public interface ICheckInFee
{
decimal GetFee(Customer customer);
}
public class Customer
{
public bool IsMale { get; set; }
public int Seq { get; set; }
}
public class Pub
{
private readonly ICheckInFee _checkInFee;
private decimal _inCome;
public Pub(ICheckInFee checkInFee)
{
this._checkInFee = checkInFee;
}
/// <summary>
/// 入场
/// </summary>
/// <param name="customers"></param>
/// <returns>收费的人数</returns>
public int CheckIn(List<Customer> customers)
{
var result = ;
foreach (var customer in customers)
{
var isFemale = !customer.IsMale;
//女生免费入场
if (isFemale)
{
continue;
}
else
{
//for stub, validate status: income value
//for mock, validate only male
this._inCome += this._checkInFee.GetFee(customer);
result++;
}
}
//for stub, validate return value
return result;
}
public decimal GetInCome()
{
return this._inCome;
}
}
CheckIn 说明:
当顾客进场时,如果是女生,则免费入场。若为男生,则根据 ICheckInFee 接口来取得门票的费用,并累计到 inCome 中。通过 GetInCome()
方法取得这一次的门票收入总金额。
Stub
Stub 通常使用在验证目标回传值,以及验证目标对象状态的改变。
这两种验证方式的重点,都摆在目标对象自身的逻辑。
即测试目标对象时,并不在乎目标对象与外部依赖对象如何互动,关注在当外部相依对象回传什么样的数据时,会导致目标对象内部的状态或逻辑变化。
所以这类的验证方式,是通过 stub object 直接模拟外部依赖回传的数据,来验证目标对象行为是否如同预期。
范例:
第一个测试,是验证收费人数是否符合预期,代码如下:
[TestMethod]
public void Test_Charge_Customer_Count()
{
//Arrange
var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
var target = new Pub(stubCheckInFee);
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = false},
new Customer {IsMale = false},
};
decimal expected = ;
//Act
var actual = target.CheckIn(customers);
//Assert
Assert.AreEqual(expected, actual);
}
使用 Rhino.Mocks 相当简单,步骤如下:
- 通过
MockRepository.GenerateStub<T>()
,来建立某一个 T 类型的 stub object,以上面例子来说,是建立实现 ICheckInFee 接口的子类。
- 把该 stub object 通过构造函数,传给测试目标对象。
- 定义当调用到该 stub object 的哪一个方法时(例子中是GetFee方法),传入的参数是什么, stub 要回传是什么。
通过 Rhino.Mocks,就这么简单地通过 Lambda 的方式定义 stub object 的行为,取代了原本要自己建立一个实体类型,并实现ICheckInFee 接口,定义 GetFee 要回传的值。
上面的测试案例,是入场顾客人数3人,一男两女,因为目前 Pub 的 CheckIn 方法,只针对男生收费,所以回传收费人数应为1人。
第二个测试,则是验证收费的总数,是否符合预期。测试案例一样是一男两女,而通过 stub object模拟每一人收费为100元,所以预期结果门票收入总数为100。测试程序如下:
[TestMethod]
public void Test_Income()
{
//Arrange
var stubCheckInFee = MockRepository.GenerateMock<ICheckInFee>();
var target = new Pub(stubCheckInFee);
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return();
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = false},
new Customer {IsMale = false},
};
//Act
decimal inComeBeforeCheckIn = target.GetInCome();
//Assert
Assert.AreEqual(, inComeBeforeCheckIn);
decimal expectedIncome = ;
//Act
int chargeCustomerCount = target.CheckIn(customers);
var actualIncome = target.GetInCome();
//Assert
Assert.AreEqual(expectedIncome, actualIncome);
}
可以看到这里有两个 Assert,因为我们这里是验证状态的改变,期望在调用目标对象的 CheckIn 方法之前,取得的门票收入应为0。而调用之后,依照这个测试案例,门票收入应为100。
通过这两个测试案例,其实实际要测试的部分是,CheckIn 的方法只针对男生收费这一段逻辑。不管实际 production code,门票一人收费多少,都不会影响到这一份商业逻辑。
怎么根据环境或顾客来进行计价,那是在 production code 中,实现 ICheckInFee 接口的子类,要自己进行测试的,与 Pub 对象无关。这样一来,才能隔离 ICheckInFee 背后的变化。
Mock
使用时机:
上面提到验证对象的第三种方式:「验证目标对象与外部依赖接口的互动方式」,如下图所示:
这听起来可能相当抽象,但在开发中,的确可能会碰到这样的测试需求。
Mock 的验证比起 stub 要复杂许多,变动性通常也会大一点,但往往在验证一些 void 的行为会使用到,例如:在某个条件发生时,要记录 Log。这种情境,用 stub 就很难验证,因为对目标对象来说,没有回传值,也没有状态变化,就只能透过 mock object 来验证,目标对象是否正确的与Log 接口进行互动。
范例:
以这个范例来说,我们想验证的是:在2男1女的测试案例中,是否只呼叫 ICheckInFee 接口两次。程序代码如下:
[TestMethod]
public void Test_CheckIn_Charge_Only_Male()
{
//Arrange
//两男一女
var customers = new List<Customer>
{
new Customer {IsMale = true},
new Customer {IsMale = true},
new Customer {IsMale = false},
};
MockRepository mock=new MockRepository();
ICheckInFee stubCheckInFee = mock.StrictMock<ICheckInFee>();
using (mock.Record())
{
//期望调用ICheckInFee的GetFee()次数为2
//Assert
stubCheckInFee.GetFee(customers.ElementAt());
LastCall.IgnoreArguments()
.Return((decimal) )
.Repeat.Times();
}
using (mock.Playback())
{
var target = new Pub(stubCheckInFee);
//Act
target.CheckIn(customers);
}
}
Fake
使用时机:
当目标对象使用到静态方法,或 .net framework 本身的对象,甚至于针对一般直接相依的对象,我们都可以透过 fake object 的方式,直接仿真相依对象的行为。
范例:
以这例子来说,假设 CheckIn 的需求改变,从原本的「女生免费入场」变成「只有当天为星期五,女生才免费入场」,修改程序代码如下:
01 |
public int CheckIn(List<Customer> customers) |
05 |
foreach (var customer in customers) |
07 |
var isFemale = !customer.IsMale; |
09 |
var isLadyNight = DateTime.Today.DayOfWeek == DayOfWeek.Friday; |
11 |
if (isLadyNight && isFemale) |
17 |
//for stub, validate status: income value |
18 |
//for mock, validate only male |
19 |
this ._inCome += this ._checkInFee.GetFee(customer); |
25 |
//for stub, validate return value |
碰到 DateTime.Today
这类东西,测试案例就会卡住。总不能每次测试都去改测试机上面的日期,或是只有星期五或非星期五才执行某些测试吧。
所以,我们得透过 Isolation framework 来辅助,针对使用到的组件,建立 fake object。
首先,因为这个例子建立的 fake object,是针对 System.DateTime
,所以在测试项目上,针对System.dll来新增 Fake 组件,如下图所示:
可以看到增加了一个 Fakes 的 folder,其中会针对要 fake 的 dll,产生对应的程序代码,以便我们进行拦截与改写。
使用 fake 对象也相当简单,先以测试星期五为例,程序代码如下:
02 |
public void Test_Friday_Charge_Customer_Count() |
04 |
using (ShimsContext.Create()) |
06 |
System.Fakes.ShimDateTime.TodayGet = () => |
09 |
return new DateTime(2012, 10, 19); |
13 |
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>(); |
14 |
Pub target = new Pub(stubCheckInFee); |
16 |
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100); |
18 |
var customers = new List<Customer> |
20 |
new Customer{ IsMale= true }, |
21 |
new Customer{ IsMale= false }, |
22 |
new Customer{ IsMale= false }, |
28 |
var actual = target.CheckIn(customers); |
31 |
Assert.AreEqual(expected, actual); |
说明如下:
- 在
using (ShimsContext.Create()){}
的范围中,会使用 Fake 组件。
- 当在 fake context 环境下,呼叫到
System.DateTime.Today
时,会转呼叫 System.Fakes.ShimDateTime.TodayGet
,并定义其回传值为「2012/10/19」,因为这一天是星期五。
接着就跟原本的测试程序代码一样,当星期五时,只对男生收费。
侦错时,可以看到 DateTime.Today
变成我们仿真的「2012/10/19」,但实际系统日期是「2012/10/15」。
再增加一个星期六的测试案例,程序代码如下:
02 |
public void Test_Saturday_Charge_Customer_Count() |
05 |
using (ShimsContext.Create()) |
07 |
System.Fakes.ShimDateTime.TodayGet = () => |
10 |
return new DateTime(2012, 10, 20); |
14 |
ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>(); |
15 |
Pub target = new Pub(stubCheckInFee); |
17 |
stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100); |
19 |
var customers = new List<Customer> |
21 |
new Customer{ IsMale= true }, |
22 |
new Customer{ IsMale= false }, |
23 |
new Customer{ IsMale= false }, |
29 |
var actual = target.CheckIn(customers); |
32 |
Assert.AreEqual(expected, actual); |
因为是星期六,所以1男2女,收费人数为3人。
补充:
连 System.dll 都可以进行 fake object 仿真了,所以即使是我们自定义,直接相依,也可以透过这种方式来仿真。
这样一来,即便是直接相依的对象,也可以进行独立测试了。
但强烈建议,针对自定义对象的部分,这是黑魔法类型的作法,如果没有包袱,建议对象设计还是要采 IoC 方式设计。如果是 legacy code,想要进行重构,摆脱直接相依的问题,则可先透过 fake object 来建立单元测试,接下来进行重构,重构后当对象不直接相依时,再改用上面的 stub/mock 方式来进行测试。
可以参考这篇在 Martin Fowler 网站上的文章:Modern Mocking Tools and Black Magic
注:即使不是在VS2012的环境底下,也可以到 Microsoft Research 上 download Moles: Moles - Isolation framework for .NET使用
结论
今天这篇文章介绍了 stub, mock 与 fake 的用法,但依笔者实际经验,使用 stub 的比例大概是8~9成,使用mock的比例大概仅1~2成。而 fake 的方式,则用在特例,例如静态方法跟 .net framework 原生组件。
也请读者朋友务必记得几个基本原则:
- 同一测试案例中,请避免 stub 与 mock 在同一个案例一起验证。原因就如同一直在强调的单元测试准则,一次只验证一件事。而 stub 与 mock 的用途本就不同,stub 是用来辅助验证回传值或目标对象状态,而 mock 是用来验证目标对象与相依对象互动的情况是否符合预期。既然八竿子打不着,又怎么会在同一个测试案例中,验证这两个完全不同的情况呢?
- Mock 的验证可以相当复杂,但越复杂代表维护成本越高,代表越容易因为需求异动而改变。所以,请谨慎使用 mock,更甚至于当发生问题时,针对问题的测试案例才增加 mock 的测试,笔者都认为是合情合理的。
- 当要测试一个目标对象,要 stub/mock/fake 的 object 太多时,请务必思考目标对象的设计是否出现问题,是否与太多细节耦合,是否可将这些细节职责合并。
- 当测试程序写的一狗票落落长时,请确认目标对象的职责是否太肥,或是方法内容太长。这都是因为目标对象设计不良,导致测试程序不容易撰写或维护的情况。问题根源在目标对象的设计质量。
- 请将测试程序当作 production code 的一部份,production code 中不该出现的坏味道,一样不该出现在测试程序中,尤其是重复的程序代码。所以测试程序,基本上也需要进行重构。但也请务必提醒自己,测试程序基本上不会包含逻辑,因为包含了逻辑,您就应该再写一段测试程序,来测这个测试程序是否符合预期。
- java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...
- Learning ROS for Robotics Programming Second Edition学习笔记(六) indigo xtion pro live
中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...
- Typescript 学习笔记六:接口
中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...
- python3.4学习笔记(六) 常用快捷键使用技巧,持续更新
python3.4学习笔记(六) 常用快捷键使用技巧,持续更新 安装IDLE后鼠标右键点击*.py 文件,可以看到Edit with IDLE 选择这个可以直接打开编辑器.IDLE默认不能显示行号,使 ...
- Go语言学习笔记六: 循环语句
Go语言学习笔记六: 循环语句 今天学了一个格式化代码的命令:gofmt -w chapter6.go for循环 for循环有3种形式: for init; condition; increment ...
- 【opencv学习笔记六】图像的ROI区域选择与复制
图像的数据量还是比较大的,对整张图片进行处理会影响我们的处理效率,因此常常只对图像中我们需要的部分进行处理,也就是感兴趣区域ROI.今天我们来看一下如何设置图像的感兴趣区域ROI.以及对ROI区域图像 ...
- Linux学习笔记(六) 进程管理
1.进程基础 当输入一个命令时,shell 会同时启动一个进程,这种任务与进程分离的方式是 Linux 系统上重要的概念 每个执行的任务都称为进程,在每个进程启动时,系统都会给它指定一个唯一的 ID, ...
- # go微服务框架kratos学习笔记六(kratos 服务发现 discovery)
目录 go微服务框架kratos学习笔记六(kratos 服务发现 discovery) http api register 服务注册 fetch 获取实例 fetchs 批量获取实例 polls 批 ...
- Spring Boot 学习笔记(六) 整合 RESTful 参数传递
Spring Boot 学习笔记 源码地址 Spring Boot 学习笔记(一) hello world Spring Boot 学习笔记(二) 整合 log4j2 Spring Boot 学习笔记 ...
随机推荐
- 哈希表(散列表),Hash表漫谈
1.序 该篇分别讲了散列表的引出.散列函数的设计.处理冲突的方法.并给出一段简单的示例代码. 2.散列表的引出 给定一个关键字集合U={0,1......m-1},总共有不大于m个元素.如果m不是很大 ...
- 基于Python Pillow库生成随机验证码
from PIL import Image from PIL import ImageDraw from PIL import ImageFont import random class ValidC ...
- Linux之ssh登录
作业三:ssh登录,scp上传.下载,ssh秘钥登录,修改ssh server端的端口为8888然后进行登录和scp测试 1.ssh登录 [root@localhost network-scripts ...
- RAID各种级别详细介绍
独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),旧称廉价磁盘冗余阵列(RAID, Redundant Array of Inexpensive ...
- PHP02
PHP02 1.虚拟主机配置完毕后,机器上的ip和localhost都会默认直接请求第一个虚拟主机 2.解析文本文件显示表格 将文本文件中的数据呈现在一个表格中 1)读取文件内容 包含文本的字符串数据 ...
- JAVA自学笔记23
JAVA自学笔记23 1.多线程 1)引入: 2)进程 是正在运行的程序.是系统进行资源分配和调用的独立单位.每一个进程都有它自己的内存空间和系统资源. 多进程: 单进程的计算机只能做一件事情,而现在 ...
- poi 升级至4.x 的问题总结(POI Excel 单元格内容类型判断并取值)
POI Excel 单元格内容类型判断并取值 以前用 cell.getCachedFormulaResultType() 得到 type 升级到4后获取不到了 换为:cell.getCellType( ...
- Linux DNS 查询剖析(第四部分) | Linux 中国
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/F8qG7f9YD02Pe/article/details/82879414 在第四部分中,我将介绍容 ...
- 洛谷P1048 采药
题目OJ地址 https://www.luogu.org/problemnew/show/P1048 https://vijos.org/p/1104 题目描述辰辰是个天资聪颖的孩子,他的梦想是成为世 ...
- 读吴恩达算-EM算法笔记
最近感觉对EM算法有一点遗忘,在表述的时候,还是有一点说不清,于是重新去看了这篇<CS229 Lecture notes>笔记. 于是有了这篇小札. 关于Jensen's inequali ...