wiremock-py

wiremock-py 是基于WireMock实现的, 使用Python批量生成不同 测试场景 下不同HTTP API的 mock 数据, 然后作为mock server快速全面地对 API 进行测试。

背景

在数澜地产应用的前端测试中, 前端一般依赖于后端的数据, 前端通过后端在网关上发布的 HTTP API 获取数据. 要对前端进行充分的测试, 理想的做法是, 等待后端部署完成, 并且在数据层直接输入不同类型的数据源, 然后前端直接调用后端发布在网关上的 API 进行测试。

然而现实的情况是, 前端和后端的开发进度不完全一致, 如果前端先开发完成了, 必须要等后端对应的 API 开发完成后才能开始测试, 而且数据层的数据也不容易构造.

为了解决这个问题, 网关平台做了简单的 mock 功能, 每个 API 可以填写一个 mock数据, 然后前端调用 API 时直接使用这个 mock数据:

这种方式下, 网关充当了mock server:

但由于大家都使用同一个网关, 一个 API 只能保存一份 mock 数据, 所以有以下一些缺点:

  • 不同的测试场景需要不同的 mock 数据来测试, 此时需要删掉上个测试场景的 mock 数据, 再创建新场景的 mock 数据才能进行测试

  • 不能根据测试场景来按照一定的规则动态生成 API 对应的 mock 数据

  • 不能多人同时使用测试同一个 API时, 只能都使用同一份 mock 数据, 不能各用各的

wiremock-py 可以解决上述这些问题: wiremock-py 通过传入不同的测试场景参数来生成不同的 mock 数据, 同时不同测试场景下使用的 mock 数据可以保存起来; 生成 mock 数据时, wiremock-py 支持使用Pythonjs代码来动态生成 mock 数据(也支持直接使用 json 数据, 如果 mock 数据中的数据量很大, 人工手写 mock 时的数据量会很大, 使用代码生成则比较容易); 不同的测试人员使用各自自己的 mock server, 不会影响到其他测试人员的测试。

测试人员需要做的是: 确定哪些 API 需要进行 mock 以及不同测试场景下对应的 mock 规则是什么。

依赖环境

  • Java 1.8.0_144

  • Node v8.6.0

  • Python 3.4.3

演示

快速开始

以贸数v1.1.0版本 测试环境为例演示使用 wiremock-py 对楼层客流分布店铺客流分布两张图分布在3种场景下的测试方法

先确定本地浏览器能过正常访问 http://mall-data.com:9012

准备

克隆代码

git clone http://git.dtwave-inc.com:30000/baomi.wbm/wiremock-py.git

安装依赖

cd wiremock-py

pip install -r requirements.txt

npm install mockjs

生成目录

python mock.py -g "demo"

➜  wiremock-py git:(master) ✗ python mock.py -g "demo"
DEBUG:root:mockdir=, scene=, target=, proxy_port=5506, generate=demo, wiremock=False, rewrite=False
DEBUG:root:正在生成目录 /Users/wangbaomi/autotest/wiremock-py/demo
DEBUG:root:创建目录成功: demo
DEBUG:root:创建目录成功: demo/js
DEBUG:root:创建目录成功: demo/json
DEBUG:root:创建目录成功: demo/python
DEBUG:root:创建目录成功: demo/wiremock
DEBUG:root:创建文件成功: demo/mappings.json
DEBUG:root:生成目录完成: /Users/wangbaomi/autotest/wiremock-py/demo

编写 mock 规则

填写 mappings.json、json、python、js 数据

mappings.json 中填写内容:

[{"response": {"default": {"proxyBaseUrl": "target"}},"mapping_name": "request url not start with /api","request": {"method": "ANY","urlPattern": "/(?!api).*"}}, {"mapping_name": "楼层客流分布","request": {"urlPattern": "/api/v1/mall_data/customer_flow/every_floor\\?(.*)","method": "POST"},"response": {"default": {"proxyBaseUrl": "target"},"测试场景1": {"bodyFileName": {"json": "楼层客流分布.json"}},"测试场景2": {"bodyFileName": {"python": "楼层客流分布.py","python_args": "测试场景2"}},"测试场景3": {"bodyFileName": {"js": "楼层客流分布.js"}}}},{"mapping_name": "店铺客流分布","request": {"urlPattern": "/api/v1/mall_data/customer_flow/every_shop\\?(.*)","method": "POST"},"response": {"default": {"proxyBaseUrl": "target"},"测试场景1": {"bodyFileName": {"js": "店铺客流分布.js"}},"测试场景2": {"bodyFileName": {"json": "店铺客流分布.json"}},"测试场景3": {"bodyFileName": {"python": "店铺客流分布.py","python_args": "测试场景3"}}}}
]

