Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。
mock测试介绍
单元测试是保证程序正确性的一种有效的测试手段,对于不同的开发语言,通常都能找到相应的单元框架。
框架帮我们提供了case的管理,执行,断言集,运行参数,全局事件工作,所有的这些使得我们只需关注:于对于特定的输入,被测对象的返回是否正常。
那么,这些xUnit系列的单元测试框架是如何做到这些的了?分析这些框架,发现所有的单元测试框架都是基于以下的一种体系结构设计的。
如上图所示,单测框架中通常包括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
正因为相似的体系结构,所以大多数单元测试框架都提供了类似的功能和使用方法。
单元测试框架的一些问题
我在单元测试框架中写一个TestCase,与我单独写一个cpp文件在main()方法里写测试代码有什么本质却别吗?用了单元测试框架,并没有解决我在对复杂系统做单测时遇到的问题。
没错,对于单个case这两者从本质上说是没有区别的。单元测试框架本身并没有告诉你如何去写TestCase,在这一点上他是没有提供任何帮助的。所以对于一些复杂的场景,只用单元测试框架是有点多少显得无能为力的。
使用单元测试框架往往适用于以下场景的测试:单个函数,一个class,或者几个功能相关class的测试,对于纯函数测试,接口级别的测试尤其适用,如房贷计算器公式的测试。
但是,对于一些复杂场景:
被测对象依赖复杂,甚至无法简单new出这个对象
对于一些failure场景的测试
被测对象中涉及多线程合作
被测对象通过消息与外界交互的场景
…
单纯依赖单测框架是无法实现单元测试的,而从某种意义上来说,这些场景反而是测试中的重点。
以分布式系统的测试为例,class 与 function级别的单元测试对整个系统的帮助不大,当然,这种单元测试对单个程序的质量有帮助;分布式系统测试的要点是测试进程间的交互:一个进程收到客户请求,该如何处理,然后转发给其他进程;收到响应之后,又修改并应答客户;同时分布式系统测试中通常更关注一些异常路径的测试,这些场景才是测试中的重点,也是难点所在。
Mock方法的引入通常能帮助我们解决以上场景中遇到的难题。
Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。
引入Mock最大的优势在于:Mock的行为固定,它确保当你访问该Mock的某个方法时总是能够获得一个没有任何逻辑的直接就返回的预期结果。
Mock Object的使用通常会带来以下一些好处:
- 隔绝其他模块出错引起本模块的测试错误。
- 隔绝其他模块的开发状态,只要定义好接口,不用管他们开发有没有完成。
- 一些速度较慢的操作,可以用Mock Object代替,快速返回。
对于分布式系统的测试,使用Mock Object会有另外两项很重要的收益: - 通过Mock Object可以将一些分布式测试转化为本地的测试
- 将Mock用于压力测试,可以解决测试集群无法模拟线上集群大规模下的压力
Mock的应用场景
在使用Mock的过程中,发现Mock是有一些通用性的,对于一些应用场景,是非常适合使用Mock的:
- 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
- 真实对象很难被创建(比如具体的web容器)
- 真实对象的某些行为很难触发(比如网络错误)
- 真实情况令程序的运行速度很慢- 真实对象有用户界面
测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)
当然,也有一些不得不Mock的场景:
一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
执行操作的时间较长Object:有一些Object的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行Mock。
异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过Mock可以人为的控制触发异常逻辑。
在一些压力测试的场景下,也不得不使用Mock,例如在分布式系统测试中,通常需要测试一些单点(如namenode,jobtracker)在压力场景下的工作是否正常。而通常测试集群在正常逻辑下无法提供足够的压力(主要原因是受限于机器数量),这时候就需要应用Mock去满足。
5 Mock工具的介绍
手动的构造 Mock 对象通常带来额外的编码量,而且这些为创建 Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。
C++: GoogleMock http://code.google.com/p/googlemock/
Java: EasyMock http://easymock.org/
通常Mock工具通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过这些Mock工具我们可以方便的构造 Mock 对象从而使单元测试顺利进行,能够应用于更加复杂的测试场景。以EasyMock为例,通过 EasyMock,我们可以为指定的接口动态的创建 Mock 对象,并利用 Mock 对象来模拟协同模块,从而使单元测试顺利进行。这个过程大致可以划分为以下几个步骤:
使用 EasyMock 生成 Mock 对象
设定 Mock 对象的预期行为和输出
将 Mock 对象切换到 Replay 状态
调用 Mock 对象方法进行单元测试
对 Mock 对象的行为进行验证
使用 EasyMock 生成 Mock 对象
根据指定的接口或类,EasyMock 能够动态的创建 Mock 对象(EasyMock 默认只支持为接口生成 Mock 对象,如果需要为类生成 Mock 对象,在 EasyMock 的主页上有扩展包可以实现此功能),我们以 ResultSet 接口为例说明EasyMock的功能。java.sql.ResultSet 是每一个 Java 开发人员都非常熟悉的接口:
ResultSet 接口
public interface java.sql.ResultSet {
......
public abstract java.lang.String getString(int arg0) throws java.sql.SQLException;
public abstract double getDouble(int arg0) throws java.sql.SQLException;
......
}
通常,构建一个真实的 RecordSet 对象需要经过一个复杂的过程:在开发过程中,开发人员通常会编写一个 DBUtility 类来获取数据库连接 Connection,并利用 Connection 创建一个 Statement。执行一个 Statement 可以获取到一个或多个 ResultSet 对象。这样的构造过程复杂并且依赖于数据库的正确运行。数据库或是数据库交互模块出现问题,都会影响单元测试的结果。
我们可以使用 EasyMock 动态构建 ResultSet 接口的 Mock 对象来解决这个问题。一些简单的测试用例只需要一个 Mock 对象,这时,我们可以用以下的方法来创建 Mock 对象:
ResultSet mockResultSet =easyMock.createMock(ResultSet.class);
如果需要在相对复杂的测试用例中使用多个 Mock 对象,EasyMock 提供了另外一种生成和管理 Mock 对象的机制:
IMocksControl control = EasyMock.createControl();
java.sql.Connection mockConnection = control.createMock(Connection.class);
java.sql.Statement mockStatement = control.createMock(Statement.class);
java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class);
如果要模拟的是一个具体类而非接口,那么您需要下载扩展包 EasyMock Class Extension 2.2.2。在对具体类进行模拟时,您只要用 org.easymock.classextension.EasyMock 类中的静态方法代替 org.easymock.EasyMock 类中的静态方法即可。
设定 Mock 对象的预期行为和输出
在一个完整的测试过程中,一个 Mock 对象将会经历两个状态:Record 状态和 Replay 状态。Mock 对象一经创建,它的状态就被置为 Record。在 Record 状态,用户可以设定 Mock 对象的预期行为和输出,这些对象行为被录制下来,保存在 Mock 对象中。
添加 Mock 对象行为的过程通常可以分为以下3步:
- 对 Mock 对象的特定方法作出调用
- 通过 org.easymock.EasyMock 提供的静态方法 expectLastCall 获取上一次方法调用所对应的 IExpectationSetters 实例;
- 通过 IExpectationSetters 实例设定 Mock 对象的预期输出。
设定预期返回值
Mock 对象的行为可以简单的理解为 Mock 对象方法的调用和方法调用所产生的输出。
在 EasyMock 2.3 中,对 Mock 对象行为的添加和设置是通过接口 IExpectationSetters来实现的。
Mock 对象方法的调用可能产生两种类型的输出:
(1)产生返回值;
(2)抛出异常。接口 IExpectationSetters 提供了多种设定预期输出的方法,其中和设定返回值相对应的是 andReturn 方法.
IExpectationSetters<T> andReturn(T value);
我们仍然用 ResultSet 接口的 Mock 对象为例,如果希望方法 mockResult.getString(1) 的返回值为 "My return value",那么你可以使用以下的语句:
mockResultSet.getString(1);
expectLastCall().andReturn("My return value");
有时,我们希望某个方法的调用总是返回一个相同的值,为了避免每次调用都为 Mock 对象的行为进行一次设定,我们可以用设置默认返回值的方法:
void andStubReturn(Object value);
举例:
假设我们创建了 Statement 和 ResultSet 接口的 Mock 对象 mockStatement 和 mockResultSet,在测试过程中,我们希望 mockStatement 对象的 executeQuery 方法总是返回 mockResultSet,我们可以使用如下的语句
mockStatement.executeQuery("SELECT * FROM sales_order_table");
expectLastCall().andStubReturn(mockResultSet);
EasyMock 在对参数值进行匹配时,默认采用 Object.equals() 方法。因此,如果我们以 "select * from sales_order_table" 作为参数,预期方法将不会被调用。如果您希望上例中的 SQL 语句能不区分大小写,可以用特殊的参数匹配器来解决这个问题,我们将在 "在 EasyMock 中使用参数匹配器" 一章对此进行说明。
设定预期异常抛出
对象行为的预期输出除了可能是返回值外,还有可能是抛出异常。IExpectationSetters 提供了设定预期抛出异常的方法:
IExpectationSetters<T> andThrow(Throwable throwable);
和设定默认返回值类似,IExpectationSetters 接口也提供了设定抛出默认异常的函数:
void andStubThrow(Throwable throwable);
设定预期方法调用次数
通过以上的函数,您可以对 Mock 对象特定行为的预期输出进行设定。除了对预期输出进行设定,IExpectationSetters 接口还允许用户对方法的调用次数作出限制。在 IExpectationSetters 所提供的这一类方法中,常用的一种是 times 方法:
IExpectationSetters<T>times(int count);
该方法可以 Mock 对象方法的调用次数进行确切的设定。
举例:
假设我们希望 mockResultSet 的 getString 方法在测试过程中被调用3次,期间的返回值都是 "My return value",我们可以用如下语句:
mockResultSet.getString(1);
expectLastCall().andReturn("My return value").times(3);
除了设定确定的调用次数,IExpectationSetters 还提供了另外几种设定非准确调用次数的方法:
times(int minTimes, int maxTimes):该方法最少被调用 minTimes 次,最多被调用 maxTimes 次。
atLeastOnce():该方法至少被调用一次。
anyTimes():该方法可以被调用任意次。
某些方法的返回值类型是 void,对于这一类方法,我们无需设定返回值,只要设置调用次数就可以了。以 ResultSet 接口的 close 方法为例,假设在测试过程中,该方法被调用3至5次:
mockResultSet.close();
expectLastCall().times(3, 5);
将 Mock 对象切换到 Replay 状态
在生成 Mock 对象和设定 Mock 对象行为两个阶段,Mock 对象的状态都是 Record 。在这个阶段,Mock 对象会记录用户对预期行为和输出的设定。
在使用 Mock 对象进行实际的测试前,我们需要将 Mock 对象的状态切换为 Replay。在 Replay 状态,Mock 对象能够根据设定对特定的方法调用作出预期的响应。将 Mock 对象切换成 Replay 状态有两种方式,您需要根据 Mock 对象的生成方式进行选择。如果 Mock 对象是通过 org.easymock.EasyMock 类提供的静态方法 createMock 生成的(第1节中介绍的第一种 Mock 对象生成方法),那么 EasyMock 类提供了相应的 replay 方法用于将 Mock 对象切换为 Replay 状态:
**replay(mockResultSet);**
如果 Mock 对象是通过 IMocksControl 接口提供的 createMock 方法生成的(第1节中介绍的第二种Mock对象生成方法),那么您依旧可以通过 IMocksControl 接口对它所创建的所有 Mock 对象进行切换:
control.replay();
以上的语句能将在第1节中生成的 mockConnection、mockStatement 和 mockResultSet 等3个 Mock 对象都切换成 Replay 状态。
Mock 对象的重用
为了避免生成过多的 Mock 对象,EasyMock 允许对原有 Mock 对象进行重用。要对 Mock 对象重新初始化,我们可以采用 reset 方法。
和 replay 和 verify 方法类似,EasyMock 提供了两种 reset 方式:
(1)如果 Mock 对象是由 org.easymock.EasyMock 类中的静态方法 createMock 生成的,那么该 Mock 对象的可以用 EasyMock 类的静态方法 reset 重新初始化;
(2)如果 Mock 方法是由 IMocksControl 实例的 createMock 方法生成的,那么该 IMocksControl 实例方法 reset 的调用将会把所有该实例创建的 Mock 对象重新初始化。
在重新初始化之后,Mock 对象的状态将被置为 Record 状态。
测试demo
- 测试一个接口
public class UserServiceImpl{
private UserDao dao;
public User query(String id) throws Exception{
try{
return dao.getById(id);
}catch(Exception e){
throw e;
}
return null;
}
}
public class UserDao{
public User getById(String id) throws Exception{
try{
return ……;
}catch(Exception e){
throw e;
}
return null;
}
}
测试代码
public class UserServiceImplTest {
@Test
public void testQuery() {
User expectedUser = new User();
user.setId(“1001”);
UserDao mock = EasyMock.createMock(UserDao.class);//创建Mock对象
Easymock.expect(mock.getById("1001")).andReturn(expectedUser);//录制Mock对象预期行为
Easymock.replay(mock);//重放Mock对象,测试时以录制的对象预期行为代替真实对象的行为
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(mock);
user user = service.query("1001");//调用测试方法
assertEquals(expectedUser, user); //断言测试结果
Easymock.verify(mock);//验证Mock对象被调用
}
}
- 测试一个对象
public class SalesOrderTestCase extends TestCase {
public void testSalesOrder() {
IMocksControl control = EasyMock.createControl();
......
ResultSet mockResultSet = control.createMock(ResultSet.class);
try {
......
mockResultSet.next();
expectLastCall().andReturn(true).times(3);
expectLastCall().andReturn(false).times(1);
mockResultSet.getString(1);
expectLastCall().andReturn("DEMO_ORDER_001").times(1);
expectLastCall().andReturn("DEMO_ORDER_002").times(1);
expectLastCall().andReturn("DEMO_ORDER_003").times(1);
mockResultSet.getString(2);
expectLastCall().andReturn("Asia Pacific").times(1);
expectLastCall().andReturn("Europe").times(1);
expectLastCall().andReturn("America").times(1);
mockResultSet.getDouble(3);
expectLastCall().andReturn(350.0).times(1);
expectLastCall().andReturn(1350.0).times(1);
expectLastCall().andReturn(5350.0).times(1);
control.replay();
......
int i = 0;
String[] priceLevels = { "Level_A", "Level_C", "Level_E" };
while (mockResultSet.next()) {
SalesOrder order = new SalesOrderImpl();
order.loadDataFromDB(mockResultSet);
assertEquals(order.getPriceLevel(), priceLevels[i]);
i++;
}
control.verify();
} catch (Exception e) {
e.printStackTrace();
}
}
}