1. 增加测试数据分析预警机制 2. 优化自动生成测试用例方法 3. 将用例跳过方法 提取成公共的fixture,自动运行 4. 修改pytet-html测试报告列’用例描述’值获取 5. 根 据最新修改,更新readme文件~

This commit is contained in:
floraachy
2023-06-09 15:04:32 +08:00
parent 4646bd7fed
commit 05f34366af
21 changed files with 498 additions and 129 deletions

View File

@@ -20,6 +20,8 @@ pytest = "==6.2.5"
pytest-html = "==2.1.1"
pytest-rerunfailures = "*"
allure-pytest = "==2.9.45"
pydantic = "*"
xpinyin = "*"
[dev-packages]

View File

@@ -24,23 +24,15 @@
## 二、实现功能
* 通过session会话方式解决了登录之后cookie关联处理
* 框架天然支持接口动态传参、关联灵活处理
* 支持测试数据分析,测试数据不符合规范有预警机制
* 测试数据隔离, 实现数据驱动
* 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用
* 动态多断言: 支持响应断言和数据库断言
* 多种报告随心选择框架支持pytest-html以及Allure测试报告可以动态配置所需报告
* 日志模块: 采用loguru管理日志可以输出更为优雅简洁的日志
* 钉钉、企业微信通知: 支持多种通知场景,执行成功之后,可选择发送钉钉、或者企业微信、邮箱通知
* 执行环境一键切换,解决多环境相互影响问题
* 使用pipenv管理虚拟环境和依赖文件提供了一系列命令和选项来帮助你实现各种依赖和环境管理相关的操作
@@ -52,7 +44,8 @@
│ ├────allure_handle.py 操作allure的相关方法
│ ├────platform_handle.py 跨平台的支持allure用于生成allure测试报告
│ ├────assert_handle.py 断言处理, 包括响应断言和数据库断言
│ ├────case_handle.py 根据配置文件,从指定类型文件中读取用例数据,并调用生成用例文件方法,生成用例文件
│ ├────case_fun_handle.py 根据配置文件,从指定类型文件中读取用例数据,并调用生成用例文件方法,生成用例文件
│ ├────case_data_analysis 分析用例数据是否符合规范
│ ├────data_handle.py 数据处理
│ ├────request_data_handle.py 针对用例数据进行请求前后的处理
│ ├────get_results_handle.py 从pytest-html/allure测试报告中获取测试结果
@@ -171,7 +164,7 @@ pip install pipenv
1在目录`data`下新建一个YAML/Excel文件按照要求编写测试用例数据
2在test_case.test_manual_case下新建一个以"test"开头的测试方法,进行测试用例方法编写。
### 6. 用例中相关字段的介绍
### 5. 用例中相关字段的介绍
```yaml
- case_common :公共参数
@@ -186,13 +179,36 @@ pip install pipenv
- method请求方式例如GET, POST, DELETE, PUT, PATCH等
- headers请求头注意如果在headers里面防止cookies其值类型需要是字符串
- cookies请求cookies格式是DICT CookieJar对象
- pk请求数据类型params, json, file, data
- request_type请求数据类型params, json, file, data
- payload请求参数
- files上传附件接口所需的文件绝对路径
- extract后置提取参数
- assert_response响应断言
- assert_sql数据库断言
```
### 6. Excel用例单独说明
框架支持excel多表单自动生成测试用例每一个表单作为一个测试用例模块。
例如:
excel表格名称是test_demo.xlsx
excel表单1名称是GitLink-登录模块
excel表单2名称是示例模块
生成规则:
- 如果excel表单中存在"-",我们将取"-"后面的部分的首字母拼接excel文件名称作为测试用例模块/测试用例类/测试用例方法名称
- 如果excel表单中不存在"-"我们将直接获取表单名称首字母拼接excel文件名称作为测试用例模块/测试用例类/测试用例方法名称
- 测试用例模块/测试用例类/测试用例方法名称同时也将遵循python语法规则进行适当调整
基于上述规则:
- excel第一个表单生成的测试用例
测试用例模块test_demo_dlmk.py
测试用例类TestDemoDlmkAuto
测试用例方法test_demo_dlmk_auto
- excel第二个表单生成的测试用例
测试用例模块test_demo_slmk.py
测试用例类TestDemoSlmkAuto
测试用例方法test_demo_slmk_auto
## 六、运行自动化测试
### 1. 激活已存在的虚拟环境

View File