js 文件夹中新建店铺客流分布.js文件, 内容为:

var r = {"success": true,"code": null,"message": null,"content": {"meta": {},"multi": {"group": [{"id": "rank","name": "排名","value": [1,2,3,4]}],"result": [{"id": "the_shop","name": "店铺","value": ["店铺1","店铺2","店铺3","第4个店铺"]},{"id": "customer_count","name": "人数","value": [10,100,1000,3242]}]},"single": []}
};console.log(JSON.stringify(r));

js 文件夹中新建楼层客流分布.js文件, 内容为:

var r = {"success": true,"code": null,"message": null,"content": {"meta": {},"multi": {"group": [{"id": "the_floor","name": "楼层","value": ["-1楼","1楼","2楼","3楼",]}],"result": [{"id": "customer_count","name": "人数","value": [100,1000,5000,567]}]},"single": []}
};console.log(JSON.stringify(r));

json 文件夹中新建店铺客流分布.json, 内容为:

{"success": true,"code": null,"message": null,"content": {"meta": {},"multi": {"group": [{"id": "rank","name": "排名","value": [1,2,3]}],"result": [{"id": "the_shop","name": "店铺","value": ["店铺1","店铺2","店铺3"]},{"id": "customer_count","name": "人数","value": [10,100,1000]}]},"single": []}
}

json 文件夹中新建楼层客流分布.json, 内容为:

{"success": true,"code": null,"message": null,"content": {"meta": {},"multi": {"group": [{"id": "the_floor","name": "楼层","value": ["1楼","2楼","3楼"]}],"result": [{"id": "customer_count","name": "人数","value": [100,1000,5000]}]},"single": []}
}

python 文件夹中新建店铺客流分布.py, 内容为:

#coding: utf-8resp = {"success": True,"code": None,"message": None,"content": {"meta": {},"multi": {"group": [{"id": "rank","name": "排名","value": []}],"result": [{"id": "the_shop","name": "店铺","value": []},{"id": "customer_count","name": "人数","value": []}]},"single": []}
}def main(scene):if scene == "测试场景1":resp["content"]["multi"]["group"][0]["value"] = [1,2,3,4,5]resp["content"]["multi"]["result"][0] = {"id": "the_shop","name": "店铺","value": ["店铺1", "店铺2", "店铺3", "店铺4", "店铺5"]}resp["content"]["multi"]["result"][1] = {"id": "customer_count","name": "人数","value": ["100", "150", "800", "999", "1889"]}return respelif scene == "测试场景2" or scene == "测试场景3":resp["content"]["multi"]["group"][0]["value"] = [1,2,3,4,5,6,7,8,9]resp["content"]["multi"]["result"][0] = {"id": "the_shop","name": "店铺","value": ["店铺1", "店铺2", "店铺3", "店铺4", "店铺5", "店铺_6", "店铺⑦", "店铺Ⅷ"]}resp["content"]["multi"]["result"][1] = {"id": "customer_count","name": "人数","value": ["100", "150", "800", "999", "1889", "4455", "788", "3458"]}return respelse:return {"error": "No such scene: " + str(scene)}

python 文件夹中新建楼层客流分布.py, 内容为:

#coding: utf-8resp = {"success": True,"code": None,"message": None,"content": {"meta": {},"multi": {"group": [{"id": "the_floor","name": "楼层","value": ["1楼","2楼","3楼"]}],"result": [{"id": "customer_count","name": "人数","value": [100,1000,5000]}]},"single": []}
}def main(scene):if scene == "测试场景1":resp["content"]["multi"]["group"][0]["value"] = ["一楼", "二楼", "三楼", "四楼"]resp["content"]["multi"]["result"][0] = {"id": "the_shop","name": "店铺","value": [1,2,3,4,5]}return respelif scene == "测试场景2":resp["content"]["multi"]["group"][0]["value"] = ["一楼", "二楼", "三楼", "四楼", "5lou", "6lou"]resp["content"]["multi"]["result"][0] = {"id": "the_shop","name": "店铺","value": [1,2,3,4,5,100,500]}return respelse:return {"error": "No such scene: " + str(scene)}

测试不同场景下图表的显示情况

测试场景1

python mock.py --mockdir "demo" --scene "测试场景1" --target "http://mall-data.com:9012" --proxy_port 5506

