2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

HTTPServerMock 从手工到平台的演变

  • 2015-10-21
  • 本文字数:8316 字

    阅读完需:约 27 分钟

不管是 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:014883

评论

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

朱嘉明:全面认知区块链的科学特征

CECBC

区块链

实时数据流计算引擎Flink和Spark流计算对比

小舰

大数据 flink spark 流计算

初识Golang之安装运行篇

Kylin

3月日更 21天挑战 Go 语言

我从外包辞职了,10000小时后,走进字节跳动拿了offer

Java 编程 程序员 架构 面试

你是否觉得上级的能力不如你?

石云升

心理学 28天写作 职场经验 管理经验 3月日更

标准引领 浪潮工业互联网助力澳门质量品牌国际认证联盟成立

工业互联网

【得物技术】TDengine在得物的落地应用

得物技术

数据库 tdengine 数据 sentinel 得物技术

冰河公开了其总结的一项重要的编程技能!

冰河

Java 正则表达式 程序员

利用区块链技术,打造绿色发展的中药材生态链

CECBC

中药材

融合发展是区块链的未来 数字通证新模式具有划时代意义

CECBC

数字通证

Github上堪称最全的面试题库(Java岗)到底有多香

Java 程序员 架构 面试

高质量、高并发的实时通信架构设计与探索

融云 RongCloud

linux下七种文件类型

xiezhr

Linux linux操作 linux运维 linux 文件权限控制

(28DW-S8-Day28) 战略流程重整

mtfelix

28天写作

源码分析-Netty: 架构剖析

程序员架构进阶

架构 Netty 源码剖析 28天写作 3月日更

我们为什么需要云原生?

脑极体

微信小程序开发:如何快速实现添加一条分割线的项目需求

三掌柜

微信小程序 3月日更

并发编程:一次搞定单例模式

Java架构师迁哥

安全高可用通信背后的 MySQL 优化实践

融云 RongCloud

Spark提交后都干了些什么?

小舰

大数据 spark Spark调优

我是如何拿到5大银行offer

小舰

面试 银行 笔试 校园招聘

如何在 Spring 生态中玩转 RocketMQ?

阿里巴巴云原生

Java Serverless 微服务 云原生 中间件

白话讲解,拜占庭将军问题

架构精进之路

分布式 算法 3月日更

深入剖析数据库事务的隔离级别

小舰

数据库 事务隔离级别 数据库事务

你真的懂Spring解决循环依赖吗?

Java 架构 Spring Boot

C/C++Linux服务器开发完整学习路线(含免费学习资料下载地址)

Linux服务器开发

Linux 后端 C/C++ Linux服务器开发 Linux后台开发

GO训练营第12、13周—— runtime

Glowry

打击虚拟货币洗钱:中国破获比特币跨境洗钱案

CECBC

虚拟货币

大专生阿里/腾讯/京东面经分享:Java面试精选题+架构实战笔记(技术狂补)

比伯

Java 编程 架构 面试 计算机

面试现场:遇到不会回答的问题,如何力挽狂澜 ?

xcbeyond

面试 3月日更

Hadoop UI 系统 -HUE 详细剖析

大数据技术指南

大数据 hadoop 28天写作 3月日更

HTTPServerMock从手工到平台的演变_软件工程_乐少_InfoQ精选文章