支持利用allure设置用例优先级,运行指定优先级的用例

This commit is contained in:
floraachy
2023-07-04 11:44:46 +08:00
parent d691d0bd61
commit 64ae0c0152
14 changed files with 91 additions and 12 deletions
+4
View File
@@ -29,6 +29,7 @@
* 框架天然支持接口动态传参、关联灵活处理 * 框架天然支持接口动态传参、关联灵活处理
* 支持测试数据分析,测试数据不符合规范有预警机制 * 支持测试数据分析,测试数据不符合规范有预警机制
* 支持通过用例数据动态配置pytest.mark 包括自定义标记,pytest.mark.skip以及pytest,mark.usefixtues * 支持通过用例数据动态配置pytest.mark 包括自定义标记,pytest.mark.skip以及pytest,mark.usefixtues
* 支持利用allure设置用例优先级,运行指定优先级的用例
* 测试数据隔离, 实现数据驱动 * 测试数据隔离, 实现数据驱动
* 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用 * 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用
* 多种报告随心选择:框架支持pytest-html以及Allure测试报告,可以动态配置所需报告 * 多种报告随心选择:框架支持pytest-html以及Allure测试报告,可以动态配置所需报告
@@ -272,6 +273,9 @@ excel表单2名称是:示例模块
在测试过程中,我们经常需要对测试用例进行分类,运行时仅执行这一类用例。为了实现这一功能,我在测试用例中引入了添加pytest的自定义标记的功能,同时扩展支持了pytest.mark.skip以及pytest,mark.usefixtues。 在测试过程中,我们经常需要对测试用例进行分类,运行时仅执行这一类用例。为了实现这一功能,我在测试用例中引入了添加pytest的自定义标记的功能,同时扩展支持了pytest.mark.skip以及pytest,mark.usefixtues。
注意:目前这一功能,仅支持通过YAML格式编写用例。EXCEL用例暂时不支持。 注意:目前这一功能,仅支持通过YAML格式编写用例。EXCEL用例暂时不支持。
- [如何设置测试用例优先级](https://www.gitlink.org.cn/zone/tester/newdetail/260)
测试用例会分优先级, 我们可以利用allure的特性设置用例优先级,以及运行指定优先级的用例。
- [如何提取响应数据作为全局变量并使用?](https://www.gitlink.org.cn/zone/tester/newdetail/237) - [如何提取响应数据作为全局变量并使用?](https://www.gitlink.org.cn/zone/tester/newdetail/237)
在测试过程中,通常下一个接口需要用到上一个接口的响应数据,这个时候就涉及到参数的提取。 在测试过程中,通常下一个接口需要用到上一个接口的响应数据,这个时候就涉及到参数的提取。
+15 -3
View File
@@ -5,8 +5,10 @@
# @Software: PyCharm # @Software: PyCharm
# @Desc: 分析用例数据是否符合规范 # @Desc: 分析用例数据是否符合规范
# 标准库导入
from typing import Text from typing import Text
from config.models import TestCase, TestCaseEnum, Method, RequestType # 第三方库导入
from config.models import TestCase, TestCaseEnum, Method, RequestType, Severity
class CaseDataCheck: class CaseDataCheck:
@@ -26,6 +28,15 @@ class CaseDataCheck:
self.case_data.get(TestCaseEnum.METHOD.value[0]) self.case_data.get(TestCaseEnum.METHOD.value[0])
) )
@property
def get_severity(self) -> Text:
# 如果Severity为空或者不传或者传错,视为NORMAL
attr = self.case_data.get(TestCaseEnum.SEVERITY.value[0])
if attr is None or attr.upper() not in Severity._member_names_:
return "NORMAL"
else:
return attr.upper()
@property @property
def get_request_type(self): def get_request_type(self):
return self.check_params_right( return self.check_params_right(
@@ -55,7 +66,7 @@ class CaseDataCheck:
assert attr.upper() in _member_names_, ( assert attr.upper() in _member_names_, (
f"用例ID为 {self.case_id} 的用例中 {enum_name}: {attr} 填写不正确," f"用例ID为 {self.case_id} 的用例中 {enum_name}: {attr} 填写不正确,"
f"当前框架中只支持 {_member_names_} 类型." f"当前框架中只支持 {_member_names_} 类型."
f"需新增 method 类型,请联系管理员." f"有疑问,请联系管理员."
) )
return attr.upper() return attr.upper()
@@ -81,9 +92,10 @@ class CaseDataCheck:
case_data = { case_data = {
'feature': self.case_data.get(TestCaseEnum.FEATURE.value[0]), 'feature': self.case_data.get(TestCaseEnum.FEATURE.value[0]),
'title': self.case_data.get(TestCaseEnum.TITLE.value[0]), 'title': self.case_data.get(TestCaseEnum.TITLE.value[0]),
'severity': self.get_severity,
'url': self.case_data.get(TestCaseEnum.URL.value[0]), 'url': self.case_data.get(TestCaseEnum.URL.value[0]),
'method': self.get_method,
'run': self.case_data.get(TestCaseEnum.RUN.value[0]), 'run': self.case_data.get(TestCaseEnum.RUN.value[0]),
'method': self.get_method,
'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]), 'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]),
'cookies': self.case_data.get(TestCaseEnum.COOKIES.value[0]), 'cookies': self.case_data.get(TestCaseEnum.COOKIES.value[0]),
'request_type': self.get_request_type, 'request_type': self.get_request_type,
+1 -1
View File
@@ -212,7 +212,7 @@ def gen_case_file(filename, case_template_path, case_common, case_data, target_c
"allure_epic": case_common["allure_epic"], "allure_epic": case_common["allure_epic"],
"allure_feature": case_common["allure_feature"], "allure_feature": case_common["allure_feature"],
"allure_story": case_common["allure_story"], "allure_story": case_common["allure_story"],
"case_markers": case_common["case_markers"], "case_markers": case_markers,
"case_data": case_data, "case_data": case_data,
"func_title": func_name, "func_title": func_name,
"class_title": class_name, "class_title": class_name,
+2
View File
@@ -33,6 +33,7 @@ class RequestPreDataHandle:
logger.debug(f"\n======================================================\n" \ logger.debug(f"\n======================================================\n" \
"-------------Start:处理用例数据前--------------------\n" "-------------Start:处理用例数据前--------------------\n"
f"用例标题(title): {type(request_data.get('title', None))} || {request_data.get('title', None)}\n" \ f"用例标题(title): {type(request_data.get('title', None))} || {request_data.get('title', None)}\n" \
f"用例优先级(severity): {type(request_data.get('severity', None))} || {request_data.get('severity', None)}\n" \
f"请求路径(url): {type(request_data.get('url', None))} || {request_data.get('url', None)}\n" \ f"请求路径(url): {type(request_data.get('url', None))} || {request_data.get('url', None)}\n" \
f"请求方式(method): {type(request_data.get('method', None))} || {request_data.get('method', None)}\n" \ f"请求方式(method): {type(request_data.get('method', None))} || {request_data.get('method', None)}\n" \
f"请求头(headers): {type(request_data.get('headers', None))} || {request_data.get('headers', None)}\n" \ f"请求头(headers): {type(request_data.get('headers', None))} || {request_data.get('headers', None)}\n" \
@@ -61,6 +62,7 @@ class RequestPreDataHandle:
logger.debug(f"\n======================================================\n" \ logger.debug(f"\n======================================================\n" \
"-------------End:处理用例数据后--------------------\n" "-------------End:处理用例数据后--------------------\n"
f"用例标题(title): {type(self.request_data.get('title', None))} || {self.request_data.get('title', None)}\n" \ f"用例标题(title): {type(self.request_data.get('title', None))} || {self.request_data.get('title', None)}\n" \
f"用例优先级(severity): {type(self.request_data.get('severity', None))} || {self.request_data.get('severity', None)}\n" \
f"请求路径(url): {type(self.request_data.get('url', None))} || {self.request_data.get('url', None)}\n" \ f"请求路径(url): {type(self.request_data.get('url', None))} || {self.request_data.get('url', None)}\n" \
f"请求方式(method): {type(self.request_data.get('method', None))} || {self.request_data.get('method', None)}\n" \ f"请求方式(method): {type(self.request_data.get('method', None))} || {self.request_data.get('method', None)}\n" \
f"请求头(headers): {type(self.request_data.get('headers', None))} || {self.request_data.get('headers', None)}\n" \ f"请求头(headers): {type(self.request_data.get('headers', None))} || {self.request_data.get('headers', None)}\n" \
+25 -1
View File
@@ -6,8 +6,10 @@
# @Software: PyCharm # @Software: PyCharm
# @Desc: 全局变量 # @Desc: 全局变量
# 标准库导入
from enum import Enum, unique # python 3.x版本才能使用 from enum import Enum, unique # python 3.x版本才能使用
from typing import Text, Dict, Callable, Union, Optional, List, Any from typing import Text, Dict, Union, Any
# 第三方库导入
from pydantic import BaseModel from pydantic import BaseModel
@@ -59,10 +61,25 @@ class AllureAttachmentType(Enum):
PDF = "pdf" PDF = "pdf"
class Severity(str, Enum):
"""
测试用例优先级
"""
BLOCKER = 'BLOCKER' # blocker:阻塞缺陷(中断缺陷,客户端程序无响应,无法执行下一步操作)
CRITICAL = 'CRITICAL' # critical:严重缺陷(临界缺陷,功能点缺失)
NORMAL = 'NORMAL' # normal: 一般缺陷(边界情况,格式错误)
MINOR = 'MINOR' # minor:次要缺陷(界面错误与ui需求不符)
TRIVIAL = 'TRIVIAL' # trivial: 轻微缺陷(必须项无提示,或者提示不规范)
class TestCaseEnum(Enum): class TestCaseEnum(Enum):
"""
测试用例中字段
"""
FEATURE = ("feature", False) FEATURE = ("feature", False)
TITLE = ("title", True) TITLE = ("title", True)
URL = ("url", True) URL = ("url", True)
SEVERITY = ("severity", False)
METHOD = ("method", True) METHOD = ("method", True)
HEADERS = ("headers", True) HEADERS = ("headers", True)
COOKIES = ("cookies", False) COOKIES = ("cookies", False)
@@ -76,8 +93,12 @@ class TestCaseEnum(Enum):
class TestCase(BaseModel): class TestCase(BaseModel):
"""
测试用例各数据格式要求
"""
feature: Union[None, Text] = None feature: Union[None, Text] = None
title: Text title: Text
severity: Text
url: Text url: Text
method: Text method: Text
headers: Union[None, Dict, Text] = {} headers: Union[None, Dict, Text] = {}
@@ -92,6 +113,9 @@ class TestCase(BaseModel):
class Method(Enum): class Method(Enum):
"""
请求方式
"""
GET = "GET" GET = "GET"
POST = "POST" POST = "POST"
PUT = "PUT" PUT = "PUT"
+2 -2
View File
@@ -8,13 +8,13 @@
# ------------------------------------ 配置信息 ----------------------------------------------------# # ------------------------------------ 配置信息 ----------------------------------------------------#
# 0代表执行Excel和yaml两种格式的用例, 1 代表 yaml文件,2 用例代表Excel用例, 其他数值将不自动生成用例,仅能执行手动编写的用例 # 0代表执行Excel和yaml两种格式的用例, 1 代表 yaml文件,2 用例代表Excel用例, 其他数值将不自动生成用例,仅能执行手动编写的用例
CASE_FILE_TYPE = 1 CASE_FILE_TYPE = 2
# 0表示默认不发送任何通知, 1代表钉钉通知,2代表企业微信通知, 3代表邮件通知, 4代表所有途径都发送通知 # 0表示默认不发送任何通知, 1代表钉钉通知,2代表企业微信通知, 3代表邮件通知, 4代表所有途径都发送通知
SEND_RESULT_TYPE = 0 SEND_RESULT_TYPE = 0
# 指定日志收集级别 # 指定日志收集级别
LOG_LEVEL = "INFO" LOG_LEVEL = None
""" """
支持的日志级别 支持的日志级别
None 表示捕获所有日志 None 表示捕获所有日志
+2 -1
View File
@@ -91,7 +91,8 @@ def pytest_terminal_summary(terminalreporter, config):
"-------------测试结果--------------------\n" "-------------测试结果--------------------\n"
f"用例总数: {_TOTAL}\n" f"用例总数: {_TOTAL}\n"
f"跳过用例数: {_SKIPPED}\n" f"跳过用例数: {_SKIPPED}\n"
f"实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED}\n\n" f"实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED}\n"
f"通过用例数: {_PASSED}\n"
f"异常用例数: {_ERROR}\n" f"异常用例数: {_ERROR}\n"
f"失败用例数: {_FAILED}\n" f"失败用例数: {_FAILED}\n"
f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n" f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n"
@@ -10,6 +10,7 @@ case_common:
case_glcc_demo_01: case_glcc_demo_01:
feature: GLCC feature: GLCC
title: 获取已报名成功的项目数据 title: 获取已报名成功的项目数据
severity: normal
run: True run: True
url: ${glcc_host}/api/applyInformation/list?curPage=1&pageSize=10000&round=2 url: ${glcc_host}/api/applyInformation/list?curPage=1&pageSize=10000&round=2
method: GET method: GET
@@ -28,6 +29,7 @@ case_glcc_demo_01:
case_glcc_demo_02: case_glcc_demo_02:
feature: GLCC feature: GLCC
title: 获取已报名成功的课题数据 title: 获取已报名成功的课题数据
severity: normal
run: True run: True
url: https://glcc.gitlink.org.cn/api/applyInformation/taskList?curPage=1&pageSize=20&userId=&round=2 url: https://glcc.gitlink.org.cn/api/applyInformation/taskList?curPage=1&pageSize=20&userId=&round=2
method: GET method: GET
+3
View File
@@ -13,6 +13,7 @@ case_login_demo_01:
feature: 登录 feature: 登录
title: 用户名密码正确,登录成功(不校验数据库) title: 用户名密码正确,登录成功(不校验数据库)
run: True run: True
severity: normal
url: /api/accounts/login.json url: /api/accounts/login.json
method: POST method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"} headers: {"Content-Type": "application/json; charset=utf-8;"}
@@ -42,6 +43,7 @@ case_login_demo_02:
feature: 登录 feature: 登录
title: 用户名密码正确,登录成功(校验数据库) title: 用户名密码正确,登录成功(校验数据库)
run: False run: False
severity: minor
url: /api/accounts/login.json url: /api/accounts/login.json
method: POST method: POST
headers: {"Content-Type": "application/json; charset=utf-8;"} headers: {"Content-Type": "application/json; charset=utf-8;"}
@@ -72,6 +74,7 @@ case_login_demo_02:
case_login_demo_03: case_login_demo_03:
feature: 登录 feature: 登录
title: 用户名正确,密码错误,登录失败 title: 用户名正确,密码错误,登录失败
severity: critical
run: False run: False
url: /api/accounts/login.json url: /api/accounts/login.json
method: POST method: POST
Binary file not shown.
+5 -1
View File
@@ -12,6 +12,7 @@ case_common:
case_new_project_demo_01: case_new_project_demo_01:
feature: 新建项目 feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(不校验数据库, header里面传cookies title: 正确输入各项必填参数,新建项目成功(不校验数据库, header里面传cookies
severity: critical
run: True run: True
url: /api/projects.json url: /api/projects.json
method: POST method: POST
@@ -38,7 +39,7 @@ case_new_project_demo_01:
case_new_project_demo_02: case_new_project_demo_02:
feature: 新建项目 feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(不校验数据库,单独传cookies) title: 正确输入各项必填参数,新建项目成功(不校验数据库,单独传cookies)
run: False run: True
url: /api/projects.json url: /api/projects.json
method: POST method: POST
headers: headers:
@@ -63,6 +64,7 @@ case_new_project_demo_02:
case_new_project_demo_03: case_new_project_demo_03:
feature: 新建项目 feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(校验数据库) title: 正确输入各项必填参数,新建项目成功(校验数据库)
severity: normal
run: False run: False
url: /api/projects.json url: /api/projects.json
method: POST method: POST
@@ -92,6 +94,7 @@ case_new_project_demo_03:
case_new_project_demo_04: case_new_project_demo_04:
feature: 新建项目 feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(04) title: 正确输入各项必填参数,新建项目成功(04)
severity: normal
run: False run: False
url: /api/projects.json url: /api/projects.json
method: POST method: POST
@@ -117,6 +120,7 @@ case_new_project_demo_04:
case_new_project_demo_05: case_new_project_demo_05:
feature: 新建项目 feature: 新建项目
title: 正确输入各项必填参数,新建项目成功(05) title: 正确输入各项必填参数,新建项目成功(05)
severity: normal
run: False run: False
url: /api/projects.json url: /api/projects.json
method: POST method: POST
+3
View File
@@ -6,11 +6,13 @@ case_common:
case_markers: case_markers:
- gitlink - gitlink
- upload_file - upload_file
- skip: 跳过该用例
# 用例数据 # 用例数据
case_upload_demo_01: case_upload_demo_01:
feature: 上传文件 feature: 上传文件
title: 测试单文件上传 title: 测试单文件上传
severity:
run: False run: False
url: /api/attachments.json url: /api/attachments.json
method: POST method: POST
@@ -31,6 +33,7 @@ case_upload_demo_01:
case_upload_demo_02: case_upload_demo_02:
feature: 上传文件 feature: 上传文件
title: 测试多文件上传(该接口不支持多文件上传,这是一个示例) title: 测试多文件上传(该接口不支持多文件上传,这是一个示例)
severity: normal
run: False run: False
url: /api/attachments.json url: /api/attachments.json
method: POST method: POST
+25 -1
View File
@@ -5,11 +5,15 @@
# @Software: PyCharm # @Software: PyCharm
# @Desc: # @Desc:
# 标准库导入
import json import json
# 第三方库导入
import pytest import pytest
import requests import requests
from config.global_vars import GLOBAL_VARS import allure
from loguru import logger from loguru import logger
# 本地应用/模块导入
from config.global_vars import GLOBAL_VARS
from common_utils.base_request import BaseRequest from common_utils.base_request import BaseRequest
@@ -25,6 +29,26 @@ def case_skip(request):
pytest.skip(reason) pytest.skip(reason)
def pytest_collection_modifyitems(config, items):
for item in items:
# 注意这里的"case"需要与@pytest.mark.parametrize("case", cases)中传递的保持一致
parameters = item.callspec.params["case"]
# print(f"测试参数:{type(parameters)} {parameters}")
if parameters.get("severity"):
if parameters["severity"].upper() == "TRIVIAL":
item.add_marker(allure.severity(allure.severity_level.TRIVIAL))
elif parameters["severity"].upper() == "MINOR":
item.add_marker(allure.severity(allure.severity_level.MINOR))
elif parameters["severity"].upper() == "CRITICAL":
item.add_marker(allure.severity(allure.severity_level.CRITICAL))
elif parameters["severity"].upper() == "BLOCKER":
item.add_marker(allure.severity(allure.severity_level.BLOCKER))
else:
item.add_marker(allure.severity(allure.severity_level.NORMAL))
else:
item.add_marker(allure.severity(allure.severity_level.NORMAL))
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def login_init(): def login_init():
""" """
+2 -2
View File
@@ -14,8 +14,8 @@ import allure
from case_utils.allure_handle import allure_title, allure_step from case_utils.allure_handle import allure_title, allure_step
# 读取用例数据 # 读取用例数据
cases = [{"title": "demo用例01", "user": "flora", "age": 17, "run": True}, cases = [{"title": "demo用例01", "severity": "blocker1", "user": "flora", "age": 17, "run": True},
{"title": "demo用例02", "user": "lucy", "age": 17, "run": False}] {"title": "demo用例02", "severity": "TRIVIAL", "user": "flora", "age": 17, "run": True}]
@allure.story("demo模块(手动用例)") @allure.story("demo模块(手动用例)")