Mock方法介绍

1 现有的单元测试框架
单元测试是保证程序正确性的一种有效的测试手段,对于不一样的开发语言,一般都能找到相应的单元框架。html



借助于这些单测框架的帮助,可以使得咱们编写单元测试用例的过程变得便捷而优雅。框架帮咱们提供了case的管理,执行,断言集,运行参数,全局事件工做,全部的这些使得咱们只需关注:于对于特定的输入,被测对象的返回是否正常。
那么,这些xUnit系列的单元测试框架是如何作到这些的了?分析这些框架,发现全部的单元测试框架都是基于如下的一种体系结构设计的。
 java



如上图所示,单测框架中一般包括TestRunner, Test, TestResult, TestCase, TestSuite, TestFixture六个组件。
TestRuner:负责驱动单元测试用例的执行,汇报测试执行的结果,从而简化测试
TestFixture:以测试套件的形式提供setUp()和tearDown()方法,保证两个test case之间的执行是相互独立,互不影响的。
TestResult:这个组件用于收集每一个test case的执行结果
Test:做为TestSuite和TestCase的父类暴露run()方法为TestRunner调用
TestCase:暴露给用户的一个类,用户经过继承TestCase,编写本身的测试用例逻辑
TestSuite:提供suite功能管理testCase
正由于类似的体系结构,因此大多数单元测试框架都提供了相似的功能和使用方法。那么在单测中引入单元测试框架会带来什么好处,在现有单元测试框架下还会存在什么样不能解决的问题呢?
2 单元测试框架的优势与一些问题
在单元测试中引入单测框架使得编写单测用例时,不须要再关注于如何驱动case的执行,如何收集结果,如何管理case集,只须要关注于如何写好单个测试用例便可;同时,在一些测试框架中经过提供丰富的断言集,公用方法,以及运行参数使得编写单个testcase的过程获得了最大的简化。
那这其中会存在什么样的疑问了?
我在单元测试框架中写一个TestCase,与我单独写一个cpp文件在main()方法里写测试代码有什么本质却别吗?用了单元测试框架,并无解决我在对复杂系统作单测时遇到的问题。
没错,对于单个case这二者从本质上说是没有区别的。单元测试框架自己并无告诉你如何去写TestCase,在这一点上他是没有提供任何帮助的。因此对于一些复杂的场景,只用单元测试框架是有点多少显得无能为力的。
使用单元测试框架每每适用于如下场景的测试:单个函数,一个class,或者几个功能相关class的测试,对于纯函数测试,接口级别的测试尤为适用,如房贷计算器公式的测试。
可是,对于一些复杂场景:
被测对象依赖复杂,甚至没法简单new出这个对象
 对于一些failure场景的测试
 被测对象中涉及多线程合做
 被测对象经过消息与外界交互的场景
 …
单纯依赖单测框架是没法实现单元测试的,而从某种意义上来讲,这些场景反而是测试中的重点。
以分布式系统的测试为例,class 与 function级别的单元测试对整个系统的帮助不大,固然,这种单元测试对单个程序的质量有帮助;分布式系统测试的要点是测试进程间的交互:一个进程收到客户请求,该如何处理,而后转发给其余进程;收到响应以后,又修改并应答客户;同时分布式系统测试中一般更关注一些异常路径的测试,这些场景才是测试中的重点,也是难点所在。
Mock方法的引入一般能帮助咱们解决以上场景中遇到的难题。
3 Mock的引入带来了什么
在维基百科上这样描述Mock:In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A computer programmer typically creates a mock object to test the behavior of some other object, in much the same way that a car designer uses a crash test dummy to simulate the dynamic behavior. of a human in vehicle impacts.
Mock一般是指,在测试一个对象A时,咱们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是咱们事先设定且符合预期。经过这些Mock对象来测试A在正常逻辑,异常逻辑或压力状况下工做是否正常。
引入Mock最大的优点在于:Mock的行为固定,它确保当你访问该Mock的某个方法时老是可以得到一个没有任何逻辑的直接就返回的预期结果。
Mock Object的使用一般会带来如下一些好处:
隔绝其余模块出错引发本模块的测试错误。
 隔绝其余模块的开发状态,只要定义好接口,不用管他们开发有没有完成。
 一些速度较慢的操做,能够用Mock Object代替,快速返回。
对于分布式系统的测试,使用Mock Object会有另外两项很重要的收益:
经过Mock Object能够将一些分布式测试转化为本地的测试
 将Mock用于压力测试,能够解决测试集群没法模拟线上集群大规模下的压力
4 Mock的应用场景
在使用Mock的过程当中,发现Mock是有一些通用性的,对于一些应用场景,是很是适合使用Mock的:
 真实对象具备不可肯定的行为(产生不可预测的结果,如股票的行情)
 真实对象很难被建立(好比具体的web容器)
 真实对象的某些行为很难触发(好比网络错误)
 真实状况令程序的运行速度很慢
 真实对象有用户界面
 测试须要询问真实对象它是如何被调用的(好比测试可能须要验证某个回调函数是否被调用了)
 真实对象实际上并不存在(当须要和其余开发小组,或者新的硬件系统打交道的时候,这是一个广泛的问题)
