「第二十章」 moco server
20.1 Moco 是什么?
Moco 是一个简单搭建模拟服务器的程序库/工具,主要用于测试集成,尤其是基于HTTP协议的集成——web service,REST等,在我们的项目开发中被广泛应用。
Moco 通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些 Moco 对象的行为是我们事先设定且符合预期。通过这些 Moco 对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。
引入 Moco 最大的优势在于:Moco 的行为固定,它确保当你访问该 Moco 的某个方法时总是能够获得一个没有任何逻辑的直接就返回的预期结果。
Moco就是针对这样一个特定的场景而生的。Moco是一个简单搭建模拟服务器的程序库/工具,这个基于 Java 开发的开源项目已经在 Github 上获得了不少的关注。该项目的简介是这样描述自己的:Moco 是一个简单搭建 stub 的框架,主要用于测试和集成。
Moco可以提供以下服务:
1、HTTP APIs
2、Socket APIs
3、REST API
20.2 Moco 原理简介
Moco会根据一些配置,启动一个真正的HTTP服务(会监听本地的某个端口)。当发起请求满足一个条件时,它就给回复一个应答。Moco的底层没有依赖于像Servlet这样的重型框架,而是基于一个叫Netty网络应用框架直接编写的,这样一来,绕过了复杂的应用服务器,所以,它的速度是极快的。
20.3 使用 Moco 的好处
Moco Object 的使用通常会带来以下一些好处:
1、隔绝其他模块出错引起本模块的测试错误。
2、隔绝其他模块的开发状态,只要定义好接口,不用管他们开发有没有完成。
3、一些速度较慢的操作,可以用 Moco Object 代替,快速返回。
4、对于分布式系统的测试,使用 Moco Object 会有另外两项很重要的收益。
5、通过 Moco Object 可以将一些分布式测试转化为本地的测试。
6、将 Moco 用于压力测试,可以解决测试集群无法模拟线上集群大规模下的压力。
20.4 Moco 的业务需求场景
1、我是一个企业级软件开发人员,每次面对集成就是我头疼开始的时候,漫长集成拉锯战拖延了我们的进度。幸好有了Moco,几行配置就可以模拟一个服务,我再也不需要看集成服务团队的脸色了。
2、 我是一个移动开发人员,老板催得紧,可服务器端开发进度慢,我空有一个漂亮的iphone应用,发挥不出作用。幸好有了Moco,很快就可以搭建出一个模拟服务,我再也不用把生命浪费在无效的等待上了。
3、我是一个前端开发人员,做Inception的时候,客户总想看到一个完整的应用演示,可哪有时间开发后端服务啊!幸好有了Moco,几下就可以弄出一个模拟服务,我做的页面一下就有了生命力。
20.5 Moco 操作的应用场景
在使用 Moco 的过程中,发现 Moco 是有一些通用性的,对于一些应用场景,是非常适合使用 Moco 的:
1、真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)。
2、真实对象很难被创建(比如具体的 web 容器)。
3、真实对象的某些行为很难触发(比如网络错误)。
4、真实情况令程序的运行速度很慢。
5、真实对象有用户界面。
6、测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)。
7、真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)。
当然,也有一些不得不Moco的场景:
1、一些比较难构造的 Object :这类 Object 通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
2、执行操作的时间较长 Object :有一些 Object 的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行 Moco。
3、异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过 Moco 可以人为的控制触发异常逻辑。
在一些压力测试的场景下,也不得不使用Mock,例如在分布式系统测试中,通常需要测试一些单点(如 namenode,jobtracker)在压力场景下的工作是否正常。而通常测试集群在正常逻辑下无法提供足够的压力(主要原因是受限于机器数量),这时候就需要应用 Moco去满足。
20.6 Moco 0.11.0的2大特性
Moco 0.11.0的发布主要增加了两个大的特性:REST API 和 JUnit 集成。
特性一:REST API
众所周知,REST 服务几乎已经成了现代服务端开发的标配。为了简化 REST API 的模拟,Moco 专门提供了特定的 API,比如,下面这个例子:
RestServer 的 resource 方法是为配置资源而设计的,主要配置资源的名字,以及访问的设置。这里的例子里,我们声明了一个名为 targets 的资源,我们还配置了一个 get 方法,当资源 ID 为1时,返回相应的对象。这个配置可以通过 /targets/1 访问得到。
REST API 同样支持 JSON 配置文件,上面的例子用 JSON 配置文件的形式可以写成如下格式:
特性二:JUnit 集成
Moco 的 JUnit 集成是利用 JUnit 的特性,进一步简化测试代码的编写。
这里声明了一个 JUnit 的规则(Rule),它会在测试运行之前启动一个 Moco 服务器,在测试运行完毕之后关闭它。利用 Rule 的特性,就不必在每个测试里去启停 Moco 服务器了。
20.7 用Moco-runner 搭建 Moco 测试服务器
moco目前支持多种使用方式,最基本的方式是两种:API和独立运行。
这里主要讲独立运行的sever搭建。
Moco-sever的搭建步骤:
1.下载jdk(建议是jdk1.7或者jdk1.7以上)。
2.安装jdk。
3.配置jdk环境变量。(前面的接口测试项目中已经介绍了JDK的配置)
4.验证jdk的安装。
5.去开源社区下载:moco-runner--standalone.jar
目前最新的版本是:moco-runner-0.11.1-standalone-standalone.jar
我把最新的下载下来:
如果大家不想注册开源社区的帐号,我已经把0.10、0.11、0.12 版本都下载了,大家可以到我的网盘上去下载。
6. moco是支持JSON 配置文件,在 moco-runner-0.11.1-standalone-standalone.jar 文件同一级目录下,新建一个 foo.json 文件。
7. 在 JSON 配置文件中编写如下内容:
[
{
"response" :
{
"text" : "Hello, Moco"
}
}
]
8.在Dos下执行 java -jar moco-runner-0.12.0-standalone.jar http -p 12306 -c foo.json 启动 Moco-sever 服务。
【http】:表示 http 协议。
【-p 12306】:表示 Moco-sever 端口。备注:端口可以自己编。
【-c foo.json】:表示指定的配置文件,配置文件名称可以自己命名。
9.启动之后,访问:http://localhost:12306 (如果项目组其他成员想访问这个服务,只要把localhost替换成本机IP即可)
表示Moco测试服务器搭建成功。
10.修改配置文件 foo.json 文件,Moco测试服务器自动重新启动加载配置文件的内容,这是Moco的Rule 特性。
[
{
"request" :
{
"uri" : "/"
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.8 Moco-sever 的配置文件的工作原理
moco主要是通过将配置放入json文件中,启动moco的时候,需要指定使用的配置文件路径,这样配置就可以生效了,配置文件的工作原理大致如下:
20.9 Moco HTTP API配置
20.9.1 在配置文件添加注释
json不支持注释,想要添加注释的话,可以在description字段中加入描述。
[
{
"description":"这是个注释行,可以在这添加对接口的描述",
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.2 约定请求Body
[
{
"description":"request 的 body 必须为字符串 foo ,才能匹配此接口",
"request" :
{
"text" : "foo"
},
"response" :
{
"text" : "Hello, Moco"
}
},
{
"description":"request 的 body 必须与 foo.request 相等,才能匹配此接口",
"request" :
{
"file" : "foo.request"
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.3 约定接口的uri
[
{
"description":" request 的 uri必须是foo ,才能匹配此接口",
"request" :
{
"uri" : "/foo"
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.4 约定请求参数
[
{
"description":" request 中必须包含参数:param = blah ,才能匹配此接口",
"request" :
{
"queries" :{
"param" : "blah"
}
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.5 约定请求方法
[
{
"description":"request 必须是get方法 ,才能匹配此接口,除了get,还支持post、put、delete、HEAD方法",
"request" :
{
"method" : "get"
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.6 约定HTTP版本
[
{
"description":"request 必须是 HTTP/1.0 ,才能匹配此接口",
"request" :
{
"version" : "HTTP/1.0"
},
"response" :
{
"text" : "version"
}
}
]
20.9.7 约定请求头部
[
{
"description":"request 中必须包含头部:content-type-application/json ,才能匹配此接口",
"request" :
{
"headers" :{
"content-type" : "application/json"
}
},
"response" :
{
"text" : "Hello, Moco"
}
}
]
20.9.8 约定cookie
[
{
"description":"request 中必须包含cookie:login-true ,才能匹配此接口",
"request" :
{
"cookies" :{
"login" : "true"
}
},
"response" :
{
"text" : "success"
}
}
]
20.9.9 约定请求form
[
{
"description":"request 中必须包含表单:name-foo ,才能匹配此接口",
"request" :
{
"forms" :{
"name" : "foo"
}
},
"response" :
{
"text" : "welcome foo"
}
}
]
备注:表单可以添加多项,多项的时候,必须全部匹配,接口才算匹配成功。
20.9.10 约定以指定xml作为请求body
[
{
"description":"request 的内容必须xml:且等于 ,才能匹配此接口",
"request" :
{
"text" :{
"xml" : ""
}
},
"response" :
{
"text" : "foo"
}
},
{
"description":"request 的内容必须xml:且与your_file.xml文件内容一致 ,才能匹配此接口",
"request" :
{
"file" :{
"xml" : "your_file.xml"
}
},
"response" :
{
"text" : "foo"
}
}
]
20.9.11 用xpath对请求进行匹配
[
{
"description":"request 的内容必须xml:且等于 ,才能匹配此接口",
"request" :
{
"text" :{
"xml" : ""
}
},
"response" :
{
"text" : "foo"
}
},
{
"description":"用xpath对请求进行匹配",
"request" :
{
"xpaths" :{
"/request/parameters/id/1/text()" : "1"
}
},
"response" :
{
"text" : "foo"
}
}
]
20.9.12 约定以指定json作为请求body
[
{
"description":"request 的内容必须是json :且等于{"foo":"bar"} ,才能匹配此接口",
"request" :
{
"text" :{
"json" : "{"foo":"bar"}"
}
},
"response" :
{
"text" : "foo"
}
},
{
"description":"json快捷语法,与上面的接口等价",
"request" :
{
"json" :{
"foo" : "bar"
}
},
"response" :
{
"text" : "foo"
}
},
{
"description":"request 的内容必须是json:且与your_file.xml文件内容一致 ,才能匹配此接口",
"request" :
{
"file" :{
"json" : "your_file.xml"
}
},
"response" :
{
"text" : "foo"
}
}
]
20.9.13 用正则表达式对请求进行匹配
[
{
"description":"request 的 uri必须与正则表达式匹配,才能匹配此接口",
"request" :
{
"uri" :{
"match" : "/w*/foo"
}
},
"response" :
{
"text" : "hello foo"
}
}
]
20.9.14 匹配操作
[
{
"description":"request 的uri必须以 foo 开头 ,才能匹配此接口",
"request" :
{
"uri" :{
"startsWith" : "/foo"
}
},
"response" :
{
"text" : "bar"
}
},
{
"description":"request 的uri必须以 foo 结束 ,才能匹配此接口",
"request" :
{
"uri" :{
"endWith" : "foo"
}
},
"response" :
{
"text" : "bar"
}
},
{
"description":"request 的uri必须包含foo ,才能匹配此接口",
"request" :
{
"uri" :{
"contain" : "foo"
}
},
"response" :
{
"text" : "bar"
}
}
]
20.9.15 设置Response content
[
{
"description":"接口匹配后,response 的 body 为 bar ",
"request" :
{
"text" :"foo"
},
"response" :
{
"text" : "bar"
}
},
{
"description":"接口匹配后,response 返回的文件bar.response,文件的编码为GBK,编码为可选字段",
"request" :
{
"text" :"foo"
},
"response" :
{
"file" : "bar.response",
"charset" : "GBK"
}
}
]
20.9.16 设置Response 状态码
[
{
"description":"接口匹配后,response 返回的状态码为:200",
"request" :
{
"text" : "foo"
},
"response" :
{
"status" : "200"
}
}
]
20.9.17设置Response HTTP版本
[
{
"description":"接口匹配后,response 指定 Http 版本为 HTTP/1.0 ",
"request" :
{
"uri" : "/version10"
},
"response" :
{
"version" : "HTTP/1.0"
}
}
]
20.9.18 设置Response 头部
[
{
"description":"接口匹配后,response 返回的头部为 content-type-application/json",
"request" :
{
"text" : "foo"
},
"response" :
{
"headers" :
{
"content-type" : "application/json"
}
}
}
]
20.9.19 设置重定向
[
{
"description":"接口匹配后,返回重定向地址",
"request" :
{
"uri" : "/redirect"
},
"redirectTo" :"http://www.github.com"
}
]
20.9.20 设置cookie
[
{
"description":"接口匹配后,cookie 中包含login-true",
"request" :
{
"uri" : "/cookie"
},
"response" :
{
"cookies" :
{
"login" : "true"
}
}
}
]
20.9.21挂载文件
[
{
"description":"挂载文件 dir 到 uri 路径",
"mount" :
{
"dir" : "dir",
"uri" : "/uri"
}
}
]
20.10 template的用法
Moco内置了一些变量,在response中可以使用这些变量,让反馈更加智能,以下列举了常用的变量
1、req.version
2、req.version
3、req.method
4、req.content
5、req.headers
6、req.queries
7、req.forms
8、req.cookies
例子:
[
{
"description":"template 模板",
"request" :
{
"uri" : "/template"
},
"response" :
{
"text" : {
"template" : "${req.headers['foo']}"
}
},
{
"description":"template 模板",
"request" :
{
"uri" : "/template"
},
"response" :
{
"text" : {
"template" : "${req.queries['foo']}"
}
},
{
"description":"template 模板",
"request" :
{
"uri" : "/template"
},
"response" :
{
"text" : {
"template" : "${req.forms['foo']}"
}
}
}
]
20.11 moco在Python3单元测试中的使用
在 Python3.x中,moco已经被集成到了unittest单元测试框架中,命名了一个叫 mock的模块,用命令:from unittest import mock 可以直接引用 mock 模块。
在 Python3 的 C:Python34Lib甥楮瑴est 目录下有个mock的文件,如下图:
大家如果有兴趣,可以去研究下mock模块里面的方法。
20.11.1 查看mock常用的方法
代码:
from unittest import mock
print('查看modk库常用的方法:',dir(mock))
运行结果:
20.11.2 查看mock库详细的帮助信息
代码:
from unittest import mock
print('查看mock库详细的帮助信息:',type(help(mock)))
20.11.3 mock库方法的使用案例
案例:用mock实现删除D盘下的 D:mock_file 文件夹。
第一步:先编写一个rmdir方法删除D:mock_file 文件夹的程序。
import mock
import os
class Remove(object):
def rmdir(self,path='D:/mock_file'):
os.rmdir(path)
第二步:为了达到 mock 的目的,就需要模拟删除文件夹操作,对上面的代码如下处理:
from unittest import mock
import os
class Remove(object):
def rmdir(self,path='D:/mock_file'):
r=os.rmdir(path)
if r==None :
return '删除成功'
else:
return '删除失败'
def exists_get_rmdir(self):
return self.rmdir()
第三步:在unittest单元测试框架里编写测试用例。
from unittest import mock
import os
import unittest
class Remove(object):
def rmdir(self,path='c:/log'):
r=os.rmdir(path)
if r==None :
return 'success'
else:
return 'fail'
def exists_get_rmdir(self):
return self.rmdir()
class MockTest(unittest.TestCase):
def setUp(self):
self.r=Remove()
def tearDown(self):
pass
def test_success_rmdir(self):
'''
删除目录成功
:return:
'''
success_path=mock.Mock(return_value='success')
self.r.rmdir=success_path
self.assertEqual(self.r.exists_get_rmdir(),'success')
def test_fail_rmdir(self):
'''
删除目录失败
:return:
'''
fail_path=mock.Mock(return_value='fail')
self.r.rmdir=fail_path
self.assertEqual(self.r.exists_get_rmdir(),'fail')
if __name__=='__main__':
unittest.main(verbosity=2)
运行结果:
执行如上的代码的时候,我们就不需要考虑是否存在该文件夹,以及该文件夹是否可正常的删除,我们完全使用 mock 来解决了这个问题,那么我们来看它的执行顺序:
1、找到替换的对象,我们需要测试的是 exists_get_imdir()方法,那么我们就需要替换掉rmdir() 方法
2、对 Mock 类进行实例化对象得到 mock,并且设置这个 mock 的行为 return_value 值,也就是 mock 虚构对象,在测试通过中,我们虚构 return_value为'success',在测试不通过我们虚构 return_value 为 'fail'。
3、使用 mock 对象我们想替换的方法 rmdir(),这样我们就替换到了self.r.rmdir。
4、编写测试代码,进行断言,我们调用 self.r.exists_get_imdir() 方法,并且期望它的返回值与我们预期的结果一致(不管是成功的还是失败的)
20.12 moco在Python3接口测试中的使用
20.12.1 mock 模拟get请求
第一步:新建一个 mock_http 目录 。
第二步:在目录下创建 config.json 配置文件,并在文件中写入如下内容:
[
{
"request":
{
"method":"get",
"uri":"/"
},
"response":
{
"json":"huanyingni moco"
}
}
]
第三步:把最新版的 moco-runner-0.12.0-standalone 放到新建的目录下。
第四步:然后cmd命令下切换到 D:mock_http 目录下,执行:
java -jar moco-runner-0.12.0-standalone.jar http -p 12306 -c config.json 命令启动 moco server 。
代表已经启动,那么我们通过url访问。
可以看到我们的server已经启动,我们看下命令行给我们的记录信息。
第五步:在Python3中编写代码,模拟get请求。
import unittest
import requests
import json
class InterfaceTestCase(unittest.TestCase):
def setUp(self):
self.domain = 'http://localhost:12306'
def tearDown(self):
print('end_test')
def test_get(self):
r = requests.get(self.domain)
print(r.text)
if __name__ == '__main__':
unittest.main()
运行结果:
20.12.2 mock 模拟数据驱动
第一步:在目录下新增一个 data.json 数据驱动文件,在文件中编写数据如下:
[
{
"title":'first1',
'url':'/post/1'
},
{
"title":'first2',
'url':'/post/2'
},
{
"title":'first3',
'url':'/post/3'
}
]
第二步:修改config.json 配置文件
[
{
"request":
{
"method":"get",
"uri":"/get"
},
"response":
{
"file":"data.json"
}
}
]
保存后,后台检查到变动,就会自动重新加载 config.json 配置文件。
第三步:浏览器访问url。
20.12.2 mock 模拟post请求
修改config.json 配置文件内容,如下:
[
{
"request":
{
"method":"post",
"uri":"/post",
"text":
{
"json":"{"bike":"2000","book":"198"}"
}
},
"response":
{
"status":"200"
}
}
]
20.13 Moco的不足
Moco 的使用很简单,配置也很方便,目前更是提供了 http、rest、socket 服务。但是也仅仅是能 stub 出接口,模拟出简单的场景。如果接收到请求后需要做一些处理,如需查询数据库、进行运算、或者一些复杂的操作,就无能为力了。所以是否选用 Moco,就取决于开发者是否只是需要一个简单的模拟服务器。