浏览器访问 http://localhost:5506 来看看楼层客流分布店铺客流分布这两张图中的数据

测试场景2

python mock.py --mockdir "demo" --scene "测试场景2" --target "http://mall-data.com:9012" --proxy_port 5506

浏览器访问 http://localhost:5506 来看看楼层客流分布店铺客流分布这两张图中的数据的变化

测试场景3

python mock.py --mockdir "demo" --scene "测试场景3" --target "http://mall-data.com:9012" --proxy_port 5506

浏览器访问 http://localhost:5506 来看看楼层客流分布店铺客流分布这两张图中的数据的变化

功能介绍

目录结构

wiremock-py||__example                              // 示例目录||__Jar  |   ||   |__wiremock-standalone-2.8.0.jar    // wiremock 文件||__mockjs|   ||   |__mock.js                          // 用于支持 json 数据中的使用 mock.js 规则||__mock.py                              // 生成目录结构已经运行 mock server||__readme.md||__.gitignore||__requirements.txt

功能

生成 mock 数据目录结构

python mock.py -g 目录名称

生成的目录结构是这样:

mockdir|__mappings.json              // 保存当前测试环境下所有request和response的映射关系, 即 mock 规则|__python                     // 保存python代码, python chart_x.py --场景1 这样的调用方式可以返回一个json字符串|  |_chart1.py                // 这个json字符串将作为response的body体保存在 wiremock/某个场景目录/__files 目录下|  |_chart2.py                |__js                         // 保存js代码, 作用类似于python代码, 最终生成json数据|  |_chart3.js                // 最终的json数据也是作为response的body体保存在 wiremock/某个场景目录/__files 目录下|  |_chart4.js|__json                       // 保存json数据, json数据支持mock.js规则, |  |_chart5.json              // 最终的json数据也是作为response的body体保存在 wiremock/某个场景目录/__files 目录下|  |_chart6.json|__wiremock                   // 最终wiremock读取的目录|  |__scene1                  // 场景目录|  |  |__ mappings            // request和response的映射关系|  |  |  |_mapping1.json|  |  |  |_mapping2.json|  |  |__ __files             // response中的body体内容|  |  |  |_chart1.json|  |  |  |_chart2.json

按照 mock 规则生成数据并运行 mock server (如果规则已生成数据, 不覆盖)

python mock.py --mockdir "demo" --scene "测试场景1" --target "http://mall-data.com:9012" --proxy_port 5506

--mockdir 为目录名称

--scene 为测试场景名称, mock.py 根据 scene 参数, 在 mappings.json 中选择不同的场景来返回不同的 response 数据, 比如 scene 指定为 “场景2”时, 会去 mappings.json中找所有规则中 response 在 “场景2” 下的应该返回的数据. 当指定的scene不存在时, 会使用”default”场景

--target 为目标 url, 如果 API 在某个测试场景下不需要 mock 就会请求这个 target 地址

--proxy_port mock server 端口号

按照 mock 规则生成数据并运行 mock server (如果规则已生成数据, 覆盖)

python mock.py --mockdir "demo" --scene "测试场景1" --target "http://mall-data.com:9012" --proxy_port 5506 --rewrite

不需要生成数据, 只使用已有数据运行 mock server

python mock.py -w 目录名称

原理

mappings.json 中描述了每个 API 在每个 测试场景 下生成 mock 数据的 规则, 规则可以描述为 json 形式, 或者 python 和 js 代码的形式

文件内容格式为:

[{"mapping_name": "非图表请求","request": {"urlPattern":"/(?!api).*","method": "ANY"},"response": {"default": {"proxyBaseUrl": "target"},"场景2": {"status": 200}} }
]

mapping_name

mapping_name为映射关系名称, 只是个名称而已

request

request 为要拦截的request请求格式, 当匹配到特定匹配条件的request后, 返回指定的 response.

匹配 request 的 url: 使用urlPattern参数, 支持正则表达式

匹配 request 方法: 使用method参数, 比如 GET, POST, PUT, DELETE, HEAD, TRACE, OPTIONS, 要匹配所有的请求方法可使用ANY

匹配 request 中的 JSON 数据, 使用equalToJson来完全匹配 json 数据:

"bodyPatterns" : [ {"equalToJson" : { "total_results": 4 }
} ]

使用matchesJsonPath来匹配 json 路径:

