接口测试 接口测试工具: 基于 mitmproxy 实现接口录制,减少写用例的成本

· 2021年05月22日 · 最后由 moku 回复于 2021年05月25日 · 8645 次阅读

apiAutoTest

先软文介绍下:apiAutoTest 是个人和众多测试同行参与 (提供新的需求) 的一个接口测试工具项目.

Github: https://github.com/zy7y/apiAutoTest

目前功能

  • [x] 测试前后数据库备份操作,个人理解算数据清洗
  • [x] 各接口之间的测试数据依赖
  • [x] 自定义扩展函数定义,解决部分加密算法
  • [x] 后置 sql,结果用于依赖或者断言 (select 语句只能查出第一条)
  • [x] 实际结果可动态提取,与预期结果绝对==
  • [x] 可选用例失败重跑机制
  • [x] 基于 mitmproxy 录制接口生成用例文件

重大更新 (个人认为)

在之前的一篇自定义函数简单实现方式时,有提醒到语法可能出现冲突,所以在前两天更新时已经统一了语法${}

无论是使用依赖参数还是自定义方法都使用${}, 为了避免每次使用其他接口返回提取jsonpath表达式在用例中的冗余 (或许也提高了些性能,之前版本是会保存整个响应内容的),用例中增加了提取参数来实现形式如下

{
    // key -> id 为其他接口使用时的参数变量 用法 ${id}
    "id": "$.data.id" // $.data.id 实则为jsonpath表达式 从当前响应中提取id值
}

相关使用文档: 可到源码 Readme.md 文件中前往 在线文档查看

本次更新

*本次更新内容使用演示视频: 点击访问 B 站 *

契机

有同志,希望有个录制功能来减少手写参数的时间

根本

基于 mitmproxy, 抓包微信小程序 使用其提供的扩展 API, 通过 mitmproxy 实现代理之后捕获到 HTTP/HTTPS 请求,并把请求已追加的形式添加到 excel 中,当录制完成务必使用 ctrl + c 关闭录制,将生成一个完成的用例数据文件

可指定录制包含请求地址的接口

如何录制

  1. 前置条件: https://www.cnblogs.com/zy7y/p/14798151.html

  2. 打开本机代理

  1. 修改tools\recording.py中配置抓包请求地址, 用例生成路径

  1. apiAutoTest根目录下执行
mitmweb -s tools\recording.py

  1. 正常去使用就行了,当不需要录制的时候 在上面这个窗口Ctrl + C停止录制,然后关闭本机代理

录制的用例

因为默认录制的 url 是完整的 url,所以如果直接用这个文件,请把config/config.yaml中的serve dev 基准地址换成"", 因为条件有限没法覆盖测试很多内容这快功能可能会有Bug, 目前个人测试了Graphql规范接口的录制,RestFul规范接口录制, 不排除其他的无法完整的生成用例文件

需要注意 Excel 单元格字符数限制问题, Graphql 规范接口非常容易出现不可写入的情况, 单从业务接口来说应该不容易出现此类问题

执行录制的用例

config/config.yaml修改基准地址dev"",指定使用录制的用例文件

server:
  # 本地接口服务
  test: http://127.0.0.1:8888/
  # https://space.bilibili.com/283273603 演示项目后端服务来自
#  dev: http://www.ysqorz.top:8888/api/private/v1/
  dev: ''
# 基准的请求头信息
request_headers: {}
file_path:
  test_case: data/case_data1.xls    # 指定使用那个用例,这里使用了录制的用例
  report: report/
  log: log/run{time}.log
....

执行结果

实现源码

#!/usr/bin/env/ python3
# -*- coding:utf-8 -*-
"""
@Project: apiAutoTest
@File  :recording.py
@Author:zy7y
@Date  :2021/5/21 22:07
@Desc  : 录制接口,生成用例文件
基于mitmproxy实现
参考资料:
https://blog.wolfogre.com/posts/usage-of-mitmproxy/
https://www.cnblogs.com/liuwanqiu/p/10697373.html
"""

import json

import mitmproxy.http
import xlwt

# 上传文件接口不能录入文件参数 , excel单元格限制: Exception: String longer than 32767 characters
from mitmproxy import ctx


