写点什么

【得物技术】无侵入式 mock 平台在得物的实践

用户头像
得物技术
关注
发布于: 2021 年 01 月 29 日
【得物技术】无侵入式mock平台在得物的实践

一、概述

1.1 背景介绍

作为测试应该都遇到过如下两大痛点:

1.只想测试被测系统 A,却需要从依赖系统开始一层层造自己想要的测试数据,造数花费时间长,边界值及异常场景不好模拟。

2.接口自动化,UI 自动化,埋点自动化因为服务或者测试数据的不稳定性导致自动化维护成本高。

要解决上述问题,基本都会想到 mock。目前市面上有很多优秀的开源 mock 框架:Mockito、PowerMock、EasyMock、JMockit 等,但这些框架对于我们现在的业务场景及主要是在集成测试过程中使用,显然不是我们想要的。因为我们希望在不改动开发代码的情况下支持随心所欲的构造 mock 接口的返回报文来测试不同的业务场景,基于这种外部依赖服务走 http 形式的技术架构,一套无侵入式的 mock 平台应运而生。

hulk 是一个无侵入式的 http mock 平台,支持客户端代理,从网关层 mock,支持后端服务之间的 mock。支持返回报文函数配置,并且具备放行逻辑。未来还将支持 filter,根据不同的入参返回不同的 mock 数据。


1.2 系统架构

基于 Django + mitmproxy + vue + MongoDB + MySQL。

目前整个技术架构比较简单,mock 服务基于 Django 框架开发,代理层主要是在开源框架 mitmproxy 基础上做了二次开发打通和 mock 系统的交互,前端配置平台使用了公司的脚手架 poizon-cli。数据存储主要用了 MongoDB 和 MySQL,提高性能后续会考虑引入 redis,将配置信息缓存到 redis 中降低接口响应时间。



1.2.1 服务端 mock 时序图



1.2.2 客户端 mock 时序图



二、mock 服务

2.1 部署及性能

通过 Nginx + Uwsgi + Django 部署,支持高并发。

可以直接通过测试组 jenkins 构建部署。

部署脚本:

# 服务器项目地址# shellcheck disable=SC2164cd /home/dhk/workspace/hulkpython37 -m venv venv            # 生成虚拟环境source venv/bin/activate            # 启动虚拟环境python37 -m pip install --upgrade pip           # 升级pippython37 -m pip install -r requirements.txt     # 安装依赖库# shellcheck disable=SC2164cd /home/dhk/workspace/hulk/hulk  #进到uwsgi.ini目录
# shellcheck disable=SC2006# shellcheck disable=SC2009# 获取uwsgi父进程pid=`ps -ef | grep "uwsgi" | grep -v grep | awk '{print $2}' | awk 'NR==1{print}'`if [ -n "$pid" ]then uwsgi --reload uwsgi.pidelse uwsgi --ini uwsgi.inifi
复制代码

性能:在 4C8G 的机器上的单机性能指标



2.2 框架设计原理

mock 服务可以理解为一个类似于业务系统的应用,可以请求该服务的接口地址并返回对应的报文。并提供了前端配置功能,支持配置自定义的 mock 接口信息及放行接口对应的业务系统 host 映射关系。想要设计成一个支持动态接收自定义接口路径的 mock 服务,需要先了解 Django 处理请求的原理及路由配置。


2.2.1 Django 如何处理一个请求

先看一下 Django 框架处理请求的原理,熟悉该原理后,可以很好地利用这点来设计成一个 mock 服务所需要的支持自定义路由的功能。感兴趣的可以参照官方文档。

当一个用户请求 Django 站点的一个页面,下面是 Django 系统决定执行哪个 Python 代码使用的算法:

  1. Django 确定使用根 URLconf 模块。通常,这是 ROOT_URLCONF 设置的值,但如果传入 HttpRequest 对象拥有 urlconf 属性(通过中间件设置),它的值将被用来代替 ROOT_URLCONF 设置。

  2. Django 加载该 Python 模块并寻找可用的 urlpatterns 。它是 django.urls.path() 和(或) django.urls.re_path() 实例的序列(sequence)。

  3. Django 会按顺序遍历每个 URL 模式,然后会在所请求的 URL 匹配到第一个模式后停止,并与 path_info 匹配。

  4. 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图,这个视图是一个 Python 函数(或基于类的视图class-based view)。视图会获得如下参数:

一个 HttpRequest 实例。

如果匹配的 URL 包含未命名组,那么来自正则表达式中的匹配项将作为位置参数提供。

关键字参数由路径表达式匹配的任何命名部分组成,并由 django.urls.path()django.urls.re_path() 的可选 kwargs 参数中指定的任何参数覆盖。

Changed in Django 3.0:

在旧版本里,带有 None 值的关键字参数也可以由未提供的命名部分组成。

  1. 如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。参照下面的错误处理( Error handling )。

https://docs.djangoproject.com/en/3.1/


2.2.2 路由配置

对于高质量的 Web 应用来说,使用简洁、优雅的 URL 模式是一个非常值得重视的细节。Django 允许你自由地设计你的 URL,不受框架束缚。


这里用了 Django 框架的 url 正则表达式配置规则,达到了类似于动态注入接口地址的效果。这时候随便请求一个接口进来,会按顺序依次匹配,直到匹配到对应的 path 为止,除了 web 端的接口路径,其余的都会匹配上正则,例如请求:/rec/sns/du/ct_push_V2/recommend会进到view_mock.mock()函数中。

urlpatterns = [    path('admin/', admin.site.urls),    path('hulk/attention', view_attention.attention),    path('hulk/check', view_attention.check),    path('hulk/query_url_info', view_web.query_url_info),    path('hulk/insert_url_info', view_web.insert_url_info),    path('hulk/update_is_del_1', view_web.update_is_del_1),    path('hulk/update_url_info_doc', view_web.update_url_info_doc),    path('hulk/update_is_open', view_web.update_is_open),    path('hulk/proxy_query_url_info', view_proxy.proxy_query_url_info),
url(r'^(.*)$', view_mock.mock)]
复制代码

2.2.3 mock 逻辑

根据上述路由配置的原理,业务系统所有请求被 mock 系统的接口都将走到mock()函数中,这里会先去读数据库的接口配置信息,如果该接口配置了 mock 那么直接把配置的 response 返回,没有配置 mock 时,会继续往下走,去查询该接口的路由配置信息,对应放行到被 mock 系统做正常的业务请求,相当于做了一层转发。

注:目前只做了常见的 POST 和 GET 请求方式的放行逻辑,POST 请求的 body 类型也是默认 json。


logger = logging.getLogger('log')def mock(request, interface):    logger.info(request.body)    path = request.path    # 查询mongodb    data = MockApiInfo().query_url_info(path)    if data:        url_info = data[0]        res = url_info['response']        # 处理配置的返回报文        response = JsonResponse(process_response.handle_variate(res))        # 组装返回header        if url_info.get('response_headers'):            response_headers = json.loads(url_info['response_headers'])            for k, v in response_headers.items():                response.__setitem__(k, v)        return response    else:        # 放行逻辑        config = MockTransConfig().query_config(path)        headers = request.headers        if config:            if request.method == 'POST':                host = request.scheme + '://' + config[0]['host'] + path                headers['Content-Type'] = 'application/json'                res = requests.request(request.method, url=host, headers=headers,                                       data=request.body)                logger.info(res.json())                return JsonResponse(res.json())            elif request.method == 'GET':                host = request.scheme + '://' + config[0]['host'] + request.get_full_path_info()                res = requests.request(request.method, url=host, headers=request.headers)                logger.info(res.json())                return JsonResponse(res.json())        else:            response = JsonResponse({"code": 1001, "status": 200, "msg": '请先配置接口或者开启,当前接口路径:' + path})            response.__setitem__("Content-Type", "application/json; charset=utf-8")            return response
复制代码


2.2.4 数据库设计

1.放行接口、host 映射关系配置

这里选择的是关系型数据库 mysql 来存储配置信息



字段说明:

path -> 接口路径

host -> 域名

bussiness -> 业务域

description -> 描述


2.接口信息配置,考虑到接口返回报文是 json,很显然 MongoDB 比较适合

接口配置表mock_api_info