"bodyPatterns" : [ {"matchesJsonPath" : "$.name"
} ]
"bodyPatterns" : [ {"matchesJsonPath" : "$.things[?(@.name == 'RequiredThing')]"
} ]
"bodyPatterns" : [ {"matchesJsonPath" : "$.things[?(@.name =~ /Required.*/i)]"
} ]
"bodyPatterns" : [ {"matchesJsonPath" : "$[?(@.things.size() == 2)]"
} ]
"bodyPatterns" : [ {"matchesJsonPath" : {"expression": "$.outer","equalToJson": "{ \"inner\": 42 }"}
} ]

request匹配格式可以参考wiremock的Request Matching部分

注意细节

如果 mappings.json 里所有的mapping规则都无法匹配某个request, 这个request对应的response将会是404, 而不是走target的真实请求.

比如 一个 request 为 http://localhost:5506/abcde, target 为 https://mall-data.com, 当 mappings.json 里能够匹配到这个 request 时, 就返回匹配规则中的 response (可以是返回某个特定的信息, 也可能是返回 target 真实的返回数据, 视 response 信息而定), 但如果 mappings.json 中没有匹配到这个 request, 那这个 request 的 response 值就只能是404.

response

response为要返回的response数据, response里需要指定 测试场景 的名称, 比如 默认场景 下的response是啥, 场景2 下的response是啥.

response 的格式, 可以参考wiremock的stubbing文档 和 simulating-faults文档

response 中支持使用json文件

如果想直接返回一个json文件里的json数据, 可以指定 "json" 为目标文件(目标json文件必须放在 json 目录中)

{"mapping_name": "客流变化趋势","request": {"urlPattern": "/api/v1/mall_data/overview/customer_day\\?(.*)","method": "POST"},"response": {"默认场景": {"bodyFileName": {"json": "客流变化趋势.json"} }}}

json文件内容可参考:

{"success": true,"code": null,"message": null,"content": {"meta": {},"multi": {},"tab": {"currentValue": 99999,"chainValue": -0.22916559652349008}}
}

json数据会自动转换成按照mock.js规则转换后的json数据

比如json数据:

"result": [{"value": ["@integer(1,10000)","@integer(1,10000)"],"id": "customer_count","name": "\u4eba\u6570"}
]

会按照mock.js规则转换成:

"result": [{"value": [7798,1768],"name": "\u4eba\u6570","id": "customer_count"}
]
response 中支持使用python代码生成数据

如果想使用python代码根据不同场景生成不同的数据作为 response 的话, 可以指定 "python"为指定的 目标python文件(目标python文件必须放在 python 目录中), "python_args" 为运行指定 python 代码时传的参数

    {"mapping_name": "老客数","request": {"urlPattern": "/api/v1/mall_data/overview/customer_old\\?(.*)","method": "POST"},"response": {"默认场景": {"status": 200,"bodyFileName": {"python": "老客数.py","python_args": "默认场景"}}}}

python代码中, 必须包含一个 main(scene) 函数, 内容可参考:

def main(scene):return {"kk": scene}
response 中支持使用js代码生成数据

如果想使用js代码根据不同场景生成不同的数据作为 response 的话, 可以指定 "js" 为指定的 目标js文件(目标js文件必须放在 js 目录中), "js_args" 为运行指定 js 代码时传的参数

    {"mapping_name": "老客数","request": {"urlPattern": "/api/v1/mall_data/overview/customer_old\\?(.*)","method": "POST"},"response": {"默认场景": {"status": 200,"bodyFileName": {"js": "老客数.js","js_args": "默认场景"}}}}

python 代码中会使用Naked运行 js 脚本, 运行方式就是 muterun_js("js_file args"), 然后从 js 脚本的标准输出来拿到返回数据, 所以js文件需要能够从命令行中读取参数, 并且能过将返回结果打印到标准输出中, 可参考:

if (process.argv[2] == "测试场景1"){console.log({"k1":"v1"})
}
response 中指定返回状态码

使用status参数返回指定的状态码, 如果不指定, 则返回200

response 中指定返回头部内容

使用headers参数返回指定的头部内容, 如果不指定, 代码会默认指定一个头部内容

response 中返回 base64 数据

使用base64Body参数返回指定的数据

wiremock

wiremock 的高级功能可参考 wiremock文档

mock.py 按照 mappings.json 中的规则, 会在 wiremock 目录中生成 __files 和 mappings两个目录, 然后拉起 wiremock, wiremock 会按照这两个目录中的数据来做 mock server.

参考资料

wiremock: http://wiremock.org/

作者简介:咪咪,5年+软件测试经验,参与过深信服VDI产品的自动化测试和测试开发,以及华为公有云的自动化测试。

