写点什么

HTTPServerMock从手工到平台的演变

2015 年 10 月 21 日

不管是 Web 系统、还是移动 APP,各自在与内部、外部系统之间进行数据交互时,大多数情况下都是依赖接口。在基于接口约定开发的模式下,依赖接口的产出时间如果延迟,将直接影响了整个研发调试的效率;如果不能对接口进行及早测试,那发现问题的时间就要被推迟了。既然双方约定了接口格式,为何不按照这个规范直接测试,何必在乎依赖接口什么时候产出,优先做到及早自测,后续只要替换接口联调通过即可。本文主要讲解基于 HTTP 协议的 API 接口模拟,从手工 Mock 到平台的演变过程。

遇到的困惑

曾经遇到的困扰:在研发过程中接口调试对接难的问题:

场景一:

【需求阶段】新功能开发,Portal 依赖计费的接口,双方约定基于接口开发(内部、外部依赖接口场景均通用)

【开发阶段】Portal 在开发进行中,计费尚未开发完毕,Portal 迟迟不能与计费对接调试(也有可能版本迭代步伐不一致的情况),测试阶段一直被推迟;

另外,即使计费接口开发完毕,Portal 需要修改计费约定的接口数据进行调试,当发现没有对方接口权限或者计费没有过多人力资源来配合时,也无法进入更丰富的数据细节调试;

【测试阶段】测试人员无法及早介入到调试阶段进行接口测试,造成发现缺陷的最佳时期被推迟;

场景二:

【需求阶段】Portal 前、后端约定基于接口开发

【开发阶段】前端开发完毕,后端接口尚未开发完毕,前端只能硬编码数据进行测试,造成接口对接调试延后,而且每次进行更多场景的数据调试,需要频繁重启服务、本地部署;

研发自测阶段无法及早开展,依赖接口约束大。

场景三:

【需求阶段】移动 APP 项目依赖后端获取带宽数据的接口

【开发阶段】移动 APP 端通过后端系统 API 获取带宽数据,绘制带宽图,APP 端绘图工具开发完毕,后端 API 带宽接口尚未开发完毕,移动 APP 端只能硬编码数据进行测试,造成对接延后,每次进行更丰富的数据调试,需要频繁重启服务、本地部署;

研发自测阶段无法及早开展,依赖接口约束大。

总而言之,如图所示:

依赖接口开发完毕,才能够进入到接口联调测试阶段,即使 Portal 的功能开发已经完成,也无法进行自测联调,消耗的等待时间代价是不可估量的,效率低,。

图 -1- 传统的接口对接调试流程

野蛮的石器时代:手工作坊 -Nginx 反向代理

要解决在研发过程中接口对接调试难的问题,无非是所需即所有,减少等待时间,增加研发自测环节,同时也让测试及早参与进来,因此需要能够把依赖接口模拟出来(白盒方面的 Mock 有许多解决方案,这里主要讲的是基于 HTTP 请求的 API Server Mock),以便提高生产效率,改进流程如图所示:

图 -2- 改进的接口对接调试流程

当前最简单的想法是要解决:基于 HTTP 请求、固定 url、能够正则匹配,在这个需求的驱动下,通过 Nginx 的反向代理能够解决问题。

匹配具体路径下某 html 文件

复制代码
location ~ ^/live/(.*)\.html$ {
root /home/htmlfile/ms;
}
location ~ ^/live/([A-Z0-9]+)$ {
}

定义具体返回码

复制代码
location ~ ^/schedule/.*\.(json)$ {
error_page 404 /404.html;
}

请求 http://info.schedul.com/schedule/1234.json 返回 404。

定义其它状态码也是同样道理:

复制代码
error_page 403 /error/403.html;
error_page 500 501 502 503 504 /error/500.html;

俗话说:术业有专攻,Nginx 并不擅长做 Mock API 的工具,在管理配置文件即使可以通过 svn 进行管理,依然是维护比较困难,对于不熟悉 Nginx 的测试工程师,也有一定的学习成本。

拿来主义:不重复造轮子- 开源WireMock

