实现通过用例数据动态配置pytest.mark, 包括自定义标记,pytest.mark.skip以及pytest,mark.usefixtues功能

This commit is contained in:
floraachy
2023-07-03 11:27:08 +08:00
parent 18089f35bd
commit 165deebedc
13 changed files with 148 additions and 55 deletions

View File

@@ -28,6 +28,7 @@
* 动态多断言: 如接口需要同时校验响应数据和sql校验支持多场景断言
* 框架天然支持接口动态传参、关联灵活处理
* 支持测试数据分析,测试数据不符合规范有预警机制
* 支持通过用例数据动态配置pytest.mark 包括自定义标记pytest.mark.skip以及pytest,mark.usefixtues
* 测试数据隔离, 实现数据驱动
* 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用
* 多种报告随心选择框架支持pytest-html以及Allure测试报告可以动态配置所需报告
@@ -183,6 +184,8 @@ case_common :公共参数
allure_epic用作于@allure.epic()装饰器中的内容。
allure_feature用作于@allure.feature()装饰器中的内容。
allure_story用作于@allure.story()装饰器中的内容。
case_markers: 给测试方法添加标记支持自定义标记skip, usefixtures。 格式是列表嵌套字符串或者字典。例如:['glcc', {'skip': '跳过执行该用例'}]
case_001用例ID
feature用例所属模块 类似于@allure.feature()。
title用例标题
@@ -193,7 +196,7 @@ case_001用例ID
cookies请求cookies格式是DICT CookieJar对象
request_type请求数据类型params, json, file, data
payload请求参数
files: 需要上传的文件,参考如下传参:{接口中文件参数的名称:文件路径地址/[文件地址1, 文件地址2]}
files: 需要上传的文件,参考如下传参:{接口中文件参数的名称:'文件路径地址'/['文件地址1', '文件地址2']}
extract后置提取参数
assert_response响应断言
assert_sql数据库断言
@@ -251,42 +254,46 @@ excel表单2名称是示例模块
## 九 、详细功能说明
- [如何实现动态数据、随机数据的热加载?](https://www.gitlink.org.cn/zone/tester/newdetail/204)
- [如何实现动态数据、随机数据的热加载?](https://www.gitlink.org.cn/zone/tester/newdetail/236)
我们有些特殊的场景,可能会涉及到一些定制化的数据,每次执行数据,需要按照指定规则随机生成,实时加载数据,那么这部分应该如何处理呢?
- [如何提取响应数据作为全局变量并使用](https://www.gitlink.org.cn/zone/tester/newdetail/205)
在测试过程中,通常下一个接口需要用到上一个接口的响应数据,这个时候就涉及到参数的提取。
- [如何进行响应数据断言?](https://www.gitlink.org.cn/zone/tester/newdetail/206)
持5种响应断言方式eq in gt lt not。
- [如何进行数据库断言?](https://www.gitlink.org.cn/zone/tester/newdetail/207)
目前暂时支持两种数据库断言方式len eq。其他方式待扩展。
- [如何配置邮箱通知?](https://www.gitlink.org.cn/zone/tester/newdetail/208)
我们通过第三方模块yagmail发送邮件。
- [如何配置钉钉通知?](https://www.gitlink.org.cn/zone/tester/newdetail/209)
我们通过封装钉钉机器人发送钉钉通知。
- [如何配置企业微信通知?](https://www.gitlink.org.cn/zone/tester/newdetail/210)
过封装企业微信机器人发送通知。
- [如何测试上传文件接口?](https://www.gitlink.org.cn/zone/tester/newdetail/211)
我们通过MultipartEncoder的方式进行文件上传。
- [如何处理同一环境存在多域名的情况?](https://www.gitlink.org.cn/zone/tester/newdetail/214)
- [如何处理同一环境存在多域名的情况](https://www.gitlink.org.cn/zone/tester/newdetail/234)
很多公司,通常一套环境是由多个微服务组成。每一个微服务具备不同的域名。那么针对这种同一环境存在多域名的情况,我们应该如何处理呢?
- [如何处理同一套框架测试多套环境的情况?](https://www.gitlink.org.cn/zone/tester/newdetail/215)
- [如何处理同一套框架测试多套环境的情况?](https://www.gitlink.org.cn/zone/tester/newdetail/233)
假如我想要我的自动化代码分别在不同环境执行,如何处理呢?
- [如何处理用例中需要依赖登录的token/cookies的情况](https://www.gitlink.org.cn/zone/tester/newdetail/228)
- [如何处理用例中需要依赖登录的token/cookies的情况](https://www.gitlink.org.cn/zone/tester/newdetail/235)
我们进行测试的时候,很多接口都是需要先登录之后再进行操作。但是我们不可能每测试一次接口,都登录一次吧,这样有点冗余了。那么,针对这种情况如何处理呢?
- [如何测试上传文件接口?](https://www.gitlink.org.cn/zone/tester/newdetail/238)
我们通过MultipartEncoder的方式进行文件上传。
- [如何通过用例数据动态配置pytest.mark](https://www.gitlink.org.cn/zone/tester/newdetail/257)
在测试过程中我们经常需要对测试用例进行分类运行时仅执行这一类用例。为了实现这一功能我在测试用例中引入了添加pytest的自定义标记的功能同时扩展支持了pytest.mark.skip以及pytest,mark.usefixtues。
注意目前这一功能仅支持通过YAML格式编写用例。EXCEL用例暂时不支持。
- [如何提取响应数据作为全局变量并使用?](https://www.gitlink.org.cn/zone/tester/newdetail/237)
在测试过程中,通常下一个接口需要用到上一个接口的响应数据,这个时候就涉及到参数的提取。
- [如何进行响应数据断言?](https://www.gitlink.org.cn/zone/tester/newdetail/239)
目前支持5种响应断言方式eq in gt lt not。
- [如何进行数据库断言?](https://www.gitlink.org.cn/zone/tester/newdetail/240)
目前暂时支持两种数据库断言方式len eq。其他方式待扩展。
- [如何配置邮箱通知?](https://www.gitlink.org.cn/zone/tester/newdetail/242)
我们通过第三方模块yagmail发送邮件。
- [如何配置钉钉通知?](https://www.gitlink.org.cn/zone/tester/newdetail/243)
我们通过封装钉钉机器人发送钉钉通知。
- [如何配置企业微信通知?](https://www.gitlink.org.cn/zone/tester/newdetail/241)
我们通过封装企业微信机器人发送通知。
## 十、初始化项目可能遇到的问题
- [测试机安装的是python3.7但是本框架要求3.9.5,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/212)
- [无法安装依赖包或者安装很慢,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/213)
- [测试机安装的是python3.7但是本框架要求3.9.5,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/245)
- [无法安装依赖包或者安装很慢,怎么办?](https://www.gitlink.org.cn/zone/tester/newdetail/244)

View File

@@ -8,6 +8,7 @@
import os
from string import Template
import datetime
import re
# 第三方库导入
from xpinyin import Pinyin # 纯 Python 编写的中文字符串拼音转换模块,不需要依赖外部程序和词库。
from loguru import logger
@@ -18,6 +19,7 @@ from common_utils.files_handle import get_files, get_relative_path
from config.models import CaseFileType
from config.settings import CASE_FILE_TYPE
from config.path_config import DATA_DIR, CASE_TEMPLATE_DIR, AUTO_CASE_DIR
from config.global_vars import CUSTOM_MARKERS
from case_utils.case_data_analysis import CaseDataCheck
"""
@@ -138,16 +140,17 @@ def generate_cases():
if CASE_FILE_TYPE == CaseFileType.EXCEL.value:
# 在用例数据"DATA_DIR"目录中寻找后缀是xlsx, xls的文件
excel_files = get_files(target=DATA_DIR, start="test_", end=".xlsx") \
+ get_files(target=DATA_DIR, start="test_", end=".xls")
+ get_files(target=DATA_DIR, start="test_", end=".xls")
elif CASE_FILE_TYPE == CaseFileType.YAML.value:
# 在用例数据"DATA_DIR"目录中寻找后缀是yaml, yml的文件
yaml_files = get_files(target=DATA_DIR, start="test_", end=".yaml") \
+ get_files(target=DATA_DIR, start="test_", end=".yml")
+ get_files(target=DATA_DIR, start="test_", end=".yml")
elif CASE_FILE_TYPE == CaseFileType.ALL.value:
# 在用例数据"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")
end=".xls")
yaml_files = get_files(target=DATA_DIR, start="test_", end=".yaml") + get_files(target=DATA_DIR, start="test_",
end=".yml")
else:
logger.error(f"{CASE_FILE_TYPE}不在CaseFileType内不能自动生成用例")
# 自动生成测试用例
@@ -173,9 +176,28 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
如果设置为 True则不论目录是否已存在os.makedirs 都不会报错;如果设置为 False默认值则在目录已存在时会引发 FileExistsError 异常。
"""
os.makedirs(target_case_path, exist_ok=True)
# 定义生成的测试用例的模板
# 获取用例数据中的标记
case_markers = case_common.get("case_markers", [])
logger.debug(f"从用例中拿到的标记有:{case_markers}")
# 先读取用例模板中每一行的内容
with open(file=case_template_path, mode="r", encoding="utf-8") as f:
case_template = f.read()
case_template = f.readlines()
current_case_template = []
for line_num, content in enumerate(case_template):
current_case_template.append(content)
# 这里是预计往 @pytest.mark.parametrize( 这一行的上面插入标记
if content.strip().startswith('@pytest.mark.parametrize('):
# 往测试用例模板中插入自定义标记
for case_marker in case_markers:
# 获取符合要求格式的自定义标记名称,并插入到测试模板中
marker = is_valid_marker(case_marker)
if marker and isinstance(marker, str):
# 注意这里的4个空格必须要有4个空格
current_case_template.append(f" @pytest.mark.{marker}\n")
if marker and isinstance(marker, dict):
for k, v in marker.items():
# 注意这里的4个空格必须要有4个空格
current_case_template.append(f" @pytest.mark.{k}('{v}')\n")
# 将用例数据的名称作为测试用例文件名称, 如test_login_demo
func_name = filename
# 方法名test_demo的类名是TestDemo
@@ -184,11 +206,13 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
"""
string.Template是将一个string设置为模板通过替换变量的方法最终得到想要的string。
"""
my_case = Template(case_template).safe_substitute(
current_template = ''.join(current_case_template)
my_case = Template(current_template).safe_substitute(
{
"allure_epic": case_common["allure_epic"],
"allure_feature": case_common["allure_feature"],
"allure_story": case_common["allure_story"],
"case_markers": case_common["case_markers"],
"case_data": case_data,
"func_title": func_name,
"class_title": class_name,
@@ -199,3 +223,38 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
# 将测试用例方法写入py文件中
with open(os.path.join(target_case_path, func_name + '.py'), "w", encoding="utf-8") as fp:
fp.write(my_case)
def is_valid_marker(markers):
"""
检查标记名称是否合法:仅支持非数字/下划线开头,由数字,字母,下划线组成的标记名称
"""
pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*$'
if isinstance(markers, str):
if re.match(pattern, markers):
# 将自定义标记放到CUSTOM_MARKERS 方便后续统一注册
if markers not in ("skip", "skipif", "parametrize", "usefixtures", "xfail", "filterwarings"):
CUSTOM_MARKERS.append(markers)
# 返回合法有效的标记名称,用于添加到测试方法中
return markers
else:
logger.error(f"{markers} 格式不合法, 建议仅输入数字,字母,下划线组合,且不能以数字,下划线开头")
return False
elif isinstance(markers, dict):
if len(markers) == 1:
marker_name = list(markers.keys())[0]
if re.match(pattern, marker_name):
# 将自定义标记放到CUSTOM_MARKERS 方便后续统一注册
if marker_name not in ("skip", "skipif", "parametrize", "usefixtures", "xfail", "filterwarings"):
CUSTOM_MARKERS.append(markers)
return markers
else:
logger.error(f"{markers} 格式不合法, 建议仅输入数字,字母,下划线组合,且不能以数字,下划线开头")
return None
else:
logger.error(f"{markers} 格式不合法, 只能存在一对键值对")
return None
else:
logger.error(f"{markers} 仅支持字符串或者字典格式")
return None

View File

@@ -4,9 +4,13 @@
# @File : global_vars.py
# @Software: PyCharm
# @Desc:
# 定义一个全局变量,用于存储运行过程中相关数据
GLOBAL_VARS = {}
# 定义一个变量。存储自定义的标记markers
CUSTOM_MARKERS = []
ENV_VARS = {
"common": {
"report_title": "自动化测试报告",

View File

@@ -8,7 +8,7 @@
# ------------------------------------ 配置信息 ----------------------------------------------------#
# 0代表执行Excel和yaml两种格式的用例 1 代表 yaml文件2 用例代表Excel用例 其他数值将不自动生成用例,仅能执行手动编写的用例
CASE_FILE_TYPE = 3
CASE_FILE_TYPE = 1
# 0表示默认不发送任何通知 1代表钉钉通知2代表企业微信通知 3代表邮件通知 4代表所有途径都发送通知
SEND_RESULT_TYPE = 0

View File

@@ -15,13 +15,14 @@ from loguru import logger
from py._xmlgen import html # 安装pytest-html版本最好是2.1.1
import pytest
# 本地应用/模块导入
from config.global_vars import ENV_VARS, GLOBAL_VARS
from config.global_vars import ENV_VARS, GLOBAL_VARS, CUSTOM_MARKERS
# ------------------------------------- START: pytest钩子函数处理---------------------------------------#
def pytest_configure(config):
"""
# 在测试运行前修改Environment部分信息配置测试报告环境信息
1. 在测试运行前修改Environment部分信息配置测试报告环境信息
2. 注册自定义标记
"""
# 给环境表 添加项目名称及开始时间
config._metadata["项目名称"] = ENV_VARS["common"]["project_name"]
@@ -29,10 +30,17 @@ def pytest_configure(config):
# 给环境表 移除packages 及plugins
config._metadata.pop("Packages")
config._metadata.pop("Plugins")
# 向pytest的配置中添加marker
# TODO 暂时还没给用例添加
config.addinivalue_line("markers", 'smoke')
config.addinivalue_line("markers", '回归测试')
# 注册自定义标记
print(f"需要注册的标记:{CUSTOM_MARKERS}")
markers = list(set(CUSTOM_MARKERS))
for custom_marker in markers:
if isinstance(custom_marker, str):
config.addinivalue_line('markers', f'{custom_marker}')
print(f"注册了自定义标记:{custom_marker}")
elif isinstance(custom_marker, dict):
for k, v in custom_marker.items():
config.addinivalue_line('markers', f'{k}:{v}')
print(f"注册了自定义标记:{custom_marker}")
@pytest.hookimpl(tryfirst=True)
@@ -83,7 +91,7 @@ def pytest_terminal_summary(terminalreporter, config):
"-------------测试结果--------------------\n"
f"用例总数: {_TOTAL}\n"
f"跳过用例数: {_SKIPPED}\n"
f"实际执行用例总数: {_TOTAL - _SKIPPED}\n\n"
f"实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED}\n\n"
f"异常用例数: {_ERROR}\n"
f"失败用例数: {_FAILED}\n"
f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n"

View File

@@ -1,7 +1,11 @@
case_common:
allure_epic: GitLink接口(自动生成用例)
allure_epic: GitLink接口自动生成用例
allure_feature: 开源夏令营模块
allure_story: 获取项目列表接口
case_markers:
- glcc
- get_project
- skip: 跳过执行该用例
case_glcc_demo_01:
feature: GLCC

View File

@@ -3,11 +3,16 @@ case_common:
allure_epic: GitLink接口手动编写用例 # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_feature: 登录模块 # 功能点的描述相当于class级的标签, 理解成模块往下是 story
allure_story: 登录接口 # 故事可以理解为场景相当于method级的标签, 往下是 title
case_markers: # pytest框架的标记 pytest.mark.
- glcc: glcc相关的接口
- get_project
- skip: 跳过执行该用例
# 用例数据
case_login_demo_01:
feature: 登录
title: 用户名密码正确,登录成功(不校验数据库)
run: False
run: True
url: /api/accounts/login.json
method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"}

View File

@@ -3,6 +3,10 @@ case_common:
allure_epic: GitLink接口自动生成用例 # 敏捷里面的概念定义史诗相当于module级的标签, 往下是 feature
allure_feature: 开源项目模块 # 功能点的描述相当于class级的标签, 理解成模块往下是 story
allure_story: 新建项目接口 # 故事可以理解为场景相当于method级的标签, 往下是 title
case_markers:
- gitlink
- new_project
- usefixtures: login_init
# 用例数据
case_new_project_demo_01:
@@ -34,7 +38,7 @@ case_new_project_demo_01:
case_new_project_demo_02:
feature: 新建项目
title: 正确输入各项必填参数新建项目成功不校验数据库单独传cookies
run: True
run: False
url: /api/projects.json
method: POST
headers:
@@ -88,7 +92,7 @@ case_new_project_demo_03:
case_new_project_demo_04:
feature: 新建项目
title: 正确输入各项必填参数新建项目成功04
run: True
run: False
url: /api/projects.json
method: POST
headers:
@@ -113,7 +117,7 @@ case_new_project_demo_04:
case_new_project_demo_05:
feature: 新建项目
title: 正确输入各项必填参数新建项目成功05
run: True
run: False
url: /api/projects.json
method: POST
headers:

View File

@@ -1,14 +1,17 @@
# 公共参数
case_common:
allure_epic: GitLink接口(自动生成用例)
allure_epic: GitLink接口自动生成用例
allure_feature: 上传文件模块
allure_story: 上传文件
case_markers:
- gitlink
- upload_file
# 用例数据
case_upload_demo_01:
feature: 上传文件
title: 测试单文件上传
run: True
run: False
url: /api/attachments.json
method: POST
headers:

View File

@@ -9,6 +9,5 @@ addopts =
--reruns-delay=5
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
markers =
test_login_demo: login
auto: auto generate case
test_demo: demo case

1
run.py
View File

@@ -10,6 +10,7 @@
1、用例创建原则测试文件名必须以“test”开头测试函数必须以“test”开头。
2、运行方式
> python run.py (默认在test环境运行测试用例, 报告采用allure)
> python run.py -m demo 在test环境仅运行打了标记demo用例 默认报告采用allure
> python run.py -env live 在live环境运行测试用例
> python run.py -env=test 在test环境运行测试用例
> python run.py -report=pytest-html (默认在test环境运行测试用例, 报告采用pytest-html)

View File

@@ -25,7 +25,7 @@ def case_skip(request):
pytest.skip(reason)
@pytest.fixture(scope="session", autouse=True)
@pytest.fixture(scope="session")
def login_init():
"""
获取登录的cookie

View File

@@ -34,8 +34,7 @@ for k, v in yaml_data.items():
class TestLoginDemo:
@allure.story(case_common["allure_story"])
@pytest.mark.test_login_demo
@pytest.mark.auto
@pytest.mark.test_demo
@pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases])
def test_login_demo_auto(self, case, extra):
logger.info("\n-----------------------------START-开始执行用例-----------------------------\n")