固然,也有一些不得不Mock的场景:
一些比较难构造的Object:这类Object一般有不少依赖,在单元测试中构造出这样类一般花费的成本太大。
 执行操做的时间较长Object:有一些Object的操做费时,而被测对象依赖于这一个操做的执行结果,例如大文件写操做,数据的更新等等,出于测试的需求,一般将这类操做进行Mock。
 异常逻辑:一些异常的逻辑每每在正常测试中是很难触发的,经过Mock能够人为的控制触发异常逻辑。
在一些压力测试的场景下,也不得不使用Mock,例如在分布式系统测试中,一般须要测试一些单点(如namenode,jobtracker)在压力场景下的工做是否正常。而一般测试集群在正常逻辑下没法提供足够的压力(主要缘由是受限于机器数量),这时候就须要应用Mock去知足。
在这些场景下,咱们应该如何去作Mock的工做了,一些现有的Mock工具能够帮助咱们进行Mock工做。
5 Mock工具的介绍
手动的构造 Mock 对象一般带来额外的编码量,并且这些为建立 Mock 对象而编写的代码颇有可能引入错误。目前,有许多开源项目对动态构建 Mock 对象提供了支持,这些项目可以根据现有的接口或类动态生成,这样不只能避免额外的编码工做,同时也下降了引入错误的可能。
C++: GoogleMock http://code.google.com/p/googlemock/node

Java: EasyMock http://easymock.org/web

一般Mock工具经过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,可以经过录制、回放、检查三步来完成大致的测试过程,能够验证方法的调用种类、次数、顺序,能够令 Mock 对象返回指定的值或抛出指定异常。经过这些Mock工具咱们能够方便的构造 Mock 对象从而使单元测试顺利进行,可以应用于更加复杂的测试场景。
以EasyMock为例,经过 EasyMock,咱们能够为指定的接口动态的建立 Mock 对象,并利用 Mock 对象来模拟协同模块,从而使单元测试顺利进行。这个过程大体能够划分为如下几个步骤:
使用 EasyMock 生成 Mock 对象
 设定 Mock 对象的预期行为和输出
 将 Mock 对象切换到 Replay 状态
 调用 Mock 对象方法进行单元测试
 对 Mock 对象的行为进行验证
EasyMock的使用和原理: http://www.ibm.com/developerworks/cn/opensource/os-cn-easymock/网络

EasyMock 后台处理的主要原理是利用 java.lang.reflect.Proxy 为指定的接口建立一个动态代理,这个动态代理,就是咱们在编码中用到的 Mock 对象。EasyMock 还为这个动态代理提供了一个 InvocationHandler 接口的实现,这个实现类的主要功能就是将动态代理的预期行为记录在某个映射表中和在实际调用时从这个映射表中取出预期输出。
借助相似于EasyMock这样工具,大大下降了编写Mock对象的成本,一般来讲Mock工具依赖于单元测试框架,为用户编写TestCase提供便利,可是自己依赖于单元测试框架去驱动,管理case,以及收集测试结果。例如EasyMock依赖于JUint,GoogleMock依赖于Gtest。
那么有了单元测试框架和相应的Mock工具就万事俱备了,还有什么样的问题?正如单元测试框架没有告诉你如何写TestCase同样,Mock工具也没有告诉你如何去选择Mock的点。
6 如何选择恰当的mock点
对于Mock这里存在两个误区,1.是Mock的对象越多越好;2.Mock会引入巨大的工做量,一般得不偿失。这都是源于不恰当的Mock点的选取。
这里说的如何选择恰当的mock点,是说对于一个被测对象,咱们应当在外围选择恰当的mock对象,以及须要mock的接口。由于对于任意一个对象,任意一段代码逻辑咱们都是有办法进行Mock的,而Mock点选择直接决定了咱们Mock的工做量以及测试效果。从另一种意义上来讲,不恰当Mock选择反而会对咱们的测试产生误导,从而在后期的集成和系统测试中引入更多的问题。
在mock点的选择过程当中,如下的一些点会是一些不错的选择
网络交互:若是两个被测模块之间是经过网络进行交互的,那么对于网络交互进行Mock一般是比较合适的,如RPC
 外部资源:好比文件系统、数据源,若是被测对象对此类外部资源依赖性很是强,而其行为的不可预测性极可能致使测试的随机失败,此类的外部资源也适合进行Mock。
 UI:由于UI不少时候都是用户行为触发事件,系统自己只是对这些触发事件进行相应,对这类UI作Mock,每每可以实现很好的收益,不少基于关键字驱动的框架都是基于UI进行Mock的
 第三方API:当接口属于使用者,经过Mock该接口来肯定测试使用者与接口的交互。
