关于实践:无侵入式mock平台在得物的实践

48次阅读

共计 7870 个字符,预计需要花费 20 分钟才能阅读完成。

一、概述

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=SC2164
cd /home/dhk/workspace/hulk
python37 -m venv venv            # 生成虚拟环境
source venv/bin/activate            # 启动虚拟环境
python37 -m pip install --upgrade pip           # 降级 pip
python37 -m pip install -r requirements.txt     # 装置依赖库
# shellcheck disable=SC2164
cd /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.pid
else
    uwsgi --ini uwsgi.ini
fi

性能:在 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 值的关键字参数也能够由未提供的命名局部组成。

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

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 代理等。

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 的接口。

接下来就能够十分丝滑的进行客户端接口 mock,不须要 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
关注得物技术,携手走向技术的云端

正文完
 0