这篇文章简介一下,如何通过 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

  1. 效益:顾客入场时,帮助店员统计出门票收入,确认是否核帐正确
  2. 角色:Pub 店员
  3. 目的:根据顾客与相关条件,算出对应的门票收入总值
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 相当简单,步骤如下:

  1. 通过 MockRepository.GenerateStub<T>(),来建立某一个 T 类型的 stub object,以上面例子来说,是建立实现 ICheckInFee 接口的子类。
  2. 把该 stub object 通过构造函数,传给测试目标对象。
  3. 定义当调用到该 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)
02 {
03     var result = 0;
04  
05     foreach (var customer in customers)
06     {
07         var isFemale = !customer.IsMale;
08         //for fake
09         var isLadyNight = DateTime.Today.DayOfWeek == DayOfWeek.Friday;
10         //禮拜五女生免費入場
11         if (isLadyNight && isFemale)
12         {
13             continue;
14         }
15         else
16         {
17             //for stub, validate status: income value
18             //for mock, validate only male
19             this._inCome += this._checkInFee.GetFee(customer);
20  
21             result++;
22         }
23     }
24  
25     //for stub, validate return value
26     return result;
27 }

碰到 DateTime.Today 这类东西,测试案例就会卡住。总不能每次测试都去改测试机上面的日期,或是只有星期五或非星期五才执行某些测试吧。

所以,我们得透过 Isolation framework 来辅助,针对使用到的组件,建立 fake object。

首先,因为这个例子建立的 fake object,是针对 System.DateTime,所以在测试项目上,针对System.dll来新增 Fake 组件,如下图所示:

可以看到增加了一个 Fakes 的 folder,其中会针对要 fake 的 dll,产生对应的程序代码,以便我们进行拦截与改写。

使用 fake 对象也相当简单,先以测试星期五为例,程序代码如下:

01 [TestMethod]
02 public void Test_Friday_Charge_Customer_Count()
03 {
04     using (ShimsContext.Create())
05     {
06         System.Fakes.ShimDateTime.TodayGet = () =>
07             {
08                 //2012/10/19為Friday
09                 return new DateTime(2012, 10, 19);
10             };
11  
12         //arrange
13         ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
14         Pub target = new Pub(stubCheckInFee);
15  
16         stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
17  
18         var customers = new List<Customer>
19         {
20             new Customer{ IsMale=true},
21             new Customer{ IsMale=false},
22             new Customer{ IsMale=false},
23         };
24  
25         decimal expected = 1;
26  
27         //act
28         var actual = target.CheckIn(customers);
29  
30         //assert
31         Assert.AreEqual(expected, actual);
32     }
33 }

说明如下:

  1. using (ShimsContext.Create()){} 的范围中,会使用 Fake 组件。
  2. 当在 fake context 环境下,呼叫到 System.DateTime.Today 时,会转呼叫 System.Fakes.ShimDateTime.TodayGet,并定义其回传值为「2012/10/19」,因为这一天是星期五。

接着就跟原本的测试程序代码一样,当星期五时,只对男生收费。

侦错时,可以看到 DateTime.Today 变成我们仿真的「2012/10/19」,但实际系统日期是「2012/10/15」。

再增加一个星期六的测试案例,程序代码如下:

01 [TestMethod]
02 public void Test_Saturday_Charge_Customer_Count()
03 {
04      
05     using (ShimsContext.Create())
06     {
07         System.Fakes.ShimDateTime.TodayGet = () =>
08         {
09             //2012/10/20為Saturday
10             return new DateTime(2012, 10, 20);
11         };
12  
13         //arrange
14         ICheckInFee stubCheckInFee = MockRepository.GenerateStub<ICheckInFee>();
15         Pub target = new Pub(stubCheckInFee);
16  
17         stubCheckInFee.Stub(x => x.GetFee(Arg<Customer>.Is.Anything)).Return(100);
18  
19         var customers = new List<Customer>
20         {
21             new Customer{ IsMale=true},
22             new Customer{ IsMale=false},
23             new Customer{ IsMale=false},
24         };
25  
26         decimal expected = 3;
27  
28         //act
29         var actual = target.CheckIn(customers);
30  
31         //assert
32         Assert.AreEqual(expected, actual);
33     }
34 }

因为是星期六,所以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 原生组件。

也请读者朋友务必记得几个基本原则:

  1. 同一测试案例中,请避免 stub 与 mock 在同一个案例一起验证。原因就如同一直在强调的单元测试准则,一次只验证一件事。而 stub 与 mock 的用途本就不同,stub 是用来辅助验证回传值或目标对象状态,而 mock 是用来验证目标对象与相依对象互动的情况是否符合预期。既然八竿子打不着,又怎么会在同一个测试案例中,验证这两个完全不同的情况呢?
  2. Mock 的验证可以相当复杂,但越复杂代表维护成本越高,代表越容易因为需求异动而改变。所以,请谨慎使用 mock,更甚至于当发生问题时,针对问题的测试案例才增加 mock 的测试,笔者都认为是合情合理的。
  3. 当要测试一个目标对象,要 stub/mock/fake 的 object 太多时,请务必思考目标对象的设计是否出现问题,是否与太多细节耦合,是否可将这些细节职责合并。
  4. 当测试程序写的一狗票落落长时,请确认目标对象的职责是否太肥,或是方法内容太长。这都是因为目标对象设计不良,导致测试程序不容易撰写或维护的情况。问题根源在目标对象的设计质量。
  5. 将测试程序当作 production code 的一部份,production code 中不该出现的坏味道,一样不该出现在测试程序中,尤其是重复的程序代码。所以测试程序,基本上也需要进行重构。但也请务必提醒自己,测试程序基本上不会包含逻辑,因为包含了逻辑,您就应该再写一段测试程序,来测这个测试程序是否符合预期