Mock工具wiremock-py相关推荐

  1. python 项目构建工具_python的构建工具setup.py

    一.构建工具setup.py的应用场景 在安装python的相关模块和库时,我们一般使用 "pip install 模块名" 或者 "python setup.py in ...

  2. python的构建工具setup.py

    一.构建工具setup.py的应用场景 在安装python的相关模块和库时,我们一般使用"pip install  模块名"或者"python setup.py inst ...

  3. Mock工具之Mockito实战

    在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行.为了解决这类问题我们引入 ...

  4. 说说初用 Mock 工具测试碰到的坑

    说说初用 Mock 工具测试碰到的坑 我是一个在校实习生,作为一个程序猿,是个菜鸟中战斗机!对于测试,只写过一点点简单到不能再简单了的 Junit 单元测试的例子(因为当时这足以应付学校课程的内容与要 ...

  5. 史上最轻量​!阿里新型单元测试Mock工具开源了

    简介:为了探索更轻量易用的Mock测试手段,阿里云云效团队尝试给工具减负,在主流Mock工具的基础上让Mock的定义和置换干净利落,最终设计了一款极简风格的测试辅助工具TestableMock,无需初 ...

  6. mock java_JAVA的mock工具mockito简介

    在测试过程中,难免会碰到交互的外围系统不给力的情况,这时候mock就派上用场了,前段时间跟同学聊到这块的时候,他向我推荐mockito这个mock工具,试用了一下,确实很好用,这里给大家介绍下这款工具 ...

  7. 测试私有方法_史上最轻量!阿里开源了新型单元测试Mock工具

    点击上方蓝色字体,选择"设为星标" 回复"666"获取面试宝典 TestableMock是基于源码和字节码增强的Java单元测试辅助工具,包含以下功能: 访问被 ...

  8. Python 的构建工具 setup.py

    一.构建工具setup.py的应用场景 在安装python的相关模块和库时,我们一般使用"pip install  模块名"或者"python setup.py inst ...

  9. Python 代码覆盖率统计工具 coverage.py

    Python 代码覆盖率统计工具 coverage.py coverage.py是一个用来统计python程序代码覆盖率的工具.它使用起来非常简单,并且支持最终生成界面友好的html报告.在最新版本中 ...

  10. libsvm——参数优化工具grid.py的使用

    工具grid.py主要通过交叉验证的方法求最优的核函数参数C和gamma. 参考来源:http://blog.csdn.net/flydreamgg/article/details/4470477 一 ...

最新文章

  1. 专业的java培训机构是否靠谱,对比一下就知道了!
  2. “深度学习”这十年:52篇大神级论文再现AI荣与光
  3. 推荐:微服务架构的深度解析!
  4. 《编写高质量代码:改善Java程序的151个建议》笔记
  5. 如何正确使用SqlConnection
  6. 【数据结构与算法】之深入解析“课程表II”的求解思路与算法示例
  7. Objective-C设计模式(MVC)的实现,以及协议与委托的运用
  8. 端口如何支持非localhost访问_新特性解读 | MySQL 8.0.19 支持 DNS SRV
  9. 因子分析——matlab
  10. USA 2008 Presidential Debate
  11. EasyUI:datagrid重置排序按钮状态
  12. Unity3D 脚本实现动画效果
  13. csrf 功能 及 csrf装饰器使用
  14. ADS1292R的使用
  15. 09:整型与布尔型的转换
  16. SSM+图书馆电子文件资源管理 毕业设计-附源码091426
  17. 堆及堆排序(超超超超超详细讲解~~~~)-----数据结构
  18. MID:为3G而生,因3G引爆流行
  19. Qt编写地图综合应用22-动态轨迹
  20. 【 win10多用户登录 】win10环境下实现非企业版多用户登录

热门文章

  1. jeecgboot:主表(antd table)默认选中第一条记录
  2. CentOS 7 找回root密码
  3. 有一种数叫回文数,正读和反读都一样,如12321便是一个回文数。编写一个程序,从命令行得到一个整数,判断该数是不是回文数。
  4. java jar 指定路径_java – 指定jar的类路径
  5. 5G凭什么比4G快那么多?
  6. Simulink永磁同步电机控制仿真系列五:使用滑模观测器的反电动势法位置估计
  7. 你为什么选择计算机这个专业英语,英文作文:为什么选择计算机作为你的专业...
  8. AAAI 2023 Fello名单出炉,李学龙教授完成AI 领域Fellow大满贯!
  9. loadrunner入门教程(11)--回放脚本
  10. 《算法零基础100讲》(第42讲) 位运算 (位与) 入门