一、Mock操作的含义和好处

  Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。

引入Mock最大的优势在于:Mock的行为固定,它确保当你访问该Mock的某个方法时总是能够获得一个没有任何逻辑的直接就返回的预期结果。

Mock Object的使用的好处:

  • 隔绝其他模块出错引起本模块的测试错误。
  • 隔绝其他模块的开发状态,只要定义好接口,不用管他们开发有没有完成。
  • 一些速度较慢的操作,可以用Mock Object代替,快速返回。
  • 对于分布式系统的测试,使用Mock Object会有另外两项很重要的收益。
  • 通过Mock Object可以将一些分布式测试转化为本地的测试。
  • 将Mock用于压力测试,可以解决测试集群无法模拟线上集群大规模下的压力。

二、Mock操作的应用场景

以下应用场景,适合使用Mock的:

  • 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
  • 真实对象很难被创建(比如具体的web容器)
  • 真实对象的某些行为很难触发(比如网络错误)
  • 真实情况令程序的运行速度很慢
  • 真实对象有用户界面
  • 测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
  • 真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)

 当然,也有一些不得不Mock的场景:

  • 一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
  • 执行操作的时间较长Object:有一些Object的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行Mock。
  • 异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过Mock可以人为的控制触发异常逻辑。

在一些压力测试的场景下,也不得不使用Mock,例如在分布式系统测试中,通常需要测试一些单点(如namenode,jobtracker)在压力场景下的工作是否正常。而通常测试集群在正常逻辑下无法提供足够的压力(主要原因是受限于机器数量),此时就需要应用Mock。

在这些场景下,我们应该如何去做Mock的工作了,一些现有的Mock工具可以帮助我们进行Mock工作。

三、Mock操作相关工具

  手动的构造 Mock 对象通常带来额外的编码量,而且这些为创建 Mock对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建 Mock对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。

通常Mock工具通过简单的方法对于给定的接口生成 Mock对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock对象返回指定的值或抛出指定异常。通过这些Mock工具我们可以方便的构造 Mock 对象从而使单元测试顺利进行,能够应用于更加复杂的测试场景。

以EasyMock为例,通过 EasyMock,我们可以为指定的接口动态的创建 Mock 对象,并利用Mock 对象来模拟协同模块,从而使单元测试顺利进行。这个过程大致可以划分为以下几个步骤:

  • 使用 EasyMock 生成 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的点。

四、根据需求选择恰当的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的例子。

五、mock与Junit和单元测试的区别

单元测试JUnit与Mock测试
单元测试一般只测试某一个功能,但是由于类之间的耦合性往往难以把功能隔离开来。例如你希望测试某个业务逻辑处理数据的功能,但是数据是从Database取回的,这就涉及到DAO层的类调用;你不希望单元测试函数去访问数据库(除非是测试DAO的单元测试),于是你希望有一个假的DAO类刚好返回你需要的测试数据。Mock的作用就是在单元测试里模拟类的行为和状态。

JUnit断言
junit.framework包下的Assert提供了多个断言方法. 主用于比较测试传递进去的两个参数.
Assert.assertEquals();及其重载方法:
1. 如果两者一致, 程序继续往下运行.
2. 如果两者不一致, 中断测试方法, 抛出异常信息 AssertionFailedError .

TDD--测试驱动开发(Test-Driven Development)
是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
TDD带来的好处
最重要的是提高了单元测试的覆盖率。 传统的先写产品代码,再写单元测试,有两个弊端:
    一方面是由于产品代码已经成型,生米已经煮成熟饭,你再来写,很容易就会陷入思维定式中,起不到发现Bug的作用;
    另一方面,这往往会让单元测试成为一项政治任务,产品发布前几天,发现单元测试覆盖率不足,来一场全员写测试用例的运动,这样写出来的代码质量肯定不高。

没有等出来的美丽,只有走出来的辉煌,越努力,越幸运,加油!