优化请求前数据处理方法,支持用户使用jsonpath提取断言数据,支持多数据断言

This commit is contained in:
zy7y 2020-11-22 22:20:33 +08:00
parent 260d963fee
commit 876cc53ef4
9 changed files with 108 additions and 133 deletions

View File

@ -122,6 +122,9 @@ https://www.bilibili.com/video/BV1EE411B7SU?p=10
2020/11/21 更新用例文档,合并文件对象,文件地址,优化文件上传处理方式
2020/11/21 config.yaml文件中新增request_headers 选项默认header在此设置优化test_api.py文件整合read_file.py
2020/11/22 优化请求断言方法支持用户自定义提取响应自定内容,支持多数据断言,整合请求方法,优化测试启动方法,部分日志移除,修改预期结果处理
#### 博客园首发
https://www.cnblogs.com/zy7y/p/13426816.html

View File

@ -7,59 +7,79 @@
@ide: PyCharm
@time: 2020/7/31
"""
from loguru import logger
import requests
from tools import convert_json
import requests
from tools import allure_step, allure_title, logger, extractor
from tools.data_process import DataProcess
from tools.read_file import ReadFile
class BaseRequest(object):
def __init__(self):
# 修改时间2020年9月14日17:09
# 确保整个接口测试中使用同一个requests.Session() 来管理cookie
self.session = requests.Session()
session = None
# 请求
def api_send(self, method, url, parametric_key=None, data=None, file_obj=None, header=None):
@classmethod
def get_session(cls):
if cls.session is None:
cls.session = requests.Session()
return cls.session
@classmethod
def send_request(cls, case: list, env: str = 'dev') -> object:
"""处理case数据转换成可用数据发送请求
:param case: 读取出来的每一行用例内容可进行解包
:param env: 环境名称 默认使用config.yaml server下的 dev 后面的基准地址
return: 响应结果 预期结果
"""
case_number, case_title, path, token, method, parametric_key, file_obj, data, expect = case
# allure报告 用例标题
allure_title(case_title)
# 处理url、header、data、file、的前置方法
url = ReadFile.read_config(f'$.server.{env}') + DataProcess.handle_path(path)
allure_step('请求地址', url)
header = DataProcess.handle_header(token)
allure_step('请求头', header)
data = DataProcess.handle_data(data)
allure_step('请求参数', data)
file = DataProcess.handler_files(file_obj)
allure_step('上传文件', file)
# 发送请求
res = cls.send_api(url, method, parametric_key, header, data, file)
allure_step('响应耗时(s)', res.elapsed.total_seconds())
allure_step('响应内容', res.json())
# 响应后操作
if token == '':
DataProcess.have_token['Authorization'] = extractor(res.json(), ReadFile.read_config('$.expr.token'))
allure_step('请求头中添加Token', DataProcess.have_token)
DataProcess.save_response(case_number, res.json())
allure_step('存储实际响应', DataProcess.response_dict)
return res.json(), expect
@classmethod
def send_api(cls, url, method, parametric_key, header=None, data=None, file=None) -> object:
"""
:param method: 请求方法
:param url: 请求url
:param parametric_key: 入参关键字 get/delete/head/options/请求使用params,
post/put/patch请求可使用jsonapplication/json/data
:param data: 参数数据默认等于None
:param file_obj: 文件对象的地址 单个文件直接放地址/Users/zy7y/Desktop/vue.js
多个文件格式["/Users/zy7y/Desktop/vue.js","/Users/zy7y/Desktop/jenkins.war"]
:param file: 文件对象
:param header: 请求头
:return: 返回json格式的响应
:return: 返回res对象
"""
# 修改时间2020年9月14日17:09
session = self.session
files = DataProcess.handler_files(file_obj)
session = cls.get_session()
if parametric_key == 'params':
logger.info(f'{method, url, data, header}')
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)
res = session.request(method=method, url=url, data=data, files=file, headers=header)
elif parametric_key == 'json':
res = session.request(method=method, url=url, json=data, files=files, headers=header)
res = session.request(method=method, url=url, json=data, files=file, headers=header)
else:
raise ValueError(
'可选关键字为get/delete/head/options/请求使用params, post/put/patch请求可使用jsonapplication/json/data')
logger.info(f'请求方法:{method},请求路径:{url}, 请求参数:{data}, 请求文件:{files}, 请求头:{header})')
return res.json()
def api_front(self):
"""请求api的前置处理方法"""
pass
def api_position(self):
"""请求api的后置处理方法"""
pass
logger.info(f'\n请求地址:{url}\n请求方法:{method}\n请求头:{header}\n请求参数:{data}\n响应数据:{res.json()}')
return res

View File

@ -11,18 +11,15 @@ request_headers:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36
# 实际响应jsonpath提取规则设置
# 提取规则设置
expr:
# 提取token的jsonpath表达式
token: $.data.token
# 提取实际响应的断言数据jsonpath表达式与excel中预期结果的数据进行比对用
response: $.meta
file_path:
case_data: ../data/case_data.xlsx
report_data: ../report/data/
report_generate: ../report/html/
log_path: ../log/运行日志{time}.log
test_case: ../data/case_data.xlsx
report: ../report/
log: ../log/run{time}.log
email:
# 发件人邮箱
@ -35,6 +32,4 @@ email:
# 收件人邮箱
addressees: ["收件人邮箱1","收件人邮箱2","收件人邮箱3"]
title: 接口自动化测试报告(见附件)
# 附件地址
enclosures: ["../report/html/apiAutoTestReport.zip",]

Binary file not shown.

View File

@ -1,87 +1,47 @@
#!/usr/bin/env/python3
#!/usr/bin/env/ python3
# -*- coding:utf-8 -*-
"""
@project: apiAutoTest
@author: zy7y
@file: test_api.py
@ide: PyCharm
@time: 2020/7/31
@time: 2020/11/22
@desc: 测试方法
"""
import os
import shutil
import pytest
from tools import logger
from api.base_requests import BaseRequest
from tools import *
from tools.data_process import DataProcess
from tools.read_file import ReadFile
base_url = ReadFile.read_config('$.server.dev')
res_reg = ReadFile.read_config('$.expr.response')
report_data = ReadFile.read_config('$.file_path.report_data')
report_generate = ReadFile.read_config('$.file_path.report_generate')
log_path = ReadFile.read_config('$.file_path.log_path')
report = ReadFile.read_config('$.file_path.report')
logfile = ReadFile.read_config('$.file_path.log')
# 读取excel数据对象
data_list = ReadFile.read_testcase()
# 请求对象
br = BaseRequest()
logger.info(f'配置文件/excel数据/对象实例化,等前置条件处理完毕\n\n')
cases = ReadFile.read_testcase()
class TestApiAuto(object):
# 启动方法
def run_test(self):
import os, shutil
if os.path.exists('../report') and os.path.exists('../log'):
class TestApi:
@classmethod
def run(cls):
if os.path.exists('../report'):
shutil.rmtree(path='../report')
shutil.rmtree(path='../log')
# 日志存取路径
logger.add(log_path, encoding='utf-8')
pytest.main(args=[f'--alluredir={report_data}'])
# 本地生成 allure 报告文件,需注意 不用pycharm等类似ide 打开会出现无数据情况
os.system(f'allure generate {report_data} -o {report_generate} --clean')
logger.add(logfile, enqueue=True, encoding='utf-8')
logger.info('开始测试...')
pytest.main(args=[f'--alluredir={report}/data'])
os.system(f'allure generate {report}/data -o {report}/html --clean')
logger.success('报告已生成')
# 直接启动allure报告会占用一个进程建立一个本地服务并且自动打开浏览器访问ps 程序不会自动结束,需要自己去关闭)
# os.system(f'allure serve {report_data}')
logger.warning('报告已生成')
@pytest.mark.parametrize('case_number,case_title,path,is_token,method,parametric_key,file_obj,'
'data,expect', data_list)
def test_main(self, case_number, case_title, path, is_token, method, parametric_key, file_obj,
data, expect):
logger.debug(f'⬇️⬇️⬇️...执行用例编号:{case_number}...⬇️⬇️⬇️️')
allure_title(case_title)
path = DataProcess.handle_path(path)
allure_step('请求地址', base_url + path)
allure_step('请求方式', method)
header = DataProcess.handle_header(is_token)
allure_step('请求头', header)
data = DataProcess.handle_data(data)
allure_step('请求数据', data)
res = br.api_send(method=method, url=base_url + path, parametric_key=parametric_key, file_obj=file_obj,
data=data, header=header)
allure_step('实际响应结果', res)
DataProcess.save_response(case_number, res)
allure_step('响应结果写入字典', DataProcess.response_dict)
# 写token的接口必须是要正确无误能返回token的
if is_token == '':
DataProcess.have_token['Authorization'] = extractor(res, ReadFile.read_config('$.expr.token'))
really = extractor(res, res_reg)
allure_step('根据配置文件的提取响应规则提取实际数据', really)
expect = convert_json(expect)
allure_step('处理读取出来的预期结果响应', expect)
allure_step('响应断言', (really == expect))
assert really == expect
@pytest.mark.parametrize('case', cases)
def test_main(self, case):
response, expect = BaseRequest.send_request(case)
# 断言操作
DataProcess.assert_result(response, expect)
if __name__ == '__main__':
TestApiAuto().run_test()
# 使用jenkins集成将不会使用到这两个方法邮件发送/报告压缩zip
# from tools.zip_file import zipDir
# from tools.send_email import send_email
# zipDir(report_generate, report_zip)
# send_email(email_setting)
TestApi.run()

View File

@ -38,10 +38,8 @@ def rep_expr(content: str, data: dict, expr: str = '&(.*?)&') -> str:
:param expr: 查找用的正则表达式
return content 替换表达式后的字符串
"""
logger.info(f'替换前内容{content}')
for ctt in re.findall(expr, content):
content = content.replace(f'&{ctt}&', str(extractor(data, ctt)))
logger.info(f'替换后内容{content}')
return content
@ -67,7 +65,6 @@ def convert_json(dict_str: str) -> dict:
dict_str = dict_str.replace('false', 'False')
dict_str = eval(dict_str)
logger.error(e)
logger.info(f'{dict_str}, {type(dict_str)}')
return dict_str
@ -83,7 +80,3 @@ def allure_step(step: str, var: str) -> None:
"""
with allure.step(step):
allure.attach(json.dumps(var, ensure_ascii=False, indent=4), step, allure.attachment_type.TEXT)
if __name__ == '__main__':
print(convert_json('["1","2"]'))

View File

@ -7,16 +7,13 @@
@ide: PyCharm
@time: 2020/11/18
"""
from tools import *
from tools import logger, extractor, convert_json, rep_expr, allure_step
from tools.read_file import ReadFile
class DataProcess:
response_dict = {}
header = ReadFile.read_config('$.request_headers')
logger.error(header)
have_token = header.copy()
@classmethod
@ -57,7 +54,7 @@ class DataProcess:
实例- 单个文件: &file&D:
"""
if file_obj == '':
return None
return
for k, v in convert_json(file_obj).items():
# 多文件上传
if isinstance(v, list):
@ -79,5 +76,21 @@ class DataProcess:
return
data = rep_expr(variable, cls.response_dict)
variable = convert_json(data)
logger.info(f'最终的请求数据如下: {variable}')
return variable
@classmethod
def assert_result(cls, response: dict, expect_str: str):
""" 预期结果实际结果断言方法
:param response: 实际响应字典
:param expect_str: 预期响应内容从excel中读取
return None
"""
expect_dict = convert_json(expect_str)
index = 0
for k, v in expect_dict.items():
actual = extractor(response, k)
index += 1
logger.info(f'{index}个断言,实际结果:{actual} | 预期结果:{v} \n断言结果 {actual == v}')
allure_step(f'{index}个断言', f'实际结果:{actual} = 预期结果:{v}')
assert actual == v

View File

@ -10,14 +10,13 @@
"""
import yaml
import xlrd
from tools import *
from tools import extractor
class ReadFile:
config_dict = None
@classmethod
@logger.catch
def get_config_dict(cls, config_path: str = '../config/config.yaml') -> dict:
"""读取配置文件,并且转换成字典
:param config_path: 配置文件地址 默认使用当前项目目录下的config/config.yaml
@ -30,7 +29,6 @@ class ReadFile:
return cls.config_dict
@classmethod
@logger.catch
def read_config(cls, expr: str = '.') -> dict:
"""默认读取config目录下的config.yaml配置文件根据传递的expr jsonpath表达式可任意返回任何配置项
:param expr: 提取表达式, 使用jsonpath语法,默认值提取整个读取的对象
@ -39,14 +37,13 @@ class ReadFile:
return extractor(cls.get_config_dict(), expr)
@classmethod
@logger.catch
def read_testcase(cls):
"""
读取excel格式的测试用例
:return: data_list - pytest参数化可用的数据
"""
data_list = []
book = xlrd.open_workbook(cls.read_config('$.file_path.case_data'))
book = xlrd.open_workbook(cls.read_config('$.file_path.test_case'))
# 读取第一个sheet页
table = book.sheet_by_index(0)
for norw in range(1, table.nrows):
@ -55,11 +52,5 @@ class ReadFile:
value = table.row_values(norw)
value.pop(3)
data_list.append(list(value))
logger.info(f'{data_list}')
return data_list
if __name__ == '__main__':
ReadFile.read_config()
ReadFile.read_testcase()

View File

@ -8,7 +8,7 @@
@time: 2020/8/3
"""
import yagmail
from loguru import logger
from tools import logger
def send_email(setting):
@ -25,7 +25,7 @@ def send_email(setting):
"""
yag = yagmail.SMTP(setting['user'], setting['password'], setting['host'])
# 发送邮件
yag.send(setting['addressees'], setting['title'], setting['contents'], setting['enclosures'])
yag.send(setting['addressees'], setting['title'], setting['contents'])
# 关闭服务
yag.close()
logger.info("邮件发送成功!")