TDD学习笔记【六】一Unit Test - Stub, Mock, Fake 简介的更多相关文章

  1. java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

    java之jvm学习笔记六(实践写自己的安全管理器) 安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessCo ...

  2. Learning ROS for Robotics Programming Second Edition学习笔记(六) indigo xtion pro live

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  3. Typescript 学习笔记六:接口

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  4. python3.4学习笔记(六) 常用快捷键使用技巧,持续更新

    python3.4学习笔记(六) 常用快捷键使用技巧,持续更新 安装IDLE后鼠标右键点击*.py 文件,可以看到Edit with IDLE 选择这个可以直接打开编辑器.IDLE默认不能显示行号,使 ...

  5. Go语言学习笔记六: 循环语句

    Go语言学习笔记六: 循环语句 今天学了一个格式化代码的命令:gofmt -w chapter6.go for循环 for循环有3种形式: for init; condition; increment ...

  6. 【opencv学习笔记六】图像的ROI区域选择与复制

    图像的数据量还是比较大的,对整张图片进行处理会影响我们的处理效率,因此常常只对图像中我们需要的部分进行处理,也就是感兴趣区域ROI.今天我们来看一下如何设置图像的感兴趣区域ROI.以及对ROI区域图像 ...

  7. Linux学习笔记(六) 进程管理

    1.进程基础 当输入一个命令时,shell 会同时启动一个进程,这种任务与进程分离的方式是 Linux 系统上重要的概念 每个执行的任务都称为进程,在每个进程启动时,系统都会给它指定一个唯一的 ID, ...

  8. # go微服务框架kratos学习笔记六(kratos 服务发现 discovery)

    目录 go微服务框架kratos学习笔记六(kratos 服务发现 discovery) http api register 服务注册 fetch 获取实例 fetchs 批量获取实例 polls 批 ...

  9. Spring Boot 学习笔记(六) 整合 RESTful 参数传递

    Spring Boot 学习笔记 源码地址 Spring Boot 学习笔记(一) hello world Spring Boot 学习笔记(二) 整合 log4j2 Spring Boot 学习笔记 ...

随机推荐

  1. 哈希表(散列表),Hash表漫谈

    1.序 该篇分别讲了散列表的引出.散列函数的设计.处理冲突的方法.并给出一段简单的示例代码. 2.散列表的引出 给定一个关键字集合U={0,1......m-1},总共有不大于m个元素.如果m不是很大 ...

  2. 基于Python Pillow库生成随机验证码

    from PIL import Image from PIL import ImageDraw from PIL import ImageFont import random class ValidC ...

  3. Linux之ssh登录

    作业三:ssh登录,scp上传.下载,ssh秘钥登录,修改ssh server端的端口为8888然后进行登录和scp测试 1.ssh登录 [root@localhost network-scripts ...

  4. RAID各种级别详细介绍

    独立硬盘冗余阵列(RAID, Redundant Array of Independent Disks),旧称廉价磁盘冗余阵列(RAID, Redundant Array of Inexpensive ...

  5. PHP02

    PHP02 1.虚拟主机配置完毕后,机器上的ip和localhost都会默认直接请求第一个虚拟主机 2.解析文本文件显示表格 将文本文件中的数据呈现在一个表格中 1)读取文件内容 包含文本的字符串数据 ...

  6. JAVA自学笔记23

    JAVA自学笔记23 1.多线程 1)引入: 2)进程 是正在运行的程序.是系统进行资源分配和调用的独立单位.每一个进程都有它自己的内存空间和系统资源. 多进程: 单进程的计算机只能做一件事情,而现在 ...

  7. poi 升级至4.x 的问题总结(POI Excel 单元格内容类型判断并取值)

    POI Excel 单元格内容类型判断并取值 以前用 cell.getCachedFormulaResultType() 得到 type 升级到4后获取不到了 换为:cell.getCellType( ...

  8. Linux DNS 查询剖析(第四部分) | Linux 中国

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/F8qG7f9YD02Pe/article/details/82879414 在第四部分中,我将介绍容 ...

  9. 洛谷P1048 采药

    题目OJ地址 https://www.luogu.org/problemnew/show/P1048 https://vijos.org/p/1104 题目描述辰辰是个天资聪颖的孩子,他的梦想是成为世 ...

  10. 读吴恩达算-EM算法笔记

    最近感觉对EM算法有一点遗忘,在表述的时候,还是有一点说不清,于是重新去看了这篇<CS229 Lecture notes>笔记. 于是有了这篇小札. 关于Jensen's inequali ...