固然如何作Mock必定是与被系统的特性精密关联的,一些强制性的约束和规范是不合适的。这里介绍几个作的比较好的mock的例子。
1. 杀毒软件更新部署模块的Mock
这个例子源于一款杀毒产品的更新部署模块的测试。对于一个杀毒软件客户端而言,须要经过更新检查模块与病毒库Server进行交互,若是发现病毒库有更新则触发病毒库部署模块的最新病毒库的数据请求和部署工做,要求部署完成后杀毒软件客户端可以正常工做。
 数据结构



对于这一场景的测试,当时受限于这样一个条件,一般的病毒库server一般最多一天只更新一次病毒库,也就是说若是使用真实的病毒库server,那么针对更新部署模块的测试一天只能被触发一次。这是测试中所不能容忍的,经过对病毒库server进行mock能够解决这个问题。
对于这个场景能够采起这样一种Mock方式:用一个本地文件夹来模拟病毒库server,选择更新部署模块与病毒库server之间交互的两个函数checkVersion(),reqData()函数进行Mock。
checkVersion()工做原先的工做是检查病毒库Server的版本号,以决定是否触发更新,将其行为Mock为检查一个本地文件夹中病毒库的版本号;reqData()原有的行为是从病毒库Server拖取病毒库文件,将其Mock为从本地文件夹中拖取病毒库文件。经过这种方式咱们用一个本地文件夹Mock病毒库Server的行为,其带来的产出是:咱们能够随意的触发病毒库更新操做以及各类异常。经过这种方式发现了一个在更新部署过程当中,病毒库Server的病毒库版本发生改变形成出错的严重bug,这个是在原有一天才触发一次更新操做的状况下永远也没法发现的。
2. 分布式系统中对NameNode模块的测试
多线程



在测试NameNode模块的过程当中存在这样一个问题,在正常逻辑无压力条件下NameNode模块都是工做正常的。可是线上集群在大压力的状况下,是有可能触发NameNode的问题的。可是原有的测试方法下,咱们是没法对NameNode模拟大压力的场景的(由于NameNode的压力主要来源于DateNode数量,而咱们测试集群是远远没法达到线上几千台机器的规模的),而NameNode单点的性能瓶颈问题偏偏是测试的重点,真实的DataNode是没法知足测试需求的,咱们必须对DataNode进行Mock。
 app



如何对DateNode进行Mock了,最直观的想法是选择NameNode与DataNode之间的交互接口进行Mock,也就是他们之间的RPC交互,可是因为NameNode与DataNode之间的交互信息种类不少,因此其实这并非一种很好的选择。
换个角度来想,NameNode之上的压力是源于对HDFS的读写操做形成的NameNode上元数据的维护,也就是说,对于NameNode而言,其实他并不关心数据到底写到哪里去了,只关心数据是否读写成功。若是是这种场景Mock就能够变的简单了,咱们能够直接将DataNode上对块的操做进行mock,好比,对一次写请求,DataNode并不触发真实的写操做,而直接返回成功。经过这种方式,DataNode去除了执行功能,只保留了消息交互功能,间接的实现了咱们的测试需求,且工做量比之第一种方案小不少。
3. 开源社区提供的MRUnit测试框架
在原有框架下,对于MapReduce程序的测试一般是没法在本地验证的,更不用说对MapReduce程序进行单测了。而MRUnit经过一个简单而优雅的Mock,却实现了一个基于MapReduce程序的单测框架。框架

基于MRUINT框架能够将单测写成以下形式:分布式



在这个框架中定义了MapDriver,ReducerDriver,MapReduceDriver三个有点相似容器的driver,经过driver来驱动map,reduce或者整个mapreduce过程的执行。
如上例,在driver中设定mapper为IdentityMapper,经过withInput方法设定输入数据,经过withOutput方法设定预期结果,经过runTest方法来触发执行并进行结果检测
他的实现原理是将outputCollector作Mock,outputCollectort中的emit方法实现的逻辑是将数据写到文件系统中,Mock后是经过另一个进程去收集数据并保存在内存中,从而实现最终结果的可检验(在本身的数据结构中比对结果)。
实现的原理很简单,这样作mock就会精巧,只选择最底层的一些简单却又依赖普遍的点(依赖普遍指模块间的数据流一般都走这样的点过)作mock,这样一般效果很好且简单
固然这个例子中也有一些缺陷:1.由于在outputcollector层作mock的数据截取,使得没法过partition的分桶逻辑;2.这个框架是写内存的,没法最终改为压力性能测试工具。
 

7 附录
1. EasyMock示例:


2. A Brief History of Mock Objects
http://ecmp.baidu.com/page/site/dmsqa/document-details?nodeRef=workspace://SpacesStore/c4e4bd14-aa79-417b-b18a-6502141bb3be&cursor=0&showFolders=all
3. http://www.mockobjects.com/(需×××)

 

本文首发于:百度测试技术空间http://hi.baidu.com/baiduqa/blog/item/44a4a6f613b8d7f67609d753.html

相关文章
相关标签/搜索