@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# @Time : 2023/6/7 16:37
# @Author : chenyinhua
# @File : case_data_analysis.py
# @Software: PyCharm
# @Desc: 分析用例数据是否符合规范
from typing import Text
from config.models import TestCase, TestCaseEnum, Method, RequestType
class CaseDataCheck:
"""
用例数据解析, 判断数据填写是否符合规范
"""
def __init__(self):
self.case_data = None
self.case_id = None
@property
def get_method(self) -> Text:
return self.check_params_right(
Method,
self.case_data.get(TestCaseEnum.METHOD.value[0])
)
@property
def get_request_type(self):
return self.check_params_right(
RequestType,
self.case_data.get(TestCaseEnum.REQUEST_TYPE.value[0])
)
def check_case_data_attr(self, attr: Text):
assert attr in self.case_data.keys(), (
f"用例ID为 {self.case_id} 的用例中缺少 {attr} 参数,请确认用例内容是否编写规范."
)
def check_params_exit(self):
"""
遍历一个枚举类中所有成员,并检查与每个成员对应的实例属性是否存在。
如果属性存在,则什么也不做,如果不存在,则抛出异常或执行其他操作
"""
for enum in list(TestCaseEnum._value2member_map_.keys()):
if enum[1]:
self.check_case_data_attr(enum[0])
def check_params_right(self, enum_name, attr):
"""
检查参数值是否正确,符合要求规范
"""
_member_names_ = enum_name._member_names_
assert attr.upper() in _member_names_, (
f"用例ID为 {self.case_id} 的用例中 {enum_name}: {attr} 填写不正确,"
f"当前框架中只支持 {_member_names_} 类型."
f"如需新增 method 类型,请联系管理员."
)
return attr.upper()
@property
def get_assert_response(self):
_assert_data = self.case_data.get(TestCaseEnum.ASSERT_RESPONSE.value[0])
assert _assert_data is not None, (
f"用例ID 为 {self.case_id} 未添加断言"
)
return _assert_data
def case_process(self, cases):
case_list = []
for key, values in cases.items():
# 公共配置中的数据,与用例数据不同,需要单独处理
if key != 'case_common':
# 检查用例数据,去除用例数据中的空格
for k, v in values.items():
values[k] = v.strip() if isinstance(v, str) else v
self.case_data = values
self.case_id = key
self.check_params_exit()
case_data = {
'feature': self.case_data.get(TestCaseEnum.FEATURE.value[0]),
'title': self.case_data.get(TestCaseEnum.TITLE.value[0]),
'url': self.case_data.get(TestCaseEnum.URL.value[0]),
'method': self.get_method,
'run': self.case_data.get(TestCaseEnum.RUN.value[0]),
'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]),
'cookies': self.case_data.get(TestCaseEnum.COOKIES.value[0]),
'request_type': self.get_request_type,
'payload': self.case_data.get(TestCaseEnum.PAYLOAD.value[0]),
'extract': self.case_data.get(TestCaseEnum.EXTRACT.value[0]),
"assert_response": self.get_assert_response,
"assert_sql": self.case_data.get(TestCaseEnum.ASSERT_SQL.value[0]),
}
case_list.append(TestCase(**case_data).dict())
return case_list

View File

