通过读取配置文件,获取到host地址、提取token的jsonpath表达式,提取实际响应结果用来与预期结果比对的jsonpath表达式。
读取excel用例文件数据,组成一个符合pytest参数化的用例数据,根据每列进行数据处理(token操作、数据依赖)
token,写,需要使用一个正常登录的接口,并且接口中要返回token数据,才可以提取,token,读为该请求将携带有token的header,token 无数据的将不携带token
数据依赖处理,从excel中读取出来的格式{"用例编号":["jsonpath表达式1", "jsonpath表达式2"]},通过用例编号来获取对应case的实际响应结果(实际响应结果在发送请求后,回写到excel中),通过jsonpath表达式提取对应的依赖参数字段,以及对应的值,最终会返回一个存储该接口需要依赖数据的字典如{"userid":500, "username": "zy7y"},在发送请求时与请求数据进行合并,组成一个新的data放到请求中
每次请求完成之后将回写实际的响应结果到excel中
根据配置文件中配置的jsonpath表达式提取实际响应内容与excel中预期结果的数据对比
生成测试报告
压缩测试报告文件夹
执行接口消耗时间变长,代码乱(语言学的不扎实),频繁读写excel(可考虑用字典存每个接口的实际响应,取值直接从响应字典中取出)
整体代码结构优化未实现,导致最终测试时间变长,其他工具单接口测试只需要39ms,该框架中使用了101ms,考虑和频繁读写用例数据导致
环境与依赖
名称
|
版本
|
作用
|
python
|
3.7.8
|
|
pytest
|
6.0.1
|
底层单元测试框架,用来实现参数化,自动执行用例
|
allure-pytest
|
2.8.17
|
allure与pytest的插件可以生成allure的测试报告
|
jsonpath
|
0.82
|
用来进行响应断言操作
|
loguru
|
0.54
|
记录日志
|
PyYAML
|
5.3.1
|
读取yml/yaml格式的配置文件
|
Allure
|
2.13.5
|
要生成allure测试报告必须要在本机安装allure并配置环境变量
|
xlrd
|
1.2.0
|
用来读取excel中用例数据
|
yagmail
|
0.11.224
|
测试完成后发送邮件
|
requests
|
2.24.0
|
发送请求
|
运行test_api.py -> 读取config.yaml(tools.read_config.py) -> 读取excel用例文件(tools.read_data.py) -> test_api.py实现参数化 -> 处理是否依赖数据 ->base_requests.py发送请求 -> test_api.py断言 -> read_data.py回写实际响应到用例文件中(方便根据依赖提取对应的数据)
config.ymal展示
server:
test: http://127.0.0.1:8888/api/private/v1/
dev: http://49.232.203.244:8888/api/private/v1/
response_reg:
token: $.data.token
response: $.meta
file_path:
case_data: ../data/case_data.xlsx
report_data: ../report/data/
report_generate: ../report/html/
report_zip: ../report/html/apiAutoTestReport.zip
log_path: ../log/运行日志{time}.log
email:
user: 123456.com
password: ASGCSFSGS
host: smtp.163.com
contents: 解压apiAutoReport.zip(接口测试报告)后,请使用已安装Live Server 插件的VsCode,打开解压目录下的index.html查看报告
addressees: ["收件人邮箱1","收件人邮箱2","收件人邮箱3"]
title: 接口自动化测试报告(见附件)
enclosures: ["../report/html/apiAutoTestReport.zip",]
EXcel用例展示
请求方法封装
@project: apiAutoTest
@author: zy7y
@file: base_requests.py
@ide: PyCharm
@time: 2020/7/31
from test import logger
import requests
class BaseRequest(object):
def __init__(self):
def base_requests(self, method, url, parametric_key=None, data=None, file_var=None, file_path=None, header=None):
:param method: 请求方法
:param url: 请求url
:param parametric_key: 入参关键字, get/delete/head/options/请求使用params,
post/put/patch请求可使用json(application/json)/data
:param data: 参数数据,默认等于None
:param file_var: 接口中接受文件的参数关键字
:param file_path: 文件对象的地址, 单个文件直接放地址:/Users/zy7y/Desktop/vue.js
多个文件格式:["/Users/zy7y/Desktop/vue.js","/Users/zy7y/Desktop/jenkins.war"]
:param header: 请求头
:return: 返回json格式的响应
session = requests.Session()
if (file_var in [None, '']) and (file_path in [None, '']):
files = None
else:
if file_path.startswith('[') and file_path.endswith(']'):
file_path_list = eval(file_path)
files = []
for file_path in file_path_list:
files.append((file_var, (open(file_path, 'rb'))))
else:
files = {file_var: open(file_path, 'rb')}
if parametric_key == 'params':
res = session.request(method=method, url=url, params=data, headers=header)
elif parametric_key == 'data':
res = session.request(method=method, url=url, data=data, files=files, headers=header)
elif parametric_key == 'json':
res = session.request(method=method, url=url, json=data, files=files, headers=header)
else:
raise ValueError('可选关键字为:get/delete/head/options/请求使用params, post/put/patch请求可使用json(application/json)/data')
logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求文件:{files}, 请求头:{header})')
return res.json()
读取excel用例数据
@project: apiAutoTest
@author: zy7y
@file: read_data.py
@ide: PyCharm
@time: 2020/7/31
import xlrd
from test import logger
class ReadData(object):
def __init__(self, excel_path):
self.excel_file = excel_path
self.book = xlrd.open_workbook(self.excel_file)
def get_data(self):
:return: data_list - pytest参数化可用的数据
data_list = []
table = self.book.sheet_by_index(0)
for norw in range(1, table.nrows):
if table.cell_value(norw, 3) == '否':
continue
value = table.row_values(norw)
value.pop(3)
value = tuple(value)
logger.info(f'{value}')
data_list.append(value)
return data_list
存储接口实际结果响应
@project: apiAutoTest的副本
@author: zy7y
@file: save_response.py
@ide: PyCharm
@time: 2020/8/8
import json
import jsonpath
from test import logger
class SaveResponse(object):
def __init__(self):
self.actual_response = {}
def save_actual_response(self, case_key, case_response):
:param case_key:用例编号
:param case_response:对应用例编号的实际响应
:return:
self.actual_response[case_key] = case_response
logger.info(f'当前字典数据{self.actual_response}')
def read_depend_data(self, depend):
:param depend: 需要依赖数据字典{"case_001":"['jsonpaht表达式1', 'jsonpaht表达式2']"}
:return:
depend_dict = {}
depend = json.loads(depend)
for k, v in depend.items():
try:
for value in v:
actual = self.actual_response[k]
d_k = value.split('.')[-1]
depend_dict[d_k] = jsonpath.jsonpath(actual, value)[0]
except TypeError as e:
logger.error(f'实际响应结果中无法正常使用该表达式提取到任何内容,发现异常{e}')
return depend_dict
处理依赖数据逻辑
@project: apiAutoTest
@author: zy7y
@file: data_tearing.py
@ide: PyCharm
@time: 2020/8/10
import json
from json import JSONDecodeError
import jsonpath
from test import logger
class TreatingData(object):
处理hader/path路径参数/请求data依赖数据代码
def __init__(self):
self.no_token_header = {}
self.token_header = {}
def treating_data(self, is_token, parameters, dependent, data, save_response_dict):
if is_token == '':
header = self.no_token_header
else:
header = self.token_header
logger.info(f'处理依赖前data的数据:{data}')
if dependent != '':
dependent_data = save_response_dict.read_depend_data(dependent)
logger.debug(f'依赖数据解析获得的字典{dependent_data}')
if data != '':
dependent_data.update(json.loads(data))
data = dependent_data
logger.info(f'data有数据,依赖有数据时 {data}')
else:
data = dependent_data
logger.info(f'data无数据,依赖有数据时 {data}')
else:
if data == '':
data = None
logger.info(f'data无数据,依赖无数据时 {data}')
else:
data = json.loads(data)
logger.info(f'data有数据,依赖无数据 {data}')
path_list = parameters.split('/')
for i in range(len(path_list)):
try:
path_dict = json.loads(path_list[i])
except JSONDecodeError as e:
logger.error(f'无法转换字典,进入下一个检查,本轮值不发生变化:{path_list[i]},{e}')
continue
else:
logger.info(f'{path_dict}')
try:
for k, v in path_dict.items():
try:
path_list[i] = jsonpath.jsonpath(save_response_dict.actual_response[k], v)[0]
except TypeError as e:
logger.error(f'无法提取,请检查响应字典中是否支持该表达式,{e}')
except AttributeError as e:
logger.error(f'类型错误:{type(path_list[i])},本此将不转换值 {path_list[i]},{e}')
path_list = map(str, path_list)
parameters_path_url = "/".join(path_list)
logger.info(f'path路径参数解析依赖后的路径为{parameters_path_url}')
return data, header, parameters_path_url
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
import json
import jsonpath
from test import logger
import pytest
import allure
from api.base_requests import BaseRequest
from tools.data_tearing import TreatingData
from tools.read_config import ReadConfig
from tools.read_data import ReadData
from tools.save_response import SaveResponse
rc = ReadConfig()
base_url = rc.read_serve_config('dev')
token_reg, res_reg = rc.read_response_reg()
case_data_path = rc.read_file_path('case_data')
report_data = rc.read_file_path('report_data')
report_generate = rc.read_file_path('report_generate')
log_path = rc.read_file_path('log_path')
report_zip = rc.read_file_path('report_zip')
email_setting = rc.read_email_setting()
save_response_dict = SaveResponse()
data_list = ReadData(case_data_path).get_data()
treat_data = TreatingData()
br = BaseRequest()
logger.info(f'配置文件/excel数据/对象实例化,等前置条件处理完毕\n\n')
class TestApiAuto(object):
def run_test(self):
import os, shutil
if os.path.exists('../report') and os.path.exists('../log'):
shutil.rmtree(path='../report')
shutil.rmtree(path='../log')
logger.add(log_path, encoding='utf-8')
pytest.main(args=[f'--alluredir={report_data}'])
os.system(f'allure generate {report_data} -o {report_generate} --clean')
logger.warning('报告已生成')
@pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_var,'
'file_path, parameters, dependent,data,expect', data_list)
def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_var,
file_path, parameters, dependent, data, expect):
:param case_number: 用例编号
:param case_title: 用例标题
:param path: 接口路径
:param is_token: token操作:写入token/读取token/不携带token
:param method: 请求方式:get/post/put/delete....
:param parametric_key: 入参关键字:params/data/json
:param file_var: 接口中接受文件对象的参数名称
:param file_path: 文件路径,单文件实例:/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py
多文件实例['/Users/zy7y/PycharmProjects/apiAutoTest/test/__init__.py','/Users/zy7y/PycharmProjects/apiAutoTest/test/test_api.py']
:param parameters: path参数(携带在url中的参数)依赖处理 users/:id(id携带在url中) 实例:{"case_001": '$.data.id'},解析
从用例编号为case_001的实际结果响应中提取data字典里面的id的内容(假设提取出来是500), 最后请求的路径将是host + users/500
:param dependent: data数据依赖,该接口需要上一个接口返回的响应中的某个字段及内容:实例{"case_001",["$.data.id","$.data.username"]}
解析: 从用例case_001的实际响应结果中提取到data下面的id,与username的值(假设id值为500,username为admin),那么提取的数据依赖内容将是{"id":500, "username":"admin"}
纳闷最终请求的data 将是 {"id":500, "username":"admin"} 与本身的data合并后的内容
:param data: 请求数据
:param expect:预期结果,最后与config/config.yaml下的response_reg->response提取出来的实际响应内容做对比,实现断言
:return:
allure.dynamic.title(case_title)
logger.debug(f'⬇️⬇️⬇️...执行用例编号:{case_number}...⬇️⬇️⬇️️')
with allure.step("处理相关数据依赖,header"):
data, header, parameters_path_url = treat_data.treating_data(is_token, parameters, dependent, data, save_response_dict)
with allure.step("发送请求,取得响应结果的json串"):
res = br.base_requests(method=method, url=base_url + path + parameters_path_url, parametric_key=parametric_key, file_var=file_var, file_path=file_path,
data=data, header=header)
with allure.step("将响应结果的内容写入实际响应字典中"):
save_response_dict.save_actual_response(case_key=case_number, case_response=res)
if is_token == '写':
with allure.step("从登录后的响应中提取token到header中"):
treat_data.token_header['Authorization'] = jsonpath.jsonpath(res, token_reg)[0]
with allure.step("根据配置文件的提取响应规则提取实际数据"):
really = jsonpath.jsonpath(res, res_reg)[0]
with allure.step("处理读取出来的预期结果响应"):
expect = json.loads(expect)
with allure.step("预期结果与实际响应进行断言操作"):
assert really == expect
logger.info(f'完整的json响应: {res}\n需要校验的数据字典: {really} 预期校验的数据字典: {expect} \n测试结果: {really == expect}')
logger.debug(f'⬆⬆⬆...用例编号:{case_number},执行完毕,日志查看...⬆⬆⬆\n\n️')
if __name__ == '__main__':
TestApiAuto().run_test()
首先确保需要的环境与依赖包无问题之后,使用Pycharm打开项目,找到settings
修改为unitest或者其他非pytest,具体操作如下
jsonpath语法学习:blog.csdn.net/liuchunming…
zip文件压缩:www.cnblogs.com/yhleng/p/94…
欢迎交流。
源码地址Gitee - version1.0分支: gitee.com/zy7y/apiAut…
源码地址GitHub- version1.0 分支:github.com/zy7y/apiAut…
2020/11/23 - 优化数据参数、路径参数依赖处理方式,现版本与之前同等环境下,测试时间提升2S
介绍:www.cnblogs.com/zy7y/p/1402…