class Counter:
    def __init__(self, filter_url: str, filename: str = "data/case_data1.xls"):
        """
        基于mitmproxy抓包生成用例数据
        :param filter_url: 需要过滤的url
        :param filename: 生成用例文件路径
        """
        self.url = filter_url
        self.excel_row = [
            '编号',
            '用例标题',
            '请求头',
            '接口地址',
            '是否执行',
            '请求方式',
            '入参关键字',
            '上传文件',
            '请求数据',
            '提取参数',
            '后置sql',
            '预期结果']
        self.cases = [self.excel_row]
        self.counter = 1
        self.file = filename

    def response(self, flow: mitmproxy.http.HTTPFlow):
        """
        mitmproxy抓包处理响应,在这里汇总需要数据
        :param flow:
        :return:
        """
        if self.url in flow.request.url:
            # 编号
            number = "C" + str(self.counter)
            # 标题
            title = "mitmproxy录制接口" + str(self.counter)
            try:
                token = flow.request.headers["Authorization"]
            except KeyError:
                token = ''
            header = json.dumps({"Authorization": token})
            data = flow.request.text
            # 请求地址,config.yaml 里面基准环境地址 写 空字符串
            method = flow.request.method.lower()
            url = flow.request.url
            try:
                content_type = flow.request.headers['Content-Type']
            except KeyError:
                content_type = ''
            if 'form' in content_type:
                data_type = "data"
            elif 'json' in content_type:
                data_type = 'json'
            else:
                data_type = 'params'
                if '?' in url:
                    data = url.split('?')[1]
            data = self.handle_form(data)
            # 预期结果
            expect = json.dumps(
                {".": json.loads(flow.response.text)}, ensure_ascii=False)

            # 日志
            ctx.log.info(url)
            ctx.log.info(header)
            ctx.log.info(content_type)
            ctx.log.info(method)
            ctx.log.info(data)
            ctx.log.info(flow.response.text)
            case = [
                number,
                title,
                header,
                url.split('?')[0],
                "是",
                method,
                data_type,
                "",
                data,
                "",
                "",
                expect]
            self.cases.append(case)
            self.counter += 1
            # 文件末尾追加
            self.excel_cases()

    def excel_cases(self):
        """
        对二维列表cases进行循环并将内容写入单元格中
        :return:
        """
        workbook = xlwt.Workbook()
        worksheet = workbook.add_sheet('用例数据')
        for x in range(len(self.cases)):
            for y in range(len(self.cases[x])):
                worksheet.write(x, y, self.cases[x][y])
        try:
            workbook.save(self.file)
        except Exception as e:
            print(e)

    def handle_form(self, data: str):
        """
        处理 Content-Type:    application/x-www-form-urlencoded
        默认生成的数据 username=admin&password=123456
        :param data: 获取的data 类似这样  username=admin&password=123456
        :return:
        """
        data_dict = {}
        if data.startswith('{') and data.endswith('}'):
            return data
        try:
            for i in data.split('&'):
                data_dict[i.split('=')[0]] = i.split('=')[1]
            return json.dumps(data_dict)
        except IndexError:
            return ''


addons = [
    Counter("http://www.ysqorz.top:8888/api/private/v1/")
]

"""

mitmweb -s tools\recording.py 启动
ctrl + C 停止 并生成完整用例
"""

参考资料

https://docs.mitmproxy.org/stable/
https://blog.wolfogre.com/posts/usage-of-mitmproxy/
https://www.cnblogs.com/liuwanqiu/p/10697373.html

共收到 19 条回复 时间 点赞

楼主加个微信呀,好六

#2 · 2021年05月22日 Author
恒温 回复

....在社区学习并分享,其实是参考了 很多其他同行的代码.

我也写了一个,不过我单独抽离出来把它当成一个服务部署,和接口测试平台抽离,接口平台这边根据 ip 和项目自己去数据查,然后再自行转换。个人感觉,其实,mitm 的数据不单单可以用来做接口数据生成,其实,可以做更多的事情,比如可以做流量的监控,接口数据结构的获取,用来做接口校验的填充品或者完善接口文档这些。😁

#17 · 2021年05月24日 Author
tester_newbee 回复

mitmproxy 功能确实强大,部署成一个服务?可以分享下 具体实现思路嘛,所以 mitmproxy 服务会一直开着,然后需要代理的时候本机配置代理地址,mitmproxy 服务把抓到的数据入库?但是 项目去查 没太明白,抓到的不是只有请求类的数据嘛。。

对啊。我是用项目的域名和用户的 ip 作为一个定位。抓包的时候,什么都抓,然后在平台这边自己根据自己的需要过滤。拿到自己要的数据以后,可以就地取材,改他的请求参数,然后再流转到 api 测试里面 (和你上面的设计类似)。同时,这些数据其实也可以保存下载,流转到 mock 服务里面。

不好意思, 我平时都是伸手党,不怎么会直接回复您。但是大体思路的话,就是你在抓的时候,需要做一层处理,把用户的 ip 和接口域名这些抽离出来,反正你后面要用什么,而且怎么样用起来方便,你就在抓到数据以后,清洗一次,弄成你要用的样子。

#14 · 2021年05月24日 Author
tester_newbee 回复

谢谢回复,分享,大体明白了

tester_newbee 回复

您好,问下,你的 mitmproxy 是搭在后端服务器上的么?

🔥🔥🔥 回复

是的,单独抽离,做成一个服务,利用消息队列,启动两个脚本,一个生产者,一个消费者。存储在 momgodb 里面。然后在接口平台这边,写个接口,通过 ip 和抓包的域名去筛选,加载到页面上。(啊哈,原来这个箭号是回复😂 我以为是分享。。。)

学习了 ,提供了个好思路 👌🏻, 之前用来做接口的监控

tester_newbee 回复

这样的话,数据库存储的流量数据太多了吧。

Zhhh 回复

我们现在也是在开发接口监控这块,通过这个,可以拿到每个接口的入参和出参的类型,然后输出 json,然后通过增量的方式去不断完善这个接口的类型文档,虽然最后这个文档的准确性还是需要人为去确定是否正确。但是最起码可以对每次抓包生成的文档进行类比,可以知道这个接口 (或者业务) 的稳定性和变更程度,如果变更程度高,测试的时候就需要特别留意了

恒温 回复

话说大佬,我点你的个人资料--->上海智涌信息科技有限公司 跳转的竟然是个黄网。😂 😂 还好旁边没女同事。吓坏了

🔥🔥🔥 回复

我们现在暂时还没考虑是否要删除这块,其实你也可以删除,或者做个定时任务,每个星期,或者每个月清理一次即可。

tester_newbee 回复

……郁闷

恒温 回复

看到这个,我也点了一下,尴尬。。。。

在路上 回复

好奇的我也点了下~ 真的! 承包了五月的笑点

莫离 回复

😀 可能是彩蛋。嗯,应该是 testhome 的彩蛋。

我也有写过类似的工具,不过生成的是自己框架的 json 文件,最近写用例录制的人又多了,感觉就像一个循环一样😂

关闭了讨论 05月26日 10:25
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册