@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
# @Time : 2023/6/7 10:07
# @Author : chenyinhua
# @File : case_fun_handle.py
# @Software: PyCharm
# @Desc:
from config.path_config import DATA_DIR, CASE_TEMPLATE_DIR, AUTO_CASE_DIR
from common_utils.excel_handle import ExcelHandle
from common_utils.yaml_handle import YamlHandle
from config.models import CaseFileType
from config.settings import CASE_FILE_TYPE
from loguru import logger
from common_utils.files_handle import get_files
from case_utils.case_data_analysis import CaseDataCheck
import os
from config.path_config import AUTO_CASE_DIR
from string import Template
import datetime
from xpinyin import Pinyin # 纯 Python 编写的中文字符串拼音转换模块,不需要依赖外部程序和词库。
"""
主要步骤:
1. 从用例数据文件EXCEL/YAML中获取用例数据
2. 分析用例数据是否符合规范
3. 确认符合规范后获取所有用例数据自动生成测试用例方法PY
"""
def generate_case_from_excel(files: list):
"""
读取excel用例数据生成测试用例
"""
for file in files:
# 读取excel文件中的用例数据存储到data中
if os.path.isfile(file):
# 读取xlsx/xls文件中的用例数据存储到data中
data = ExcelHandle(file).read()
# logger.debug(f"从{file}中读取到的用例数据是:{data}")
for index, v in enumerate(data):
excel_case = {}
# 将excel读取到的用例数据适配allure格式
"""
表单名称以短横线隔开的情况下左边部分作为allure_epic 右边部分作为allure_feature以及allure_story。
否则表单名称全部作为allure_epicallure_featureallure_story
"""
if "-" in v["sheet_name"]:
excel_case["case_common"] = {
"allure_epic": v["sheet_name"].split("-")[0],
"allure_feature": v["sheet_name"].split("-")[1],
"allure_story": v["sheet_name"].split("-")[1]
}
else:
excel_case["case_common"] = {
"allure_epic": v["sheet_name"],
"allure_feature": v["sheet_name"],
"allure_story": v["sheet_name"]
}
# 处理excel用例数据
for case in v["data"]:
excel_case[case["id"]] = case
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(excel_case)
# 生成测试方法
"""
由于excel涉及到多个表单每一个表单都会生成一个测试方法。因此会将表单名称的首字母拼接到测试方法上。
excel名称test_demo
例如表单名称:"GitLink-登录模块""登录模块",都是取关键字"登录模块"首字母
测试文件test_demo_dl.py
测试方法名称: TestDemoDl.test_demo_dl
"""
pin_yin = Pinyin()
_name = pin_yin.get_initials(excel_case["case_common"]["allure_feature"], "").lower()
gen_case_file(filename=os.path.splitext(os.path.basename(file))[0] + "_" + _name,
case_template_path=CASE_TEMPLATE_DIR,
case_common=excel_case["case_common"], case_data=tested_case,
target_case_path=AUTO_CASE_DIR)
else:
logger.error(f"{file}不是一个正确的文件路径!")
def generate_case_from_yaml(files: list):
"""
读取yaml用例数据生成测试用例
"""
for file in files:
# 从yaml/yml中读取用例数据
if os.path.isfile(file):
# 读取yaml/yml文件中的用例数据存储到data中
yaml_data = YamlHandle(file).read_yaml
# logger.debug(f"从{file}中读取到的用例数据是:{yaml_data}")
# 检查用例数据是否符合规范
tested_case = CaseDataCheck().case_process(yaml_data)
# 生成测试方法
gen_case_file(filename=os.path.splitext(os.path.basename(file))[0], case_template_path=CASE_TEMPLATE_DIR,
case_common=yaml_data["case_common"], case_data=tested_case,
target_case_path=AUTO_CASE_DIR)
else:
logger.error(f"{file}不是一个正确的文件路径!")
def generate_cases():
"""
根据配置文件,从指定类型文件中读取所有用例数据,并自动生成测试用例
"""
# 判断配置文件里面CASE_DATA_TYPE,判断用例数据是从excel还是yaml文件中读取
# 从excel中读取用例数据
if CASE_FILE_TYPE == CaseFileType.EXCEL.value:
# 在用例数据"DATA_DIR"目录中寻找后缀是xlsx, xls的文件
files = get_files(target=DATA_DIR, start="test_", end=".xlsx") \
+ get_files(target=DATA_DIR, start="test_", end=".xls")
# 自动生成测试用例
generate_case_from_excel(files)
# 从yaml中读取用例数据
elif CASE_FILE_TYPE == CaseFileType.YAML.value:
# 在用例数据"DATA_DIR"目录中寻找后缀是yaml, yml的文件
files = get_files(target=DATA_DIR, start="test_", end=".yaml") \
+ get_files(target=DATA_DIR, start="test_", end=".yml")
# 自动生成测试用例
generate_case_from_yaml(files)
else:
# 在用例数据"DATA_DIR"目录中寻找后缀是xlsx,xls, yaml, yml的文件
excel_files = get_files(target=DATA_DIR, start="test_", end=".xlsx") \
+ get_files(target=DATA_DIR, start="test_", end=".xls")
yaml_files = get_files(target=DATA_DIR, start="test_", end=".yaml") \
+ get_files(target=DATA_DIR, start="test_", end=".yml")
# 自动生成测试用例
generate_case_from_excel(excel_files)
generate_case_from_yaml(yaml_files)
def gen_case_file(filename, case_template_path, case_common, case_data, target_case_path):
"""
根据测试用例文件(yaml/yml/xlsx/xls),以及事先定义的测试用例模板,实际用例数据,生成测试用例方法(.py)
:param filename: 测试用例文件(yaml/yml/xlsx/xls)的名称,用作生成测试用例类名,方法名
:param case_template_path: 测试用例模板的绝对路径
:param case_common: 用例公共参数
:param case_data: 实际用例数据
:param target_case_path: 测试用例方法(.py)的绝对路径
"""
# 如果自动生成用例的目录不存在则自动创建一个
if not os.path.exists(AUTO_CASE_DIR):
os.makedirs(AUTO_CASE_DIR)
"""
string.Template是将一个string设置为模板通过替换变量的方法最终得到想要的string。
"""
# 将用例数据的名称作为测试用例文件名称, 如test_login_demo
func_name = filename
# 方法名test_demo的类名是TestDemo
class_name = "".join([word.capitalize() for word in func_name.split("_")])
# 定义生成的测试用例的模板
with open(file=case_template_path, mode="r", encoding="utf-8") as f:
case_template = f.read()
# 根据模板,生成测试用例方法
my_case = Template(case_template).safe_substitute({"allure_epic": case_common["allure_epic"],
"allure_feature": case_common["allure_feature"],
"allure_story": case_common["allure_story"],
"case_data": case_data,
"func_title": func_name,
"class_title": class_name,
"now": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
# 将测试用例方法写入py文件中
with open(os.path.join(target_case_path, func_name + '.py'), "w", encoding="utf-8") as fp:
fp.write(my_case)
if __name__ == '__main__':
generate_cases()

View File

@@ -30,7 +30,7 @@ class RequestPreDataHandle:
f"请求方式: {type(request_data.get('method', None))} || {request_data.get('method', None)}\n" \
f"请求头: {type(request_data.get('headers', None))} || {request_data.get('headers', None)}\n" \
f"请求cookies: {type(request_data.get('cookies', None))} || {request_data.get('cookies', None)}\n" \
f"请求类型: {type(request_data.get('pk', None))} || {request_data.get('pk', None)}\n" \
f"请求类型: {type(request_data.get('request_type', None))} || {request_data.get('request_type', None)}\n" \
f"请求内容: {type(request_data.get('payload', None))} || {request_data.get('payload', None)}\n" \
f"请求文件: {type(request_data.get('files', None))} || {request_data.get('files', None)}\n" \
f"后置提取参数: {type(request_data.get('extract', None))} || {request_data.get('extract', None)}\n" \
@@ -58,7 +58,7 @@ class RequestPreDataHandle:
f"请求方式: {type(self.request_data.get('method', None))} || {self.request_data.get('method', None)}\n" \
f"请求头: {type(self.request_data.get('headers', None))} || {self.request_data.get('headers', None)}\n" \
f"请求cookies: {type(self.request_data.get('cookies', None))} || {self.request_data.get('cookies', None)}\n" \
f"请求类型: {type(self.request_data.get('pk', None))} || {self.request_data.get('pk', None)}\n" \
f"请求类型: {type(self.request_data.get('request_type', None))} || {self.request_data.get('request_type', None)}\n" \
f"请求内容: {type(self.request_data.get('payload', None))} || {self.request_data.get('payload', None)}\n" \
f"请求文件: {type(self.request_data.get('files', None))} || {self.request_data.get('files', None)}\n" \
f"后置提取参数: {type(self.request_data.get('extract', None))} || {self.request_data.get('extract', None)}\n" \
@@ -204,7 +204,7 @@ class RequestHandle:
f"请求方式: {type(self.case_data.get('method', None))} || {self.case_data.get('method', None)}\n" \
f"请求头: {type(self.case_data.get('headers', None))} || {self.case_data.get('headers', None)}\n" \
f"请求cookies: {type(self.case_data.get('cookies', None))} || {self.case_data.get('cookies', None)}\n" \
f"请求类型: {type(self.case_data.get('pk', None))} || {self.case_data.get('pk', None)}\n" \
f"请求类型: {type(self.case_data.get('request_type', None))} || {self.case_data.get('request_type', None)}\n" \
f"请求内容: {type(self.case_data.get('payload', None))} || {self.case_data.get('payload', None)}\n" \
f"请求文件: {type(self.case_data.get('files', None))} || {self.case_data.get('files', None)}\n" \
f"请求响应数据: {response.text}\n" \

View File

@@ -37,7 +37,7 @@ class BaseRequest:
f"请求方式: {req_data.get('method', None)}\n" \
f"请求头: {req_data.get('headers', None)}\n" \
f"请求Cookies: {req_data.get('cookies', None)}\n" \
f"请求关键字: {req_data.get('pk', None)}\n" \
f"请求关键字: {req_data.get('request_type', None)}\n" \
f"请求内容: {req_data.get('payload', None)}\n" \
f"请求文件: {req_data.get('files', None)}\n" \
"=====================================================")
@@ -48,14 +48,14 @@ class BaseRequest:
f"请求方式: {req_data.get('method', None)}\n" \
f"请求头: {req_data.get('headers', None)}\n" \
f"请求Cookies: {req_data.get('cookies', None)}\n" \
f"请求关键字: {req_data.get('pk', None)}\n" \
f"请求关键字: {req_data.get('request_type', None)}\n" \
f"请求内容: {req_data.get('payload', None)}\n" \
f"请求文件: {req_data.get('files', None)}\n" \
"=====================================================")
res = cls.send_api_request(
url=req_data.get("url"),
method=req_data.get("method").lower(),
pk=req_data.get("pk", None),
request_type=req_data.get("request_type", None),
header=req_data.get("headers", None),
payload=req_data.get("payload", None),
files=req_data.get("files", None),
@@ -79,13 +79,13 @@ class BaseRequest:
return res
@classmethod
def send_api_request(cls, url: str, method: str, pk: str, header: Dict[str, str] = None, payload=None,
def send_api_request(cls, url: str, method: str, request_type: str, header: Dict[str, str] = None, payload=None,
files=None, cookies=None) -> Response:
"""
发送请求
:param method: 请求方法
:param url: 请求url
:param pk: 请求参数类型可选值为paramsjsondata
:param request_type: 请求参数类型可选值为paramsjsondata
:param payload: 请求数据对于不同请求类型可以为dictMultipartEncoder等
:param files: 请求上传的文件
:param header: 请求头
@@ -95,9 +95,9 @@ class BaseRequest:
headers = header or {}
session = cls.get_session()
if pk and pk.lower() == 'params':
if request_type and request_type.lower() == 'params':
res = session.request(method=method, url=url, params=payload, headers=headers, cookies=cookies, timeout=5)
elif pk and pk.lower() == 'data':
elif request_type and request_type.lower() == 'data':
if files:
if not isinstance(files, dict):
raise ValueError('data参数必须为dict')
@@ -108,7 +108,7 @@ class BaseRequest:
else:
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
res = session.request(method=method, url=url, data=payload, headers=headers, cookies=cookies, timeout=5)
elif pk and pk.lower() == 'json':
elif request_type and request_type.lower() == 'json':
if files:
if not isinstance(files, dict):
raise ValueError('json参数必须为dict')
@@ -120,8 +120,8 @@ class BaseRequest:
headers['Content-Type'] = 'application/json'
res = session.request(method=method, url=url, json=payload, headers=headers, cookies=cookies, timeout=5)
else:
logger.error('pk可选关键字为params, json, data')
print('pk可选关键字为params, json, data')
raise ValueError('pk可选关键字为params, json, data')
logger.error('request_type可选关键字为params, json, data')
print('request_type可选关键字为params, json, data')
raise ValueError('request_type可选关键字为params, json, data')
return res

View File

@@ -26,6 +26,7 @@ def data_replace(content, source):
return None
logger.debug("\n======================================================\n" \
"-------------Start数据替换--------------------\n"
f"替换源: {source}\n" \
f"初始字符串: {content}\n" \
"=====================================================")
print(f"-----Start-----数据替换: 初始字符串为:{content}")

View File

@@ -15,4 +15,18 @@ def add_docstring(docstring):
func.__doc__ = docstring
return func
return decorator
return decorator
class AddCLassDocstring:
"""
类装饰器它接受一个字符串参数docstring
并返回一个装饰器函数。装饰器函数接受一个函数参数func
并将func的__doc__属性设置为docstring。
"""
def __init__(self, docstring):
self.docstring = docstring
def __call__(self, func):
func.__doc__ = self.docstring
return func

View File

@@ -1,31 +1,33 @@
# -*- coding: utf-8 -*-
# @Time : ${now}
import pytest
from case_utils.assert_handle import assert_response, assert_sql
from loguru import logger
from case_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract
from pytest_html import extras # 往pytest-html报告中填写额外的内容
from common_utils.func_handle import add_docstring
from case_utils.allure_handle import allure_title
import allure
from config.settings import db_info
from config.global_vars import GLOBAL_VARS
# 用例数据
cases = ${case_data}
@allure.story(f'{cases["case_common"]["allure_story"]}')
@pytest.mark.${func_title}
@pytest.mark.auto
@pytest.mark.parametrize("case", cases.get("case_info"))
def ${func_title}_auto(case, extra):
logger.info("-----------------------------START-开始执行用例-----------------------------")
logger.debug(f"当前执行的用例数据:{case}")
# 给当前测试方法添加文档注释
add_docstring(case.get("title", ""))(${func_title}_auto)
# 添加用例标题作为allure中显示的用例标题
allure_title(case.get("title", ""))
if case.get("run", None):
@allure.epic("${allure_epic}")
@allure.feature("${allure_feature}")
class ${class_title}Auto:
@allure.story("${allure_story}")
@pytest.mark.${func_title}
@pytest.mark.auto
@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases])
def ${func_title}_auto(self, case, extra):
logger.info("-----------------------------START-开始执行用例-----------------------------")
logger.debug(f"当前执行的用例数据:{case}")
# 添加用例标题作为allure中显示的用例标题
allure_title(case.get("title", ""))
# 处理请求前的用例数据
case_data = RequestPreDataHandle(case).request_data_handle()
# 将用例数据显示在pytest-html报告中
@@ -40,9 +42,5 @@ def ${func_title}_auto(case, extra):
assert_sql(db_info[GLOBAL_VARS["env_key"]], case_data["assert_sql"])
# 断言成功后进行参数提取
after_request_extract(response, case_data.get("extract", None))
else:
reason = f"标记了该用例为false不执行\\n"
logger.warning(f"{reason}")
pytest.skip(reason)
logger.info("-----------------------------END-用例执行结束-----------------------------")
logger.info("-----------------------------END-用例执行结束-----------------------------")

View File

@@ -7,6 +7,8 @@
# @Desc: 全局变量
from enum import Enum, unique # python 3.x版本才能使用
from typing import Text, Dict, Callable, Union, Optional, List, Any
from pydantic import BaseModel
class CaseFileType(Enum):
@@ -55,3 +57,57 @@ class AllureAttachmentType(Enum):
WEBM = "webm"
PDF = "pdf"
class TestCaseEnum(Enum):
FEATURE = ("feature", False)
TITLE = ("title", True)
URL = ("url", True)
METHOD = ("method", True)
HEADERS = ("headers", True)
COOKIES = ("cookies", False)
RUN = ("run", False)
REQUEST_TYPE = ("request_type", True)
PAYLOAD = ("payload", False)
FILES = ("files", False)
EXTRACT = ("extract", False)
ASSERT_RESPONSE = ("assert_response", True)
ASSERT_SQL = ("assert_sql", False)
class TestCase(BaseModel):
feature: Union[None, Text] = None
title: Text
url: Text
method: Text
headers: Union[None, Dict, Text] = {}
cookies: Union[None, Dict, Text]
request_type: Text
run: Union[None, bool, Text] = None
payload: Any = None
files: Any = None
extract: Union[None, Dict, Text] = None
assert_response: Union[None, Dict, Text]
assert_sql: Union[None, Dict, Text] = None
class Method(Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
PATCH = "PATCH"
DELETE = "DELETE"
HEAD = "HEAD"
OPTION = "OPTION"
class RequestType(Enum):
"""
request请求发送请求参数的数据类型
"""
JSON = "JSON"
PARAMS = "PARAMS"
DATA = "DATA"
FILE = 'FILE'
EXPORT = "EXPORT"
NONE = "NONE"

View File

@@ -8,7 +8,7 @@
# ------------------------------------ 配置信息 ----------------------------------------------------#
# 0代表执行Excel和yaml两种格式的用例 1 代表 yaml文件2 用例代表Excel用例
CASE_FILE_TYPE = 1
CASE_FILE_TYPE = 0
# 0表示默认不发送任何通知 1代表钉钉通知2代表企业微信通知 3代表邮件通知 4代表所有途径都发送通知
SEND_RESULT_TYPE = 0

View File

@@ -11,16 +11,10 @@ from config.global_vars import ENV_VARS, GLOBAL_VARS
import pytest
from py._xmlgen import html # 安装pytest-html版本最好是2.1.1
from time import strftime
import re
# ------------------------------------- START: 报告处理 ---------------------------------------#
def pytest_collection_modifyitems(items):
"""# 测试用例执行收集完成时将收集到的item的name和nodeid的中文显示在控制台上"""
for item in items:
item.name = item.name.encode("utf-8").decode("unicode-escape")
item._nodeid = item._nodeid.encode("utf-8").decode("unicode_escape")
@pytest.mark.hookwrapper
def pytest_runtest_makereport(item, call):
"""设置列"用例描述"的值为用例的标题title"""
@@ -28,9 +22,10 @@ def pytest_runtest_makereport(item, call):
# 获取调用结果的测试报告返回一个report对象
# report对象的属性包括whensteup, call, teardown三个值、nodeid(测试用例的名字)、outcome(用例的执行结果passed,failed)
report = outcome.get_result()
# 将测试方法的文档注释作为结果表的Description的值如果文档注释为空则测试方法名作为结果表的Description的值
report.description = str(item.function.__doc__)
report.nodeid = report.nodeid.encode("utf-8").decode("unicode_escape")
# 将测试用例的title作为测试报告"用例描述"列的值
# 注意参数传递时需要这样写:@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases])
report.description = re.findall('\[(.*?)\]', report.nodeid)[0]
report.func = report.nodeid.split("[")[0]
def pytest_html_report_title(report):
@@ -73,10 +68,13 @@ def pytest_html_results_table_header(cells):
"""
修改结果表的表头
"""
cells.pop(1) # 移除 "Test" 列
# 往表格中增加一列"用例描述",并且给"用例描述"增加排序
cells.insert(0, html.th('用例描述', class_="sortable", col="name"))
# 往表格中增加一列"用例方法",并且给"用例方法"增加排序
cells.insert(1, html.th('用例方法', class_="sortable", col="name"))
# 往表格中增加一列"执行时间",并且给"执行时间"增加排序
cells.insert(1, html.th('执行时间', class_="sortable time", col="time"))
cells.insert(2, html.th('执行时间', class_="sortable time", col="time"))
@pytest.mark.optionalhook
@@ -84,10 +82,13 @@ def pytest_html_results_table_row(report, cells):
"""
修改结果表的表头后给对应的行增加值
"""
cells.pop(1) # 移除 "Test" 列
# 往列"用例描述"插入每行的值
cells.insert(0, html.td(report.description))
# 往列"用例方法"插入每行的值
cells.insert(1, html.td(report.func))
# 往列"执行时间"插入每行的值
cells.insert(1, html.td(strftime("%Y-%m-%d %H:%M:%S"), class_="col-time"))
cells.insert(2, html.td(strftime("%Y-%m-%d %H:%M:%S"), class_="col-time"))
def pytest_html_results_table_html(report, data):

View File

@@ -1,24 +1,24 @@
# 公共参数
case_common:
allure_epic: GitLink接口 # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_epic: GitLink接口(手动编写用例) # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_feature: 登录模块 # 功能点的描述相当于class级的标签, 理解成模块往下是 story
allure_story: 登录接口 # 故事可以理解为场景相当于method级的标签, 往下是 title
# 用例数据
case_info:
- feature: 登录
case_login_demo_01:
feature: 登录
title: 用户名密码正确,登录成功(不校验数据库)
run: True
url: /api/accounts/login.json
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies:
pk: json
request_type: json
payload: { "login": "${login}","password": "${password}","autologin": 1 }
files:
extract:
nickname: $.username
login: $.login
user_id: $.user_id
run: False
assert_response:
eq:
http_code: 200
@@ -32,20 +32,22 @@ case_info:
not:
$.user_id: 85390
assert_sql:
- feature: 登录
case_login_demo_02:
feature: 登录
title: 用户名密码正确,登录成功(校验数据库)
run: False
url: /api/accounts/login.json
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies:
pk: json
request_type: json
payload: { "login": "${login}","password": "${password}","autologin": 1 }
files:
extract:
nickname: $.username
login: $.login
user_id: $.user_id
run: False
assert_response:
eq:
http_code: 200
@@ -62,19 +64,20 @@ case_info:
eq:
sql: select count(*) from tokens where user_id=${user_id};
len: 1
- feature: 登录
case_login_demo_03:
feature: 登录
title: 用户名正确,密码错误,登录失败
run: False
url: /api/accounts/login.json
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies:
pk: json
request_type: json
payload: { "login": "chytest10","password": "password111","autologin": 1 }
files:
extract:
run: False
assert_response:
eq:
http_code: 200
$.status: -2
assert_sql:
assert_sql:

BIN
data/test_gitlink_demo.xlsx Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,19 +1,21 @@
# 公共参数
case_common:
allure_epic: GitLink接口 # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_epic: GitLink接口(自动生成用例) # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_feature: 开源项目模块 # 功能点的描述相当于class级的标签, 理解成模块往下是 story
allure_story: 新建项目接口 # 故事可以理解为场景相当于method级的标签, 往下是 title
# 用例数据
case_info:
- feature: 新建项目
case_new_project_demo_01:
feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(不校验数据库, header里面传cookies
run: True
url: /api/projects.json
method: POST
headers:
Content-Type: application/json; charset=utf-8;
cookies: ${login_cookie}
cookies:
pk: json
request_type: json
payload:
"user_id": ${user_id}
"name": ${faker.name().replace(" ", "").replace(".", "")}
@@ -23,20 +25,22 @@ case_info:
project_id: $.id
project_name: $.name
project_identifier: $.identifier
run: True
assert_response:
eq:
http_code: 200
$.login: ${login}
assert_sql:
- feature: 新建项目
case_new_project_demo_02:
feature: 新建项目
title: 正确输入各项必填参数新建项目成功不校验数据库单独传cookies
run: True
url: /api/projects.json
method: POST
headers:
Content-Type: application/json; charset=utf-8;
cookies: ${login_cookie}
pk: json
request_type: json
payload:
"user_id": ${user_id}
"name": ${faker.name().replace(" ", "").replace(".", "")}
@@ -46,19 +50,21 @@ case_info:
project_id: $.id
project_name: $.name
project_identifier: $.identifier
run: True
assert_response:
eq:
http_code: 200
$.login: ${login}
assert_sql:
- feature: 新建项目
case_new_project_demo_03:
feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(校验数据库)
run: False
url: /api/projects.json
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}
cookies:
pk: json
request_type: json
payload:
"user_id": ${user_id}
"name": ${faker.name().replace(" ", "").replace(".", "")}
@@ -68,14 +74,13 @@ case_info:
project_id: $.id
project_name: $.name
project_identifier: $.identifier
run: False
assert_response:
eq:
http_code: 200
$.login: ${login}
assert_sql:
eq:
sql: select id, `name`, identifier from projects where user_id=${user_id} ORDER BY created_on DESC;
sql: select id,`name`, identifier from projects where user_id=${user_id} ORDER BY created_on DESC;
$.id: ${project_id}
$.name: ${project_name}
$.identifier: ${project_identifier}

View File

@@ -4,7 +4,8 @@ addopts = -s --cache-clear --capture=sys --self-contained-html --reruns=0 --reru
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
markers =
test_login_demo: login
test_login_excel_demo: login excel
test_gitlink_demo_dlmkexcel: login excel
test_gitlink_demo_xjxmmkexcel: new project excel
test_new_project_demo: new_project
test_softbot: softbot
auto: auto generate case
auto: auto generate case
test_demo: demo case

13
run.py
View File

@@ -20,7 +20,7 @@ import shutil
import pytest
from config.path_config import REPORT_DIR, LOG_DIR, AUTO_CASE_DIR, CONF_DIR, LIB_DIR, ALLURE_RESULTS_DIR, \
ALLURE_HTML_DIR
from case_utils.case_handle import get_case_data
from case_utils.case_fun_handle import generate_cases
from loguru import logger
import click
from config.settings import LOG_LEVEL
@@ -55,6 +55,11 @@ def run(env, m, report):
|_|
Starting ... ... ...
""")
# ------------------------ 设置全局变量 ------------------------
# 根据指定的环境参数将运行环境所需相关配置数据保存到GLOBAL_VARS
GLOBAL_VARS["env_key"] = env.lower()
for k, v in ENV_VARS[env.lower()].items():
GLOBAL_VARS[k] = v
# ------------------------ 自动生成测试用例 ------------------------
# 删除原有的测试用例,以便生成新的测试用例
if os.path.exists(AUTO_CASE_DIR):
@@ -62,7 +67,7 @@ def run(env, m, report):
shutil.rmtree(AUTO_CASE_DIR)
# 根据data里面的yaml/excel文件自动生成测试用例
get_case_data()
generate_cases()
# ------------------------ pytest执行测试用例 ------------------------
"""
@@ -80,10 +85,6 @@ def run(env, m, report):
"--reruns=3", "--reruns-delay=2"
"""
arg_list = []
# 根据指定的环境参数将运行环境所需相关配置数据保存到GLOBAL_VARS
GLOBAL_VARS["env_key"] = env.lower()
for k, v in ENV_VARS[env.lower()].items():
GLOBAL_VARS[k] = v
# 执行指定测试用例
if m is not None:
arg_list.append(f"-m {m}")

View File

@@ -13,6 +13,18 @@ from loguru import logger
from common_utils.base_request import BaseRequest
@pytest.fixture(scope="function", autouse=True)
def case_skip(request):
"""处理跳过用例"""
# 使用 request.getfixturevalue() 方法来获取测试用例函数的参数值
# 注意这里的"case"需要与@pytest.mark.parametrize("case", cases)中传递的保持一致
case = request.getfixturevalue("case")
if case.get("run") is None or case.get("run") is False:
reason = f"{case.get('title')}: 标记了该用例为false不执行\\n"
logger.warning(f"{reason}")
pytest.skip(reason)
@pytest.fixture(scope="session", autouse=True)
def login_init():
"""
@@ -28,7 +40,7 @@ def login_init():
req_data = {
"url": host + "/api/accounts/login.json",
"method": "POST",
"pk": "json",
"request_type": "json",
"headers": {"Content-Type": "application/json; charset=utf-8;"},
"payload": {"login": login, "password": password, "autologin": 1}
}

View File

@@ -10,33 +10,25 @@
import pytest
from loguru import logger
from pytest_html import extras # 往pytest-html报告中填写额外的内容
from common_utils.func_handle import add_docstring
import allure
from case_utils.allure_handle import allure_title, allure_step
# 读取用例数据
cases = [{"title": "demo case 01", "user": "flora1", "age": 17, "run": False},
{"title": "demo case 02", "user": "lucy", "age": 17, "run": False}]
cases = [{"title": "demo用例01", "user": "flora", "age": 17, "run": True},
{"title": "demo用例02", "user": "lucy", "age": 17, "run": False}]
@allure.story("demo模块")
@pytest.mark.test_login_demo
@pytest.mark.parametrize("case", cases)
@allure.story("demo模块(手动用例)")
@pytest.mark.test_demo
@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases])
def test_demo(case, extra):
logger.info("-----------------------------START-开始执行用例-----------------------------")
logger.debug(f"当前执行的用例数据:{case}")
# 给当前测试方法添加文档注释
add_docstring(case.get("title", ""))(test_demo)
# 添加用例标题作为allure中显示的用例标题
allure_title(case.get("title", ""))
if case.get("run", None):
# 将用例数据显示在pytest-html报告中
extra.append(extras.json(case, name="用例数据"))
# 在allure报告中显示请求的用例数据
allure_step(step_title="用例数据", content=f"{case}")
assert case["user"] == "flora"
else:
reason = f"标记了该用例为false不执行\\n"
logger.warning(f"{reason}")
pytest.skip(reason)
# 将用例数据显示在pytest-html报告中
extra.append(extras.json(case, name="用例数据"))
# 在allure报告中显示请求的用例数据
allure_step(step_title="用例数据", content=f"{case}")
assert case["user"] == "flora"
logger.info("-----------------------------END-用例执行结束-----------------------------")

