大家都知道, 不管是 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;
- }
定义其它状态码也是同样道理:
- error_page 403 /error/403.HTML;
- error_page 500 501 502 503 504 /error/500.HTML;</pre>
俗话说: 术业有专攻, 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> // 如果对软件测试, 接口测试, 自动化测试, 性能测试, LR 脚本开发, 面试经验交流.
- <context-param> // 感兴趣可以 175317069, 群内会有不定期的发放免费的资料链接, 这些资料
- <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 规划
- 如果对软件测试, 接口测试, 自动化测试, 性能测试, LR 脚本开发, 面试经验交流. 感兴趣可以 175317069, 群内会有不定期的发放免费的资料链接, 这些资料都是从各个技术网站搜集, 整理出来的, 如果你有好的学习资料可以私聊发我, 我会注明出处之后分享给大家.
- 技术选型
- 由于原来的测试平台使用 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 {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
- 如果对软件测试, 接口测试, 自动化测试, 性能测试, LR 脚本开发, 面试经验交流. 感兴趣可以 175317069, 群内会有不定期的发放免费的资料链接, 这些资料都是从各个技术网站搜集, 整理出来的, 如果你有好的学习资料可以私聊发我, 我会注明出处之后分享给大家.
- 图 -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 接口开发的话, 那提升的效率就相当可贵了.
来源: http://www.jianshu.com/p/43506aa33276