一、概述

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

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
关注得物技术,携手走向技术的云端