挡板起因
当今是多服务多协作的时代,任何系统不再是孤岛,而是相互依赖。
由于存在多系统间环境稳定难、数据准备难、并行开发难问题,造成多系统间联调测试需要大量的沟通成本、排查成本,为了解决这个让人头疼的问题,引出了挡板的概念。
其实挡板测试主要是为了模拟外围的系统、服务、接口而开发的mock。
一句话为了减少外围系统的影响,在接口协议的框架下专注自己的功能,减少扯犊子。
解决问题
挡板解决不稳定的问题:Mock服务非常简单,业务逻辑简单甚至没有,所以它足够稳定。
快速构造复杂数据:通过自定义返回结果,可以构造非常复杂的数据,不需要第三方给我们准备数据,即可以做业务验证。
快速构造异常场景:对于一些异常的情况,比如网络延迟高、重试机制、特殊异常返回都可以用挡板来构造。
契约测试、驱动测试开发、性能测试、演示测试统统适用。
框架说明
先复习下我们一般的系统架构,如下面的电商系统,会调用外围的系统支付、短信、实名认证、邮件通知等,对外通讯协议有tcp、https、http、smtp。
这里主要说明了调用外围系统的情景,其实内部之间的系统调用比这复杂。
我们在引入挡板的架构后,挡板对于核心系统访问应该是透明的,是美丽的谎言,具体架构如下:
我们再深入了解下挡板的架构,从功能的角度分为动态数据、静态数据提供。
挡板前置
提供接口的服务,用什么协议、用什么端口。
静态数据
静态的数据就是在写测试代码之前根据需要准备好,利用json数据格式(1.2)或其他文件的数据(1.1)存储,他的数据基本上是无需改动的。
动态数据
动态的数据就是在测试代码运行的时候才生成或根据业务去查询数据。
数据无上下文关系只是一些动态的数据输出,利用动态的函数生成数据(2.1),动态的函数生成可以使用mockjs,它里面提供常用的数据生成,后面详细介绍。
数据存在上下文关系,当前接口需要前面接口数据,利用数据库或NOSQL提供数据(2.2)。
定时回调
利用定时任务能够根据业务需求定时的把数据推到核心系统,是整个业务调用链路闭合。他的数据来自于静态或动态的数据。
技术选型
根据上面的架构要求,在繁多的开源框架中,选择了mountebank+mockjs,其他的框架很多在这里不做评论,软件没有好坏之分,适合很重要!
mountebank本身相当强大,引用官方的话说mountebank可以提供跨平台、多协议的测试mock,就像我们所说的桩代码一样提供测试驱动, 是当前开源当中最强大的,完全可以治愈你的痛。
一句话mountebank简单便捷的实现挡板服务。
mountebank 简写 mb,注意后面直接称呼了。
mockjs本身也是可以提供mock的功能,我主要是看中了他强大的丰富的数据类型,生成动态的数据那是相当方便。 先尝尝鲜:
Mock.mock({ "number|1-100": 100 }) |
这个函数就很容易的得到1到100的随机数,相当方便吧。
环境安装
由于mountebank是基于nodejs,所以首先要安装下nodejs,版本要求是v6或以上(他的强大之处在这里就不多说了,做前端的朋友心里有数)。
官方下载https://nodejs.org/en/download/
或者
yum install -y nodejs |
那就mb和其他的组件一起安装了
#安装mountebank npm install -g mountebank #http请求提交试用 npm install superagent --save #日志打印 npm install log4js #获取UUID试用 npm install uuid --save #获取时间 npm install moment #mysql连接用 npm install mysql -S #定时任务 npm install cron #mock常用函数组件 npm install mockjs |
mb概述
安装好mb以后,直接通过mb命令启动服务,默认端口是2525,浏览器输入(http://localhost:2525) 访问试试看!
在RHEL7和Centos7下可以设定后台服务自动启动,配置的方法可以在这里配置方法获得,在这里不多扩展。 本人喜欢写段shell脚本来启动、停止mb服务,详情见后面。
mb命令启动常用参数
| 选项| 描述| 默认| |-- |-- |-- | | --command| start,stop,restart, replay(删除代理,转化为代理捕获的响应值,后面代理详解)| start| | --port 2525| 指定mb运行的端口好,默认是2525| 2525| | --configfile imposters.ejs| mountebank将加载指定的模板文件,为了方便管理会把配置存储在EJS模板中。后面详细说明。| N/A| | --logfile mb.log| mountebank用于存储日志的文件| mb.log| | --loglevel debug| 日志级别 debug, info, warn, error| info| | --allowInjection| 是否允许脚本注入,mb支持针对谓词, 存根响应,行为装饰, 等待行为函数和 tcp请求解析的 JavaScript注入,但默认情况下禁用它们。| false| | --localOnly| 请求隔离,仅接受来自localhost的请求| false| | --ipWhitelist| 白名单环境隔离| 所有IP地址| | --pidfile| 为stop命令存储pid的文件| mb.pid| |
常用术语
冒名顶替者(imposter)
从社会角度来说他就是一个诈骗团伙,里面会有很多的骗子。 从技术角度来说imposter就是一个服务或系统骗子,他有端口、协议提供测试的服务,可以根据测试需要创建多个imposter。
存根(stub)
用于为imposter生成响应的一组配置。一个imposter可以具有0个或多个stub。
从社会角度来说就是骗子公司的一个部门或产品线。
从技术角度来说stub就是提供一个服务的响应,一个请求的地址。
谓词(predicate)
配置响应stub的条件。每个stub可以有0个或更多predicate。
从社会角度来说就是要找相应的人做相应的事。
从技术角度来说predicate就是根据条件做相应的功能,返回对应的数据。
响应(response)
产生响应数据。每个stub可以有0个或更多响应。
响应类型(response type) 每个stub响应由定义响应行为的特定响应类型定义。目前支持的响应类型是:is,proxy,inject。
is是默认的类型,就是一般的应答输出。
proxy是代理响应类型,它允许记录重放行为。
inject允许您注入JavaScript函数以创建响应对象。
存根行为(stub behavior)
向响应添加其他的自定义处理,如可以后面通过shell脚本执行nodejs来获取动态的数据。
如果对上面的概念还不明白,没有关系,后面就让代码给你解释。
代码说明一切
前面对于他的结构说明了一番,不懂得没有关系,我们go on! 下面就是一个imposter,其实他就是一个json格式的文件,为了方便说明我在json里面加了注释(懂技术的人都会明白,这样会死的很惨!)。
{ "port": 8081,<!-- 端口号 --> "protocol": "http",<!-- 通讯协议 --> "stubs": [ { "predicates": [ { "equals": { "method": "POST",<!-- 提交方式 --> "path": "/customers/123"<!-- 访问地址 --> } } ], "responses": [<!-- 响应数据 --> { "is": { "statusCode": 200, "headers": { "Server": "Apache-Coyote/1.1", "Access-Control-Allow-Origin": "*", "Content-Type": "text/json;charset=UTF-8", "Content-Length": 298, "Date": "Tue, 05 Sep 2017 06:49:14 GMT", "Connection": "close" }, "body": { "head": { "rspcode": "100", "respMsg": "成功", "serialNo": "d7af14e5-a99e-4881-9b1b-695b7a760a64" }, "body": { "code": "S4393493", "startDate": "2019-04-09", "level": "3", "endDate": "2020-04-08" } } }, "_behaviors": { "shellTransform": ["node ./dcs/test.js"]<!-- 这里可以通过shell脚本执行nodejs来修改body的数据 --> } } ] }, { "responses": [ { "is": { "statusCode": 404 }<!-- 地址不匹配情况 --> } ] } ] } |
正常访问的情况下返回结果为:
{ "head": { "rspcode": "100", "respMsg": "成功", "serialNo": "d7af14e5-a99e-4881-9b1b-695b7a760a64" }, "body": { "code": "S4393493", "startDate": "2019-04-09", "level": "3", "endDate": "2020-04-08" } } |
是不是很简单?
mb详解
上面主要介绍了mb的环境安装及其关键术语,和一个简单的demo,那么接下来我们逐步了解。
其他语言客户端支持
如果你对nodejs不熟或者很倔不想接收新的语言,可以在下面找到自己的熟悉的语言,如果没有要自己去写了。 说实在的,对于有上心的开发者是难不倒的,一点就通!
| 语言| 工程| 作者| |-- |-- |-- | |C# |[MbDotNet](https://github.com/mattherman/MbDotNet) |Matthew Herman | |Clojure |[Charlatan](https://github.com/mdaley/charlatan) |Matthew Daley | |Go |[GoBank](https://github.com/durmaze/gobank) |Erkan | |Java |[javabank](https://github.com/thejamesthomas/javabank) |James Thomas | |JavaScript |[mountebank-helper](https://www.npmjs.com/package/mountebank-helper) |Alex | |Perl |[Test::Mountebank](https://metacpan.org/release/Test-Mountebank) |Dagfinn Reiers?l | |PHP |[Juggler](https://github.com/meare/juggler) |Andrejs Mironovs | |Python |[mountepy](https://pypi.org/project/mountepy/) |Micha? Bultrowicz | |Ruby |[mountebank-gem](https://github.com/CoderKungfu/mountebank-gem) |Michael Cheng | |Shell |[mountebank-sh](https://github.com/sebero/mountebank-sh) |Sergi Bech Robleda | |TypeScript |[node-mountebank](https://www.npmjs.com/package/@toincrease/node-mountebank) |Ron van der Wijngaard| |
启动脚本
看到这里应该明白了mb究竟是个什么东东,但怎么开发、启动一个自己的服务估计还有些迷糊,那么我们从启动说起。
mb start --configfile imposters.ejs --allowInjection & |
上面我说了常用的参数,这个启动指令里包含 start、--configfile、--allowInjection。
start顾名思义就是启动mb
--configfile 后面跟的imposters.ejs就是模板文件,EJS是一个JavaScript模板库,用来从JSON数据中生成HTML字符串。 有时通过配置文件加载imposters更方便,而不是通过API(就是通过指令,我也嫌麻烦就没提)加载它们。 我们可以在EJS文件里面定义端口、通讯协议、存根等,对于多个服务的模块化开发相当便利。
--allowInjection 是否允许脚本注入,后面详解。
EJS文件脚本
本次讲解主要是http的挡板服务,由于时间有限,smtp、tcp请参照官方文档或等后续文档。
imposters.ejs文件的内容是json格式的数据,具体如下:
{ "imposters": [ <% include ./test/test1.json %>, <% include ./test/test2.json %>, <% include ./test/test3.json %> ] } |
ejs内容比较简单,就是imposters的配置,它是json数组,通过include加载多个json文件来注入到imposters配置中,如test1.json是支付的挡板代码, test2.json是实名认证的挡板代码,这样从架构的角度,各司其职、分而治之不同的业务挡板不同的配置文件,便于并行开发和维护。
JSON文件脚本
test1.json文件json的格式如下,可以根据注释了解其整个结构。
{ "port": 1234,<!-- 指定端口 --> "protocol": "http",<!-- 指定协议 --> "stubs": [{<!-- 可以多个元素 --> "predicates": [{ "equals": {<!-- 如果请求url 为/test/demo时触发本stub--> "path": "/test/demo",<!-- 访问路径 --> "method": "post"<!-- 提交方法 --> } }, {}],<!-- 可以多个元素 --> "responses": [{ "is": { "statusCode": 200, "headers": { }, "body": { } }, "_behaviors": { "shellTransform": ["node ./test/test.js"]<!-- 脚本注入 --> } }, {}]<!-- 可以多个元素 --> }, {<!-- 请求不匹配时响应内容,如果没有谓词匹配,则发送默认响应 --> "responses": [{ "is": { "statusCode": 400,<!-- 服务器不理解请求的语法,这里可以随意定义的哈 --> "headers": { }, "body": { } } }] }] } |
注意:最后一个应答是没有predicate匹配,则发送默认响应,就是我们写代码是swich时的default。
说起http协议,一定要说下状态码,有时候面试官也会经常问起,目前大多数服务化治理都是采用http通讯协议。
下面简单的提下,便于大家理解,技术这玩意也是个关系网,简单的服务也会涉及到很多领域。 做前端的兄弟们一定要记住,因为大多是中后台兄弟的问题,这个时候可以吊他们一次。
1开头系列:表示请求已接收,继续处理 100 2开头系列:成功--表示请求已被成功接收、理解、接受 200 ok 表示请求成功返回网页 3开头系列:表示重定向,要完成请求必须进行更进一步的操作 301 永久跳转 302 临时跳转,请求的网页已临时跳转到新位置。 4开头系列:客户端错误--请求有语法错误或请求无法实现 400 服务器不理解请求的语法。 401 请求要求身份验证。对于登录后请求的网页,服务器可能返回此响应。 403 表示用户得到授权(与401错误相对),但是访问是被禁止的,服务器收到请求但是拒绝提供服务 404 网页没有发现 406 用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 5开头系列:服务器端错误--服务器未能实现合法的请求 500 内部服务器错误 502 一般是网关服务器请求后端服务时,后端服务没有按照http协议正确返回结果。 503 服务当前不可用, 可能因为超载或停机维护。 504 一般是网关服务器请求后端服务时,后端服务没有在特定的时间内完成服务。 |
protocol常用设置
协议这块http默认就可以了,https的话最好配置下私钥和证书,这样核心系统调用时不会出现警告。
{ "port": 1234, "protocol": "https", "key": "",<!-- SSL服务器私钥 --> "cert": "",<!-- SSL服务器证书 --> ... } |
predicates常用配置
predicate决定存根是否匹配,不管怎样它返回就是一个boolean值,true或false,如果返回true就返回对应的response响应,只要这个清楚,那么下面的所有花哨都很一般。
常用运算符
|运算符 |描述 | |-- |-- | |equals |请求字段与谓词匹配 | |deepEquals |在请求的数据包含某个关键字及对应的值(例如query,http中的字段)| |contains |请求字段包含谓词 | |startsWith |请求字段以谓词开头 | |endsWith |请求字段以谓词结束 | |matches |请求字段与正则表达式匹配。 | |exists |是否存在 | |not |取反 | |or |满足其中一个条件 | |and |同时满足条件 | |inject |注入JavaScript以确定请求是否匹配 | |
常用的条件设置
equals
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "equals": { "path": "/test", "method": "get" } }], "responses": [{ "is": { "body": { "Operator": "equals" } } }] }] } |
浏览器访问后结果http://localhost:8081/test
{ "Operator": "equals" } |
deepEquals
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "deepEquals": { "query": { "key": ["first", "second"] } } }], "responses": [{ "is": { "body": { "Operator": "deepEquals" } } }] }] } |
浏览器访问后结果http://localhost:8081/test?key=second&key=first
{ "Operator": "deepEquals" } |
exists
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "exists": { "method": true, "body": false } }], "responses": [{ "is": { "body": { "Operator": "exists" } } }] }] } |
浏览器访问后结果http://localhost:8081/test
{ "Operator": "exists" } |
还有其他场景判断
"exists": { "method": true,<!-- get或post --> "body": false <!-- 没有body,就是get情况--> } "exists": { "body": true }<!-- post情况 --> |
inject 脚本注入设置
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "inject": "function (config) {return config.request.body.indexOf('inject') === 0; }" } ], "responses": [{ "is": { "body": { "Operator": "inject" } } }] }] } |
通过postman post方法提交,http://localhost:8081/test,提交数据包含inject,返回结果如下:
{ "Operator": "inject" } |
注意 inject的函数在这里不能进行编排,造成编写、调试代码不方便,可以采用下面导入文件的方式解决,包括responses注入也适应。
inject利用stringify导入模板脚本
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "inject": "<%- stringify(filename, './test/inject.ejs') %>" }], "responses": [{ "is": { "body": { "Operator": "inject" } } }] }] } |
inject.ejs脚本
function(config) { return config.request.body.indexOf('mounte') === 0; } |
注意 其他的操作都是对tcp数据的操作,在这里不做过多解释,有需要可以和我沟通或去官网去查帮助。
xpath脚本 是挡板接收xml数据的时候进行条件的判断,由于我们的服务大多是基于restful的,所以这种场景使用不多,全凭烧下大脑,越来越聪明。
{ "port": 8081, "protocol": "http", "stubs": [{ "predicates": [{ "equals": { "body": "Harry Potter" }, "xpath": { "selector": "//title" }, "caseSensitive": true, "comment": "case sensitivity applies to the selector as well as the value" }], "responses": [{ "is": { "body": { "Operator": "xpath" } } }] }] } |
通过postman post方法提交,http://localhost:8081/,提交数据如下:
<books xmlns:isbn="http://schemas.isbn.org/ns/1999/basic.dtd"> <book> <title>Harry Potter</title> <isbn:summary>Dragons</isbn:summary> </book> </books> |
返回的结果如下:
{ "Operator": "xpath" } |
caseSensitive区分大小写,true或false。
selector查找xml那个节点,本示例是查的title节点。
responses常用设置
响应这块主要分三部分:代理模式、脚本注入以及利用shell脚本的动态行为。
代理
代理是mb是responses的一个配置项,与注射相媲美,支持记录、缓存行为,可以对其head、body进行修改, 可轻松捕获测试场景的丰富测试数据集。 代理就是给个地址,代理访问下录制各种场景的响应报文,录制以后我们可以设为静态服务响应数据。
"stubs": [{ "responses": [{ "proxy": { "to": "http://localhost:8083/mockjs" .... } }] }] |
proxy参数如下:
|参数 |默认 |类型|描述| |-- |-- |--|--| |to |需要 |代理的url地址,带http的全路径 |定义请求应代理的源服务器。 | |predicateGenerators|[] |数组 |一组对象,**用于定义如何创建新存根的谓词**,就是为了生成谓词的配置,数组中的每个对象都定义了从中生成谓词的字段。请参阅下面 的示例。 | |mode |proxyOnce|proxyOnce、proxyAlways、proxyTransparent|proxyOnce 相同的条件只访问代理地址一次,获取的响应数据会缓存在本地。proxyAlways缓存所有响应数据,要和mb replay 配合使用,否则缓存数据越来越多,proxyTransparent模式代理请求但不记录任何数据 | |addWaitBehavior |false |布尔|如果为true,则mountebank将以wait 与代理调用相同的延迟为响应添加行为。这在您希望模拟正在虚拟化的下游服务的实际延迟的性能测试场景中非常有用。 | |addDecorateBehavior|null |字符串,JavaScript |对已缓存的响应修改数据 |
从这几个参数开始我们逐步学习下mb的代理功能。
predicateGenerators 请求的条件判断
predicateGenerators 数组中的每个对象都包含以下字段:
|参数 |默认 |类型 |描述 | |-- |-- |-- |-- | |matches |{} |宾语 |用于定义如何创建新存根的谓词,关键是定义、新建,相当于代码生成模板| |caseSensitive|false|布尔 |确定匹配是否区分大小写,包括查询参数等对象的key。| |except |"" |串 |定义在匹配之前从请求字段中剥离的正则表达式。| |xpath |null |宾语 |(业务场景不多,可忽略)定义包含selector字符串的对象,以及可选的ns定义命名空间映射的 对象字段。谓词的范围仅限于请求字段中的选定值。| |jsonpath |null |宾语 |业务场景不多,可忽略)定义包含selector字符串的对象。谓词的范围仅限于请求字段中的选定值。| |
如下面的代码设置matches的一个值为query。
"stubs": [{ "responses": [{ "proxy": { "to": "http://localhost:8083/mockjs", "predicateGenerators": [{ "matches": { "query": { "q": "mountebank" } } }] } }] }] |
那么访问 http://localhost:8082/mockjs?q=mountebank以后生成的谓词如下:
"stubs": [ { "predicates": [{ "equals": { "query": { "q": "mountebank" } } }], "responses": [{ "is": { ... } }] } ] |
如果是 http://localhost:8082/mockjs?q=mountebank1,那么生成的谓词是"query": { "q": "mountebank1" }
mode定义代理的行为
proxyOnce - 确保相同的请求(由谓词定义)永远不会被代理两次。mountebank仅记录每个请求的一个响应,并在下次请求谓词匹配时自动重放该响应。
proxyAlways - 将代理所有呼叫,允许为同一逻辑请求保存多个响应。利用这个模式必须及时把代理删除,否则生成的谓词将很多,
mb replay --port 2525 #命令执行后,代理模式清除,只有录制的谓词提供服务 |
proxyTransparent - 请求访问透传不做记录处理。
injectHeaders修改请求头
顾名思义就是通过代理请求而相应数据的http的head追加一些属性。
"responses": [{ "proxy": { "to": "http://localhost:8083/mockjs", "mode": "proxyOnce", "addWaitBehavior": true, "injectHeaders": { "X-My-Custom-Header-One": "my first value", "X-My-Custom-Header-Two": "my second value" } }}] |
那么会在响应的head上追加了两行数据,具体如下:
Cache-Control →no-cache Postman-Token →5aa8b6d1-c6aa-4660-93b5-af416bb87b55 host →localhost:8083 cookie →BDSVRTM=0 accept-encoding →gzip, deflate Connection →keep-alive X-My-Custom-Header-One →my first value X-My-Custom-Header-Two →my second value Date →Sun, 19 May 2019 15:28:21 GMT Transfer-Encoding →chunked |
mb考虑的很周到,通过代理可以录制谓词,录制的数据可以提供修改http的head、body,那么下面就是介绍下怎么修改响应的数据了。
addDecorateBehavior修改已保存的响应
"responses": [{ "proxy": { "to": "http://localhost:8083/mockjs" }, "addDecorateBehavior": "<%- stringify(filename, './test/proxy.ejs') %>" } }] //proxy.ejs代码,也是通过EJS模板调用,便于代码熟悉及维护 function(request, response) { response.body = response.body + ' DECORATED!123456'; console.log("function(request, response)"); } |
那么生成的代码如下:
"_behaviors": { "decorate": "function(request, response) {\r\n\tresponse.body = response.body + ' DECORATED!123456';\n\tconsole.log(\"function(request, response)\");\r\n}" } |
addWaitBehavior
还有一个重要的参数,那就是addWaitBehavior,当为true时会记录代理请求的响应时间,方便录制的数据回放时比较逼真,说白了就是完全模拟回访。
"responses": [{ "proxy": { "to": "http://localhost:8083/mockjs", "addWaitBehavior": true } }] |
那么生成的代码
"_behaviors": { "wait": 534 } //534就是响应的时间 |
最后mb的代理就这样结束了,为了全面的了解,我把它贴出来:
//代理demo { "port": 8082, "protocol": "http", "stubs": [{ "predicates": [{ "deepEquals": { "path": "/mockjs" } }], "responses": [{ "proxy": { "to": "http://localhost:8083/mockjs", "mode": "proxyOnce", "addWaitBehavior": true, "injectHeaders": { "X-My-Custom-Header-One": "my first value", "X-My-Custom-Header-Two": "my second value" }, "predicateGenerators": [{ "matches": { "query": { "q": "mountebank" } } }], "addDecorateBehavior": "<%- stringify(filename, './test/proxy.ejs') %>" } }] } ] } |
http://localhost:8082/mockjs?q=mountebank2访问生成的谓词代码:
{ "predicates": [{ "equals": { "query": { "q": "mountebank2" } } }], "responses": [{ "is": { "statusCode": 200, "headers": { "User-Agent": "PostmanRuntime/7.11.0", "Accept": "*/*", "Cache-Control": "no-cache", "Postman-Token": "5aa8b6d1-c6aa-4660-93b5-af416bb87b55", "host": "localhost:8083", "cookie": "BDSVRTM=0", "accept-encoding": "gzip, deflate", "Connection": "keep-alive", "X-My-Custom-Header-One": "my first value", "X-My-Custom-Header-Two": "my second value", "Date": "Sun, 19 May 2019 15:28:21 GMT", "Transfer-Encoding": "chunked" }, "body": "{\n \"head\": {\n \"rspcode\": \"200\"\n },\n \"body\": {\n \"city\": \"山南地区\"\n }\n}", "_mode": "text", "_proxyResponseTime": 534 }, "_behaviors": { "wait": 534, "decorate": "function(request, response) {\r\n\tresponse.body = response.body + ' DECORATED!123456';\n\tconsole.log(\"function(request, response)\");\r\n}" } }] } |
脚本注入
前面我们说了谓词的注入,其实就是大同小异。 那么我们看下它的参数属性有哪些
|领域|描述| |--|--| |request|请求数据都在这| |state|mountebank全局变量,每次重启mb都会清空| |callback|一般情况下函数有返回值,如果没有那就是通过异步处理,此时必须使用callback调用该参数。| |logger|log句柄的引用,在函数中可以直接使用输出日志|
那么我们先看下代码:
{ "port": 8084, "protocol": "http", "stubs": [{ "responses": [{ "inject": "<%- stringify(filename, './test/responses.ejs') %>" }] }] } |
注入模板脚本responses.ejs如下:
function(config) { config.logger.info('origin called'); config.state.requests = config.state.requests || 0; config.state.requests += 1; var ret={ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: config.state.requests }) }; return ret; } |
其实从代码我们可以看出,上面的参数都是通过config这个对象传递给模板脚本的
state可以存放json数据、基本数据类型等。
如果修改为callback那么代码如下:
function(config) { config.logger.info('origin called'); config.state.requests = config.state.requests || 0; config.state.requests += 1; var ret={ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: config.state.requests }) }; config.callback(ret); } |
动态行为
_behaviors这个参数通过上面的讲解,应该大家了解一二,他的任务就是修改响应的数据,那么他的行为如下:
_behaviors这个参数通过上面的讲解,应该大家了解一二,他的任务就是修改响应的数据,那么他的行为如下: |行为 |描述 |-- |-- |wait |响应延迟毫秒数| |repeat |在转到下一个响应之前,重复一定次数的响应。| |copy |将请求字段中的一个或多个值复制到响应中。您可以使用正则表达式,xpath或jsonpath标记响应并从请求字段中选择值。| |lookup |从请求的数据中作为key值从外部数据源去取数据。| |decorate |**--allowInjection启动参数必须加**在发送之前使用JavaScript注入对响应进行后处理。| |shellTransform |**--allowInjection启动参数必须加**,不是使用JavaScript注入,而是将其发送到另一个应用程序,另一应用通过stdout输出JSON格式传递数据回来。 |
那么我们接下来对上面的参数详细说明下:
wait参数
{ "port": 8085, "protocol": "http", "stubs": [ { "responses": [ { "is": { "body": "This took at least half a second to send" }, "_behaviors": { "wait": 500 } } ] } ] } |
通过http://localhost:8085访问后:
This took at least half a second to send |
repeat参数
{ "port": 8085, "protocol": "http", "stubs": [ { "responses": [ { "is": { "body": "This will repeat 2 times" }, "_behaviors": { "repeat": 2 } }, { "is": { "body": "Then this will return" } } ] } ] } |
通过http://localhost:8085访问后:
//第一次结果 This will repeat 2 times //第二次结果 This will repeat 2 times //第三次结果 Then this will return |
从上线的结果可以看出,如果没有"repeat": 2话应该是轮询显示结果。加了以后就可以访问多少次后再往下轮询。
copy参数
{ "port": 8085, "protocol": "http", "stubs": [ { "responses": [ { "is": { "body": "The request name was ${name}. Hello, ${name}!" }, "_behaviors": { "copy": [ { "from": { "query": "name" }, "into": "${name}", "using": { "method": "regex",<!-- 用到正则表达式 --> "selector": "MOUNT\\w+$", "options": { "ignoreCase": true }<!-- 不区分大小写 --> } } ] } } ] } ] } |
上面的代码其实就是利用请求的参数中,查找包含以mount开头的数据,然后把它替换掉name变量, 通过http://localhost:8085/400?ignore=this&name=1mountebank23访问以后,因为包含1mountebank23值,所以显示结果如下:
The request name was mountebank23. Hello, mountebank23! |
如果访问http://localhost:8085/400?ignore=this&name=1mou1ntebank23,因为没有匹配 那么结果如下:
The request name was ${name}. Hello, ${name}! |
lookup参数
这个参数就是从请求的数据中,取出某个值作为查询条件去外部数据源查找对应的值。 代码如下:
{ "port": 8085, "protocol": "http", "stubs": [ { "responses": [ { "is": { "body": "Hello ${row}['Name'], have you done your ${row}['jobs'] today?" }, "_behaviors": { "lookup": [{<!-- 查找开始 --> "key": { "from": "path",<!-- 从请求路径中查找 --> "using": { "method": "regex", "selector": "/(.*)$" },<!-- 根据斜杠匹配数据 --> "index": 1<!-- 0会是原路径,1是去除/的值--> }, "fromDataSource": { "csv": { "path": "./test/values.csv", "keyColumn": "Name" } }, "into": "${row}" }] } } ] } ] } |
那么外部数据源的文件内容如下:
values.csv State_ID,code,Name,price,tree,jobs 1111111,400,liquid,235651,mango,farmer 9856543,404,solid,54564564,orange,miner 2222222,500,water,12564,pine,shepherd 1234564,200,plasma,2656,guava,lumberjack 9999999,200,lovers,9999,dogwood,steel worker |
那我通过http://localhost:8085/water访问时结果如下:
Hello water, have you done your shepherd today? |
简单说明下为什么上面是index是1,懂正则的人一看就明白,但对初学者还是一个解释好,
如果路径是/water 通过下面的表达式:
"using": { "method": "regex", "selector": "/(.*)$" } |
得到的其实是个数组{"/water",water}
index为1就取得值为water
得到water后,然后再去values.csv文件获取到对应的行数据。
decorate参数
咱们前面在说代理是说到一个属性addDecorateBehavior,它就可以生成对应的decorate,它的的目的就是动态修改或追加响应的数据。 就是利用javascript脚本注入的方式来完成。
代码上来再说哈!
{ "port": 8085, "protocol": "http", "stubs": [ { "responses": [ { "is": { "body": "The time is ${TIME}" }, "_behaviors": { "decorate": "(config) => { var pad = function (number) { return (number < 10) ? '0' + number : number.toString(); }, now = new Date(), time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); config.response.body = config.response.body.replace('${TIME}', time); }" } } ] } ] } |
通过http://localhost:8085访问后,结果如下:
The time is 23:06:29 |
当然也可以引入模板:
"_behaviors": { "decorate": "<%- stringify(filename, './test/behaviors.ejs') %>" } |
behaviors.ejs模板文件的内容:
function(config) { var pad = function(number) { return (number < 10) ? '0' + number: number.toString(); }, now = new Date(), time = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); config.response.body = config.response.body.replace('${TIME}', time); } |
shellTransform参数
shellTransform行为类似于decorate,可以对响应数据进行修改。但不同的地方是它通过shell命令调用外部的脚本, 执行时他会把request、response作为参数传给外部脚本,外部脚本拿到后可以对响应的数据进行修改,然后输出到控制台,这样mb就可以达到对应的值,千万注意控制台打印的是json格式。
具体代码如下:
{ "port": 8086, "protocol": "http", "stubs": [{ "responses": [{ "is": { "body": "Hello, ${city}!" }, "_behaviors": { "shellTransform": ["node ./test/shellTransform.js"] } }] }] } |
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。