db.createCollection("mock_api_info")

{    "_id": ObjectId("5fcc546448bfde3202d2eaf4"),    "url": "/rec/sns/du/ct_hot/recommend",    "sys_name": "",    "method": "",    "content_type": "",    "response_headers": "",    "response": "{}",    "rich_response": "",    "description": "推荐流-算法",    "is_del": "0",    "is_open": "1",    "add_time": "2020-12-06 11:47:48",    "update_time": "2020-12-07 11:11:40"}
复制代码

字段说明:

url -> 接口路径

sys_name -> 系统名

method -> 请求方法:GET,POST。。。

content_type -> body 类型:预留字段

response_headers -> 返回 header 头

response -> 返回报文

rich_response -> 富文本返回报文:预留字段

description -> 描述

is_del -> 是否删除:0:没有删除,1:删除

is_open -> 是否激活:0:关闭,1:打开


2.2.5 前端配置页面




2.3 如何 mock 服务端接口

2.3.1 配置需要 mock 的接口信息

1.在 mock 配置平台配置需要 mock 的接口信息。



如果需要自定义返回的 header 头,可以配置对应期望返回的 header 信息,都是 json 格式。没有配置会返回默认 header 头。

2.配置完验证,可以用接口测试工具比如 postman 请求配置好的接口,正常返回配置的报文说明配置正确。

例子:http://mock服务地址/rec/sns/du/ct_push_V2/recommend



3.修改服务端系统配置,下面列出了社区的两种配置。


2.3.2 Apollo 配置

Apollo地址

比如社区的 go 服务,依赖算法的接口,对应的请求域名都配置在 Apollo 上。

去修改需要 mock 的接口请求域名。

保存完,点击发布,然后重启对应的服务。



2.3.3 项目中配置

比如社区的 php 服务都是在项目中的environment.php配置文件。

直接通过跳板机在服务器上修改,jumpserver 地址。


2.4 日志

通过跳板机连上 root@dw-test-test-interfaces-01 服务器

进入/home/dhk/workspace/hulk目录

实时日志可以查看tailf uwsgi.log

所有日志都保存在 logs 文件下



三、代理层

3.1 常用的代理工具

测试过程经常用到 app 抓包工具比如:charles、fiddler、wireshark 等。

上述列出的都是需要安装在自己电脑上的工具,但有些时候我们更希望有个代理服务器使用,比如玩过爬虫或者科学参与电商平台活动的同学工具库中都会有这么一款工具。这块目前有很多优秀的开源框架,比如whistle,支持写 js 脚本。还有 anyproxy、mitmproxy 等。

框架选择:这里我选择了 mitmproxy 这个开源框架,理由很简单因为它是 Python 实现的。。。考虑到要做二次开发。

在使用 mock 功能的场景中 mitmproxy 和 Charles 的对比

  • 通过 Charles 进行 mock:

优点:方便,不会相互影响

缺点:需要本地安装,mock 的接口多了管理不方便,无法二次开发特性功能

  • 使用 hulk 平台

优点:无需本地安装软件,配置完后,只要大家连上该代理服务都可使用,比较方便的支持 UI 自动化,

埋点自动化的使用。并且支持很灵活的打开关闭 mock。支持二次开发,可扩展性高。

缺点:共享一个代理服务,抓包信息需要自己过滤


注:后续会支持 userId 的 filter 配置,就可以做到相互不影响


3.2 二次开发后的 mitmproxy

3.2.1 介绍

目前该代理服务已经部署在内网服务器上,该框架非常强大,这里不做详细介绍。


Introduction

顾名思义,mitmproxy 就是用于 MITM 的 proxy,MITM 即中间人攻击(Man-in-the-middle attack)。用于中间人攻击的代理首先会向正常的代理一样转发请求,保障服务端与客户端的通信,其次,会适时的查、记录其截获的数据,或篡改数据,引发服务端或客户端特定的行为。

