原文网址:http://www.infoq.com/cn/articles/auto-test-mock-server
mock在单元测试中已经众所周知。现今我们有各种功能强大而又好用的mock框架,可以很方便的解除单元测试中各种依赖,这大大的降低了编写单元测试的难度。而测试驱动开发(TDD)更进一步将mock作为一种设计手段,来辅助识别出元素之间交互的接口和职责。
那么在功能测试(这里提到的功能测试指的是用户级测试)这个层次,是否有必要使用mock呢?如果有必要又将如何构建呢?或者说是否有可能像单元测试中那样构建一个通用的mock server系统呢?本文将根据我的实践经历,向大家介绍一个通用mock server系统的主要组成部分以及设计思路。
现今的业务系统很少孤立存在,它们或多或少需要使用兄弟团队或是其他公司提供的服务,这为我们的联调和测试造成了麻烦。对于这种情况,我们常见的解决方案是搭建一个临时的server,模拟那些服务,提供数据进行联调和测试。这就是mock server的雏形。一般来讲,搭建这种mock server系统比较简单,不过它的功能也比较简单,而且往往需要针对不同的接口重复开发。那有没有可能像单元测试中使用的mock框架那样构建一个通用的mock server系统呢?
观察单元测试中的mock框架,我们会发现一般使用mock的流程是:
init mock //创建mock对象 config mock //设置mock期望 setup mock //将mock对象设置给被测对象 call //调用被测接口,被测接口里的代码会调用mock对象 verify //验证 拿mockito举例: User expected = new User(“admin”, “12345”); //init UserDAO dao = mock(UserDAO.class); //config when(dao.findByName(“admin”)).thenReturn(expected); //setup UserService service = new UserService(dao); //call User actual = service.login(“admin”, “12345”); //verify or assert借鉴这种做法,我么就可以构建一个简单的mock server系统,接下来的内容中,我们会在这个mock server的基础上演化出比较完善的版本。
假设我们需要mock的是HTTP接口。我们的mock server提供一个配置接口(对应着上面的config mock步骤),测试运行之前调用配置接口将需要mock的HTTP接口URL以及需要返回的值传递过去,mock server内部建立url 到返回值的关联(这里就类似存在一个哈希表一样)。Mock server还提供另外一个接口供被测系统调用。这是一个通用的接口,所有原先指向真实服务的地址全部被指向到该接口(可以通过修改配置或修改系统hosts文件)。当该接口被调用时会寻找刚才建立的url 到返回值的关联,并将mock的值返回。这样一个非常简单的mock server就构建出来了,对于一些简单的联调场景基本够用。这个时候我们的mock server的原理图如下面所示:
有了第一版的mock server,对一些只需要模拟值的场景是够用了。但是,mock的作用仅限于此么?想想单元测试中的mock。在单元测试中mock框架除了能够为被测系统构建输入值外,还能捕获到被测程序传出的值,然后对这些值进行校验。举一个实际的例子:在我们的系统中经常需要向用户发送短信,为了对发送的短信功能以及短信内容进行验证,在没有mock之前我们可能真的需要向真实手机发送短信,然后验证。这不仅降低了测试效率,也增加了不少不可控因素。为此我给mock server添加第三个接口:query接口。在被测系统调用mock接口时,mock会记录下被测系统传递给mock接口的参数值,然后测试中可以调用query接口查询到记录的值,在测试中可以对其断言,而且这里记录下的调用记录还可以作为日志提供出来,提高系统的诊断能力。这样我们的mock server的结构就变成:
现在我们的mock server已经可以提供类似verify的功能了,但实际上它还不能算一个真正的mock。它也不能处理哪怕稍微复杂一点的情况。在实际中,我们经常需要针对不同的参数返回不同的值。举个简单的例子:我们mock一个支付接口,对于订单号123我们期望支付成功,对于订单234期望支付失败。这就引入了我的mock server的两个核心组件:extractor和matcher。Extractor组件主要用于从调用的参数上提取出参数值,然后转换成key/value的格式提供给后续的环节使用;因为请求的参数格式多种多样(json,xml等),extractor 为此提供了统一的接口。
Matcher组件的作用是在拿到extractor传递过来的key/value值后利用一些匹配器匹配到具体设置的期望上,所有匹配到的调用会返回对应的值。也就是说mock server内部不再是简单的映射了。后面再介绍DSL部分的时候会介绍matcher使用的语法。这样我们的mock server结构就演化成下图:
估计有人在抱怨,说了这么多这个mock server还只能mock HTTP接口啊,我们的系统中存在HTTP接口,RPC接口,SMTP接口等等。这是mock server中协议组件的职责。协议组件是mock server的入口,它提供多种协议的服务,并且解析出协议包数据,然后将数据交给extractor组件;除此之外,协议组件在收到上层的返回值后,会按照协议的格式返回给被测系统 。利用一些开源的类库,我们可以很容易对一些通用协议提供支持,但对一些私有的二进制协议如果没有现成的库支持,要重新开发成本很大,不过我们可以从客户端来解决这个问题,这在后续的文章中会有介绍。
基本上一个功能还算完善的mock server成型了。但这就够了么?对于要模拟各种场景的测试还远远不够。我们很多接口有回调的功能,我们通常还需要模拟接口超时的情况,而对于一些支付相关的接口经常需要对参数进行加密解密,而且这些情况都需要是可配置的。有没有发现,前面我们介绍的所有实际上都是mock值。也就是我们设置一些值,然后调用的时候将值返回。但是很多时候我们不仅需要mock值,更要mock行为。这样我们有了mock server中最核心的组件:命令执行引擎(好牛的名字,其实就那样)。在设置mock的时候我们不再是设置一个值,而是设置一个预定义命令组合成的流水线(即按照类似下面xml的配置一步一步执行,并且可以将上一步的执行结果传递给下一步):
1000 {“ret”:”true”} {“ret”:”true”}
上面的流水线被命令执行引擎解析执行后就是按顺序执行对应的DelayCommand, CallbackCommand以及ReturnCommand命令了,具体命令就不介绍了。采取这种方式给我们mock server带来了很大的灵活性:只需要简单的扩展一个子命令,就可以扩充mock server的行为。比如mock某网关接口时需要使用MD5加密,只需要扩展一个MD5Command(下面代码中的$result表示前一步骤
$result
现在我们的mock不仅可以mock值了,对于各种行为的模拟也得心应手。但是要使用方便,还要提供便于使用的接口。Mock server提供两类接口:针对自动化测试的DSL,以及针对手工测试使用的管理界面。这里主要介绍这种DSL(因为我们的测试用例是使用xml编写,转换成编程语言也很容易):
1000 $result
service是对被mock的服务的描述,比如对于SMTP,我们可以这样定义: service="smtp:9000"。这个表示在9000端口上监听smtp协议。而matcher即前面介绍的matcher组件所使用的各种匹配器,用于匹配被测系统调用mock server时传递的数据。比如上面的例子表示的就是如果被测系统调用http接口/ticket.jsp,并且参数里包含orderNo则延迟1秒钟,然后返回一个json值 。
前面几节介绍了一个比较完善的通用mock server从简到繁演化的设计思路,希望可以为想要构建类似设施的读者提供一个参照。
这个mock系统包含两个主要部分:mock admin和mock server。Mock admin是管理界面,主要提供监控(可以在界面上实时看到被测系统与mock server交互)以及手工测试时的配置界面。 Mock server即前面介绍的主体,其架构如上图所示。Mock server包含几个核心组件:协议、extractor、matcher、命令执行引擎、存储(即mock server中使用的各种数据的存储)。Mock server提供三类接口:配置、被mock接口(各种服务,通过协议组件提供)、查询。