最近在做公司项目的自动化接口测试, 在现有几个小框架的基础上, 反复研究和实践, 搭建了新的测试框架. 利用业余时间, 把框架总结了下来.
AIM 框架介绍
AIM, 是 Automatic Interface Monitoring 的简称, 即自动化接口监测. 是一种基于 python unittest 的自动化接口测试框架.
设计思想
框架根据 python 语言的特点, 结合了面向对象和面向函数编程.
以高效编程为主要目的, 避免为了封装而封装. 轻配置, 重编码.
接口测试的主要处理对象是参数. 如果完全进行数据与代码的分离, 就会造成变量, 传参的冗余, 降低编程效率.
于是从不做数据与代码分离出发, 对于需要复用的参数, 提取到类之外, 视需要进行数据与代码的分离.
做到有的放矢. 兼顾效率和复用性, 迭代分离, 更具实用性.
目录结构
case: 测试用例
common: 公共函数, 全局变量
config: 配置路径等
data: 数据文件
result: 测试结果
util: 工具类
run.py: 用例执行入口
run_mail.py: 执行后自动发送邮件入口
case
BaseCase
所有 Case 的基类.
封装了 requests 库的 post 和 get 函数 req, 用于发送请求.
调用 assertEqual 等方法, 封装了用例的断言. 比如检查接口返回 flag, 检查接口状态 200, 检查值相等.
项目 Case
测试系统的用例. 按模块分别建立文件编写脚本.
Env.py: 环境配置, 包括 url 处理, 登录对象 login 实例 (用户名, 密码), 数据库对象 dao 实例 (数据库连接).
Public.py: 公共模块. 存放本系统公共的变量, 函数, 用例等.
common
Func.py: 公共函数, 比如获取时间日期, 获取随机数, 处理参数.
Login.py: 登录模块, 属于各系统通用, 故放于此目录下. 包括密码加密, 验证码处理, 强制登录.
Var.py: 全局变量. 比如 token.
config
RelativePath.py: 配置目录, 文件的相对路径.
data
echarts 数据存储 CSV 文件, 项目接口清单等.
result
log: 日志. logging 实现. 支持输出到文件和打印控制台. 文件暂时使用较少, 主要打印控制台便于调试.
接口调用记录: 输出每个测试方法调用接口的记录, 包括参数, 响应, 耗时等.
自动化测试报告: htmlTestRunner.py 实现的 HTML 页面报告.
util
AutoCode.py: 自动生成结构化测试代码.
CSV.py:CSV 相关函数封装. 比如输出接口调用记录.
Excel.py: 读取和存储 Excel 文件.
Format.py: 格式化. 比如把浏览器复制的参数格式化为代码中带有缩进的 JSON.
HTMLTestRunner.py: 用于输出自动化测试报告.
Log.py: 封装日志方法.
MySQL.py: 数据库相关操作.
Parewise.py: 结对测试. 一种测试技术, 后文详述.
Request.py: 核心工具, 封装接口发送请求.
Mail.py: 发送邮件.
run.py
执行测试用例入口, 可以选择执行一个或多个系统, 也可以执行一个系统中一个或多个模块.
核心模块
BaseCase.req
通过 requests 封装的发送接口请求的方法.
定义在 BaseCase 类的内部.
参数 | 说明 |
---|---|
p | 将 url、headers、body、method 统一封装到一个 json 里面进行处理。 |
method=‘post‘ | 默认为 post 方法。接口以 post 居多。 |
jsondata=‘json‘ | 默认 json 参数。post 方法的 json 或 data,纯 json 使用 json 参数即可。对于 receive_json 这种 dict,采用 data 参数。 |
loglevel=3 | 默认为 3。日志级别,输出请求、响应信息到控制台或接口调用记录. csv。 |
rtext=None | 一些 get 请求会返回 html 或 pdf,在控制台或 csv 文件中影响显示,可以指定文本进行替换。 |
发送请求, 并计算耗时:
- start = time.clock()
- if method == 'post': # 关闭 SSL 认证
- if jsondata == 'json':
- r = self.timeoutTry("requests.post(p['url'], headers=p['headers'], json=p['body'], verify=False)", p)
- elif jsondata == 'data':
- r = self.timeoutTry("requests.post(p['url'], headers=p['headers'], data=p['body'], verify=False)", p)
- else:
- print('jsondata 错误')
- elif method == 'get':
- r = self.timeoutTry("requests.get(p['url'], headers=p['headers'], params=p['body'])", p)
- else:
- print('method 错误')
- end = time.clock()
- elapsed = decimal.Decimal("%.2f" % float(end - start))
其中的 self.timeoutTry 是为了处理响应超时, 会在后续博文中介绍.
Parewise
结对测试. 接口参数一般是多个, 于是比较适合采用 parewise 进行用例设计.
parewaise 的概念可以百度一下.
大概意思就是, 大多数的 bug 都是条件的两两组合造成的, parewise 就是针对两两组合的情况, 设计测试用例.
算法为, 如果某一组用例的组合结果, 在其他组合中均出现, 就删除该组用例, 从而精简用例.
Windows 下有微软的 PICT,txt 文件录入参数后, 命名行执行, 就出来结果了.
比如参数
执行后结果, 只有 31 条, 精简了很多.
这个基本上一秒就出来结果了.
我自己参考网上算法写的, 就要慢的多.
估计后面有时间了再看看能不能调优.
parewise 算法:
- cp = [] # 笛卡尔积
- s = [] # 两两拆分
- for x in eval('itertools.product' + str(tuple(param_list))):
- cp.append(x)
- s.append([i for i in itertools.combinations(x, 2)])
- del_row = []
- s2 = copy.deepcopy(s)
- for i in range(len(s)): # 对每个进行匹配
- t = 0
- for j in range(len(s[i])): # 判断所有同时都存在其他中 且位置相同
- for i2 in [x for x in range(len(s2)) if s2[x] != s[i]]: # 其他 只比对有效
- flag = False
- for j2 in range(len(s2[i2])):
- if s[i][j] == s2[i2][j2] and j == j2:
- t = t + 1
- flag = True
- break
- if flag:
- break
- if t == len(s[i]):
- del_row.append(i)
- s2.remove(s[i])
- return [cp[i] for i in range(len(cp)) if i not in del_row]
网上的例子是用的 index 函数. 在我写过程中, 发现这里有个坑. 比如 list 中存在相同元素, 就始终返回前一个匹配的索引, 结果就会有问题. 我就完全避免了 index 函数. 不知道哪个是对的, 目前满足使用需要, 将就着用了. 有点小尴尬.
Case
BaseCase 断言:
- def checkFlag(self, p, r):
- """预期, 实际"""
- err = str([p['url'], p['body'], r.text])
- try:
- b = False
- if (r.JSON()['flag'] in [1, '1', '', None,'statistic_by_result', 0,
- "0", 'struct_product', 'v_select_jz_single']
- or r.JSON()['message'] in ("暂无数据", "未查询到数据")):
- b = True
- self.assertEqual(True, b, msg=err)
- except (JSON.JSONDecodeError, KeyError): # 1. 返回的不是 JSON, 比如下载, 404 2. 无 flag
- self.assertEqual(200, r.status_code, msg=err)
最简单的一个测试用例:
- from case.PyPlatform2_0_2.Public import *
- class Home(BaseCase):
- """首页"""
- def setUp(self):
- log(testname(self.__repr__()) + '\n')
- record([testname(self.__repr__())])
- def test(self):
- """xxx"""
- self.req({
- "url": full_url("xxx"),
- "body": {}
- })
setup, 输出日志.
Token
因为公司登陆用的 token, 跟 cookie 类似, 保留登陆状态, 避免重复登陆.
如何处理 token 也是框架设计的一个要点.
Env 设置 token, 因为每个系统的登陆参数值都不一样.
Var.token = login.get_token()
BaseCase.req 在每次请求时获取 token, 从而免登录.
- if "headers" not in p.keys():
- p['headers'] = {'token': ''}
- p['headers']['token'] = Var.token
- CSV
写文件:
- if not os.path.exists(path):
- f = open(path, 'a', newline='')
- a = CSV.writer(f)
- a.writerow(title)
- f.close()
- f = open(path, 'a', newline='')
- a = CSV.writer(f)
- try:
- a.writerow(d)
- except UnicodeEncodeError:
- d[4] = "Unicode 隐藏" # response
- a.writerow(d)
- f.close()
- if get_file_size(path)>= 50 * 1024 * 1024: # 超过 50M 删除文件
- os.remove(path)
- record(title)
traceback 自动生成文件名:
- def _sys_name():
- t = str(traceback.extract_stack())
- b = True
- for x in os.listdir(case_dir):
- if x not in ["BaseCase.py", "__pycache__"]:
- if x in t:
- return x + "接口调用记录" + current_date() + ".csv"
- if b:
- print("request 找不到 sysname")
- print(t)
- HTMLTestRunner
根据通用的版本, 也是参考网上一些现有的美化代码, 综合了一下, 根据自己需求做了改造.
近 20 交易日测试通过率
加了一个 echarts, 把最近 20 交易日的测试通过率, 通过折线走势图的方式展示出来. 监测系统稳定性.
数据存放和读取在 data 目录的 CSV 文件中.
统计表格
按项目进行分组统计, 增加测试说明一列, 按颜色区别测试结果状态, 可点击查看详细描述和错误信息.
同时优化了整体的样式效果.
排序:
- # 按照通过率从小到大排序
- passrate_value = []
- for key in passrate:
- if key != 'total':
- passrate_value.append(float(passrate[key].replace('%', '')))
- passrate_value.sort()
保存折线图数据:
- today = datetime.datetime.now().strftime('%Y-%m-%d')
- if '--' not in names: # 跑单个系统不存
- if dao_is_trade_date(today): # 非交易日不存
- with open(self.rct20_path, "r") as f: # 读取数据
- lines = CSV.reader(f)
- lines = list(lines)
- for lin in lines:
- lin[0] = lin[0].replace('月', '-')
- lin[0] = lin[0].replace('日', '')
- rct_data = lines
- # print(rct_data)
- nowdate = datetime.datetime.now().strftime('%m-%d')
- # 如果有重复日期, 先删
- l = len(rct_data)
- while l != 0 and nowdate == rct_data[l - 1][0]:
- rct_data.pop(l - 1)
- l = len(rct_data)
- for pt in self.passrate_tl:
- n = pt[0]
- v = pt[1]
- row = []
- row.append(str(nowdate))
- row.append(str(n))
- row.append(str(v).replace('%', ''))
- rct_data.append(row)
- # 只存近 20 条
- row_20 = len(names) * 20
- if len(rct_data)> row_20: # 超过 20 条
- for i in range(0, len(names)):
- rct_data.pop(0)
- with open(self.rct20_path, 'w', newline='') as f:
- writer = CSV.writer(f)
- writer.writerows(rct_data)
拼接折线图数据用于展示:
- while ri <len(rct_data): # 遍历 ->s_data
- scan = []
- while ri < len(rct_data) and rct_data[ri][0] == trade_date[di]:
- s_data[rct_data[ri][1]].append(rct_data[ri][2])
- scan.append(rct_data[ri][1])
- ri += 1
- chg = list(set(names) ^ set(scan)) # 差集
- for c in chg:
- s_data[c].append('--') # 增加 / 减少的项目, 为'--'
- di += 1
- series = [] # 系列序列
- s_names = s_data.keys()
- for k in s_names:
- s = {} # 单个系列
- s['name'] = k
- s['type'] = 'line'
- if s_data != {}:
- s['data'] = s_data[k]
- series.append(s)
这部分代码是很久之前写的了, 代码应该是不够简洁, 高效, 规范滴, 是可以优化滴. 偷了懒没有重构了.
用例设计
测试类型 | 描述 |
---|---|
冒烟测试 | 所有接口写单独的 test,确保调用正常。 |
全选测试 | 将所有参数尽可能多的全选上,调用接口。 一定程序上可以弥补结对测试的不足。 |
结对测试 | 如前文所述,关注两两组合的情况。 |
参数值, 部分采用随机数. 也视需求, 从数据库或其他接口获取数据.
结束语
第一次写技术博客.
马上工作 5 年.
算是一个尝试吧.
版权申明: 本文为博主原创文章, 转载请保留原文链接及作者.
来源: http://www.bubuko.com/infodetail-3259956.html