不同于 fiddler 或 wireshark 等抓包工具,mitmproxy 不仅可以截获请求帮助开发者查看、分析,更可以通过自定义脚本进行二次开发。举例来说,利用 fiddler 可以过滤出浏览器对某个特定 url 的请求,并查看、分析其数据,但实现不了高度定制化的需求,类似于:“截获对浏览器对该 url 的请求,将返回内容置空,并将真实的返回内容存到某个数据库,出现异常时发出邮件通知”。而对于 mitmproxy,这样的需求可以通过载入自定义 python 脚本轻松实现。

但 mitmproxy 并不会真的对无辜的人发起中间人攻击,由于 mitmproxy 工作在 HTTP 层,而当前 HTTPS 的普及让客户端拥有了检测并规避中间人攻击的能力,所以要让 mitmproxy 能够正常工作,必须要让客户端(APP 或浏览器)主动信任 mitmproxy 的 SSL 证书,或忽略证书异常,这也就意味着 APP 或浏览器是属于开发者本人的——显而易见,这不是在做黑产,而是在做开发或测试。

事实上,以上说的仅是 mitmproxy 以正向代理模式工作的情况,通过调整配置,mitmproxy 还可以作为透明代理、反向代理、上游代理、SOCKS 代理等。

https://www.cnblogs.com/H4ck3R-XiX/p/12624072.html


3.2.2 设计原理

一般 mock 客户端,直接拦截报文修改 response 即可。这里是当配置了需要 mock 的接口时,会把原来请求的接口域名改成 mock 服务的域名地址,从而使得客户端请求到 mock 服务,到达同样的效果。这样做完全隔离了和业务系统的交互。

mitmproxy 支持 mitmproxymitmdumpmitmweb 三个启动命令,这三个命令功能一致,且都可以加载自定义脚本,唯一的区别是交互界面的不同。这里考虑到保留前端抓包信息的展示,选用了mitmweb来启动,并且直接修改了底层源码来打通和 mock 服务的交互。

修改的核心代码片段如下,所有进来的请求都会先去判断是否需要 mock,如果需要 mock 即直接将请求转发到 mock 系统,不需要 mock 的接口原路放行。

# 是否走mock逻辑if hulk_api.proxy_query_url_info(path.decode("utf-8")) > 0:    print(('命中mock,接口:' + str(path)).center(100, '='))    host = config.HULK_HOST    scheme = b'http'    port = 80    headers.__delitem__('Host')    headers.insert(0, b'Host', bytes(host))
复制代码

3.3 如何 mock 客户端接口

第一步:连代理。

第二步:在 mock 平台配置需要 mock 的接口配置页面地址。

接下来就可以非常丝滑的进行客户端接口 moc,不需要 mock 了可以关闭 mock 按钮,或者直接断开代理即可。


四、社区实践

背景:4.60 版本推荐流「负反馈优化」需求,需要测试服务端在不同业务场景返回不同负反馈文案的逻辑,推荐流接口/sns/v2/feed/recommend依赖算法接口/rec/sns/du/ct_hot/recommend,没法固定自己想要的内容。

方案:

1.算法侧帮忙造数据(显然成本相对较高,比较麻烦)

2.mock 掉算法的接口

操作:

1.配置需要 mock 的接口



返回报文结构填和算法约定好的数据结构形式,并将数据改为自己需要测试用的数据。


2.推荐流接口在 PHP 服务,直接去跳板机上修改 php server 的配置



3.开始测试

①接入 mock 后返回的数据是自己配置的            


②不需要 mock,关闭 mock 开关正常走算法逻辑


既很方便的测试到了服务端的逻辑,也同时测试到了客户端的取值逻辑,并且方便于产品同学的验收。


五、结语

随着技术的不断发展,如果想要做一套通用的 mock 平台,任重而道远。比如现在流行的 rpc 怎么去支持?不同的技术架构有不一样的需求,这些都是需要去考虑的。当然各种测试工具平台的设计开发初衷都是提效,服务于业务。后续会不断的结合业务特性来迭代,希望能打造出贴合业务特点,真正的能带来提效的一个 mock 平台。


文|dhk

关注得物技术,携手走向技术的云端


发布于: 2021 年 01 月 29 日阅读数: 29
用户头像

得物技术

关注

得物APP技术部 2019.11.13 加入

关注微信公众号「得物技术」

评论

发布
暂无评论
【得物技术】无侵入式mock平台在得物的实践