View File

@@ -15,27 +15,33 @@ from case_utils.assert_handle import assert_response, assert_sql
from loguru import logger
from case_utils.request_data_handle import RequestPreDataHandle, RequestHandle, after_request_extract
from pytest_html import extras # 往pytest-html报告中填写额外的内容
from common_utils.func_handle import add_docstring
from case_utils.allure_handle import allure_title
import allure
from config.settings import db_info
from config.global_vars import GLOBAL_VARS
# 读取用例数据
cases = YamlHandle(filename=os.path.join(DATA_DIR, "test_login_demo.yaml")).read_yaml
yaml_data = YamlHandle(filename=os.path.join(DATA_DIR, "login_demo.yaml")).read_yaml
case_common = yaml_data["case_common"]
cases = []
for k, v in yaml_data.items():
if k != "case_common":
cases.append(v)
@allure.story(f'{cases["case_common"]["allure_story"]}')
@pytest.mark.test_login_demo
@pytest.mark.parametrize("case", cases.get("case_info"))
def test_login_demo(case, extra):
logger.info("-----------------------------START-开始执行用例-----------------------------")
logger.debug(f"当前执行的用例数据:{case}")
# 给当前测试方法添加文档注释
add_docstring(case.get("title", ""))(test_login_demo)
# 添加用例标题作为allure中显示的用例标题
allure_title(case.get("title", ""))
if case.get("run", None):
@allure.epic(case_common["allure_epic"])
@allure.feature(case_common["allure_feature"])
class TestLoginDemo:
@allure.story(case_common["allure_story"])
@pytest.mark.test_login_demo
@pytest.mark.auto
@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases])
def test_login_demo_auto(self, case, extra):
logger.info("-----------------------------START-开始执行用例-----------------------------")
logger.debug(f"当前执行的用例数据:{case}")
# 添加用例标题作为allure中显示的用例标题
allure_title(case.get("title", ""))
# 处理请求前的用例数据
case_data = RequestPreDataHandle(case).request_data_handle()
# 将用例数据显示在pytest-html报告中
@@ -50,8 +56,4 @@ def test_login_demo(case, extra):
assert_sql(db_info[GLOBAL_VARS["env_key"]], case_data["assert_sql"])
# 断言成功后进行参数提取
after_request_extract(response, case_data.get("extract", None))
else:
reason = f"标记了该用例为false不执行\\n"
logger.warning(f"{reason}")
pytest.skip(reason)
logger.info("-----------------------------END-用例执行结束-----------------------------")
logger.info("-----------------------------END-用例执行结束-----------------------------")