经历了Nginx 的配置繁琐,决定另寻新路,有开源的WireMock( http://wiremock.org/):

Ø WireMock 是一个灵活的库,用于 Web 服务测试,和其他测试工具不同的是:WireMock 创建一个实际的 HTTP 服务器来运行你的 Web 服务以方便测试;

Ø 支持 HTTP 响应存根、请求验证、代理 / 拦截、记录和回放;

创建一个基于 WireMock 的 JavaProject(运行在 tomcat 下管理):

图 -3-ServerMock Project

web.xml 配置如下:

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9"
version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<listener>
<display-name>wiremock-startup-listener</display-name>
<listener-class>com.github.tomakehurst.wiremock.servlet.
WireMockWebContextListener</listener-class>
<description>Loads WireMock and populates the servlet
context with its services</description>
</listener>
<context-param>
<param-name>WireMockFileSourceRoot</param-name>
<param-value>/WEB-INF/wiremock</param-value>
</context-param>
<context-param>
<param-name>verboseLoggingEnabled</param-name>
<param-value>false</param-value>
</context-param>
<servlet>
<servlet-name>wiremock-mock-service-handler-servlet</servlet-name>
<servlet-class>com.github.tomakehurst.wiremock.jetty6.
Jetty6HandlerDispatchingServlet</servlet-class>
<init-param>
<param-name>RequestHandlerClass</param-name>
<param-value>com.github.tomakehurst.wiremock.http.
StubRequestHandler</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>wiremock-mock-service-handler-servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>wiremock-admin-handler-servlet</servlet-name>
<servlet-class>com.github.tomakehurst.wiremock.jetty6.
Jetty6HandlerDispatchingServlet</servlet-class>
<init-param>
<param-name>RequestHandlerClass</param-name>
<param-value>com.github.tomakehurst.wiremock.http.
AdminRequestHandler</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>wiremock-admin-handler-servlet</servlet-name>
<url-pattern>/__admin/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.json</welcome-file>
<welcome-file>index.xml</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.txt</welcome-file>
</welcome-file-list>
<mime-mapping>
<extension>json</extension>
<mime-type>application/json</mime-type>
</mime-mapping>
<mime-mapping>
<extension>xml</extension>
<mime-type>application/xml</mime-type>
</mime-mapping>
<mime-mapping>
<extension>html</extension>
<mime-type>text/html</mime-type>
</mime-mapping>
<mime-mapping>
<extension>txt</extension>
<mime-type>text/plain</mime-type>
</mime-mapping>
</web-app>

web.xml 的这项配置可以改变源文件位置

复制代码
<context-param>
<param-name>WireMockFileSourceRoot</param-name>
<param-value>/WEB-INF/wiremock</param-value>
</context-param>

使用 Maven 管理依赖,配置如下:

复制代码
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>1.53</version>
<!-- Include everything below here if you have dependency conflicts -->
<classifier>standalone</classifier>
<exclusions>
<exclusion>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty</artifactId>
</exclusion>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
</exclusion>
<exclusion>
<groupId>xmlunit</groupId>
<artifactId>xmlunit</artifactId>
</exclusion>
<exclusion>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</exclusion>
<exclusion>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>
</exclusion>
</exclusions></dependency>

具体的部署这里就不介绍了,说说 WireMock 的配置:

Ø WireMock 的文件目录

如图所示:

mappings: 存放映射描述的文件

__files: 存放映射匹配结果的文件

图 -4-WireMock 的文件目录

WireMock 的匹配规则示例

分两种:完整 Url 匹配和正则 UrlPattern

Url:完全匹配

mappings:cities-mapping.json

复制代码
{
"request": {
"method": "GET",
"url": "/cities"
},
"response": {
"status": 200,
"bodyFileName": "/cities.json",
"headers": {
"Content-Type": "application/json",
"Cache-Control": "max-age=86400"
}
}
}

__files:cities.json

复制代码
{
"cityName": " 公司操作间 ",
"shortname": "WS",
"provinceName": " 北京 ",
"provinceNameEn": "BeiJing City",
"code": "0001",
"cityNameEn": "Workshop"
}

UrlPattern:正则匹配任何 6 位数的,例如:/customer/123456/

mappings:cities-mapping.json

复制代码
{
"request": {
"method": "GET",
"urlPattern": "/customer/[0-9]{6}/"
},
"response": {
"status": 200,
"bodyFileName": "/customer.json",
"headers": {
"Content-Type": "application/json",
"Cache-Control": "max-age=86400"
}
}
}

__files:customer.json

复制代码
{
"channels": [],
"code": "781",
"companyName": "",
"enable": true,
"name": "163",
"password": "CC@ne.com",
"userState": "COMMERCIAL"
}

自给自足:平台化

使用 WireMock 通过 mappings 和 __files 文件夹可以有效管理映射和返回内容文件,但是所有文件的有部分可抽取未固定模板,而这些部分目前是手动编辑,关注这些部分会分散业务的精力,如果可以做成平台化管理,所有接口通过创建完成,文件命名规则全部由系统进行管理,将节省的时间更多投入业务关注和及早进行自测,这样子的收益将会更大。

那怎么样的平台才算能够满足当前需求呢?

  • 基于 HTTP 协议
  • 支持 Url、UrlPattern 匹配
  • 支持数据存储
  • API 接口规范化管理
  • 提交表单即可生成 mapping 和 __files 所需文件
  • 不同项目接口有不同的前缀
  • 能够返回指定格式(json|xml|文本)内容

图 -4-ServerMock-v1.0- 架构图

根据架构图,做了总体规划如下:

(点击放大图像)

图-5-ServerMock-v1.0 规划

技术选型

由于原来的测试平台使用Python 编写,为了保持风格一致,从界面录入到文件生成处理依然采用Python,后台工具使用WireMock 的standalone 模式,通过shell 脚本进行一键启停管理,以及实时刷新url、mapping 映射;

HTTP API Mock 项目管理 Web 前台

使用 Python+Django+MySQL 进行开发,分为项目配置和接口配置两大部分。

项目配置页

介绍:配置协议、进行 mock 服务器的重启、重新加载(有新的接口文件生成系统会自动 reset 即可,当然手工 reset 也可以,即时加载无须重启服务等待)。

图 -6- 项目配置页

接口列表页

介绍:展示列表,列出相关 URL、方法、是否正则、返回码、返回类型。

图 -7- 接口列表页

接口配置页

介绍:选择方法、URL 类型,填写 URL(如果选择 URL 类型为 UrlPattern,则填写正则表达式),填写状态码、返回接口,以及返回头,就可以完成一个 mock 接口的创建。

图 -8- 接口配置页

接口配置有三种输入形式:

直接输入返回结果

(点击放大图像)

图-9- 手工输入

一般场景在返回结果500k 以内的内容,可以直接输入,保存进入数据库;

通过url 抓取返回结果

图-10-url 抓取

一般场景在返回结果超过500k 以上内容,目标Mock 接口已经存在,可以直接抓取生成文件;

通过文件上传返回结果

图-11- 上传文件

一般场景在返回结果比较大|目标Mock 接口还未开发完成,手工上传返回内容的文件即可。

以上三种灵活的保存返回内容方式,最终保存的接口会按照以下格式生成mapping 和__files 所需文件:

图-12-mapping 和__files 文件格式

Mock 项目管理 Server 后台

使用 Java-WireMock 进行后台服务,在项目配置页通过按钮:重启、重新加载,调用后台脚本:wiremock_controller.sh,仅供参考:

复制代码
#!/bin/bash
if [ "$#" = 0 ];then
echo "Usage: $0 (start|stop|restart|reset)"
exit 1
fi
dirWiremock=`pwd`
getCount=`ps -ef | grep "wiremock-1.53-standalone" | grep -v "grep" |wc -l`
wiremock_jar=${dirWiremock}/wiremock-1.53-standalone.jar
port=9999
wiremock_url=http://localhost:${port}
stop(){
count=${getCount}
if [ 1==${count} ];then
curl -d log=aaa ${wiremock_url}/__admin/shutdown
echo "Stop success!......"
else
echo "Already stop"
fi
}
start(){
count=${getCount}
if [ 0==${count} ];then
nohup java -jar ${wiremock_jar} --verbose=true --port=${port} &
echo "Start success!......"
else
echo "Already start"
fi
}
if [ "$1" = "restart" ];then
count=${getCount}
if [ 1==${count} ];then
echo "Wiremock is running,wait for restarting! ...."
stop
echo "Start wiremock......"
start
else
start
fi
elif [ "$1" = "start" ];then
echo "Start wiremock......"
start
elif [ "$1" = "stop" ];then
echo "Stop wiremock......"
stop
elif [ "$1" = "reset" ];then
count=${getCount}
if [ 0==${count} ];then
echo "Wiremock must be running before reset,wait for starting! ...."
start
fi
curl -d log=aaa ${wiremock_url}/__admin/mappings/reset
echo "Reset success!......"
fi

其中:

“nohup java -jar ${wiremock_jar} --verbose=true --port=${port} &”:在 linux 系统后台运行 WireMock;

“curl -d log=aaa ${wiremock_url}/__admin/mappings/reset”:是通过发送 POST 请求,重新加载新生成的配置文件,在 WireMock 的源码中可以看到:reset 的作用:

复制代码
public interface Admin {
void addStubMapping(StubMapping stubMapping);
ListStubMappingsResult listAllStubMappings();
void saveMappings();
void resetMappings();
void resetScenarios();
void resetToDefaultMappings();
VerificationResult countRequestsMatching(RequestPattern requestPattern);
FindRequestsResult findRequestsMatching(RequestPattern requestPattern);
void updateGlobalSettings(GlobalSettings settings);
void addSocketAcceptDelay(RequestDelaySpec spec);
void shutdownServer();
}

通过一系列源码追溯,可以找到重置:

复制代码
@Override
public void reset() {
mappings.clear();
scenarioMap.clear();
}

可以推测映射文件是存放到列表的:

复制代码
public class SortedConcurrentMappingSet implements Iterable<StubMapping>{
private AtomicLong insertionCount;
private ConcurrentSkipListSet<StubMapping> mappingSet;
......
}

当 WireMock 启动,日志有以下描述:

复制代码
2015-02-12 11:38:37.844 Verbose logging enabled
2015-02-12 11:38:38.657:INFO::Logging to STDERR via wiremock.org.mortbay.log.StdErrLog
2015-02-12 11:38:38.664 Verbose logging enabled
/$$ /$$ /$$ /$$ /$$ /$$
| $$ /$ | $$|__/ | $$$ /$$$ | $$
| $$ /$$$| $$ /$$ /$$$$$$ /$$$$$$ | $$$$ /$$$$ /$$$$$$ /$$$$$$$| $$ /$$
| $$/$$ $$ $$| $$ /$$__ $$ /$$__ $$| $$ $$/$$ $$ /$$__ $$ /$$_____/| $$ /$$/
| $$$$_ $$$$| $$| $$ \__/| $$$$$$$$| $$ $$$| $$| $$ \ $$| $$ | $$$$$$/
| $$$/ \ $$$| $$| $$ | $$_____/| $$\ $ | $$| $$ | $$| $$ | $$_ $$
| $$/ \ $$| $$| $$ | $$$$$$$| $$ \/ | $$| $$$$$$/| $$$$$$$| $$ \ $$
|__/ \__/|__/|__/ \_______/|__/ |__/ \______/ \_______/|__/ \__/
port: 9999
enable-browser-proxying: false
no-request-journal: false
verbose: true

图 -13-WireMock 启动

成功处理请求的日志:

复制代码
2015-02-12 11:41:10.320 Received request: GET /test/today/dkfDF123/1234/ HTTP/1.1
Host: 192.168.32.55:9999
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: csrftoken=alXbvCtMyTBI1wnSnRoljguTaBnTDbPo; sessionid=tvoi9rzs66umnt1a26wsj36eqry2e2lo
Connection: keep-alive

总结

HTTP API 接口测试痛点是什么?很多公司划分不同研发组,各组系统之间的数据交互通过接口来实现,那很多时候就是集中在接口开发不同步,测试无法及早参与,对接调试难的问题。或许很多团队遇到这种问题,就是选择同步开发或者等待。当你选择等待的时候,你的产品质量就得不到及时验证,因为根本没有测试过,在当前快速迭代的开发模式中,时间是最致命的要素,如果不能及时交付,交付的质量又得不到保证,那是相当被动的局面,最后返工的成本比你当时愿意追加测试的成本会来的更高。

遇到这类问题是想办法解决,而不是回避,我们可以借鉴《自动化单元测试实践之路》在单元测试中,使用Mockito 对依赖进行Mock,那同样道理,使用Mock 技术也可以对HTTP API 进行Mock,按照这个思路探索下去,看看有没有开源解决方案,是否能够解决当前问题,如果可以就不用重复写一套解决方案;如果不行,那能否基于开源的做二次开发呢?当团队经历过测试痛点,调研收集了一定的数据,这些问题的答案就会浮出水面了。

或许有人要问,使用之后能够提高多少效率呢?看回《图-2- 改进的接口对接调试流程》,根据我们的经验,要统计当前迭代中有多少API 需要对接调试,如果对比旧的模式来说,API 接口调试效率提升至少有10%;可想而知,迭代中全是依赖API 接口开发的话,那提升的效率就相当可贵了。

作者简介

李乐, 测试经理,8 年以上工作经验,目前就职于ChinaCache 质量部, 博客:jooben.blog.51cto.com, 微博:weibo.com/iamlile


感谢郭蕾对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

2015 年 10 月 21 日 11:013516

评论

发布
暂无评论
发现更多内容

Vmware+Ubuntu 配置静态IP

千泷

区块链与物联网融合理论架构

CECBC区块链专委会

区块链

(无聊预警)来啊一起冲浪啊 - 网络协议01

Max Zhang - 张亦弛

网络协议 基础知识 OSI七层协议

关于食堂就餐卡系统设计

MR.X

系统设计 食堂就餐卡

零基础工程师绘图指南,半小时水平越级提升!

今日长剑在握

架构 设计 软件工程 分层架构

现成花火交易所系统软件APP开发案例

开發I852946OIIO

系统开发

架构师训练营知识点整理

garlic

架构师训练营第 1 期

极客大学架构师训练营 - 架构师技术图谱 - 大作业二

好吃不贵

架构师训练营第 1 期

DeFi中的关键——智能合约 | 白话区块链入门220

CECBC区块链专委会

区块链

2020中国低代码平台市场发展年度报告(深度分析)

低代码指南

软件 低代码 开发工具 SaaS/IaaS/PaaS 软件开发、

牛笔了!难道Android真的凉了?Android面试题及解析

欢喜学安卓

android 程序员 面试 移动开发

wildfly 21的配置文件和资源管理

程序那些事

程序那些事 web服务器 应用配置 服务器部署

前端组件化基础知识

三钻

前端 组件化 前端进阶

我从 HX 辞职了

看山

辞职 闲聊

工具之书:坚韧

lidaobing

文学少女 28天写作

Hadoop 编程实战:HDFS API 编程样例

罗小龙

Java hadoop hdfs 编程

ClickHouse数据导入

一粒

kafka Logstash Clickhouse

真香系列!大牛耗时一年最佳总结,让你的app体验更丝滑!建议收藏

欢喜学安卓

android 程序员 面试 移动开发

Caddy服务器使用方法

Rayan

运维 https 服务器 SSL证书

第十一周作业

Jack

Python+Selenium——自动办公美梦的破碎与重建

Sicolas Flamel

Python 自动化 办公

LeetCode题解:347. 前 K 个高频元素,二叉堆,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

高德地图、百度地图 都不如“人的智慧”

Sicolas Flamel

Python 深度思考 日常思考

AEM公链APP系统开发|AEM公链软件开发

开發I852946OIIO

系统开发

一周信创舆情观察(12.21~12.27)

统小信uos

深入浅出Android!2021京东最新Android面试真题解析,震撼来袭免费下载!

欢喜学安卓

android 程序员 面试 移动开发

SSH 免密码/免用户名/免IP登录云服务器实践

今日长剑在握

SSH 服务器

区块链2020年终盘点

CECBC区块链专委会

区块链

2021健康快乐

escray

2021

ClickHouse常见集群部署架构

一粒

nosql 架构 Clickhouse

《从C ++开始》第9版(1200页)

计算机与AI

c++

HTTPServerMock从手工到平台的演变-InfoQ