diff --git a/README.md b/README.md index 5436bbf..1027d63 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ * 框架天然支持接口动态传参、关联灵活处理 * 支持测试数据分析,测试数据不符合规范有预警机制 * 支持通过用例数据动态配置pytest.mark, 包括自定义标记,pytest.mark.skip以及pytest,mark.usefixtues +* 支持利用allure设置用例优先级,运行指定优先级的用例 * 测试数据隔离, 实现数据驱动 * 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用 * 多种报告随心选择:框架支持pytest-html以及Allure测试报告,可以动态配置所需报告 @@ -272,6 +273,9 @@ excel表单2名称是:示例模块 在测试过程中,我们经常需要对测试用例进行分类,运行时仅执行这一类用例。为了实现这一功能,我在测试用例中引入了添加pytest的自定义标记的功能,同时扩展支持了pytest.mark.skip以及pytest,mark.usefixtues。 注意:目前这一功能,仅支持通过YAML格式编写用例。EXCEL用例暂时不支持。 +- [如何设置测试用例优先级](https://www.gitlink.org.cn/zone/tester/newdetail/260) +测试用例会分优先级, 我们可以利用allure的特性设置用例优先级,以及运行指定优先级的用例。 + - [如何提取响应数据作为全局变量并使用?](https://www.gitlink.org.cn/zone/tester/newdetail/237) 在测试过程中,通常下一个接口需要用到上一个接口的响应数据,这个时候就涉及到参数的提取。 diff --git a/case_utils/case_data_analysis.py b/case_utils/case_data_analysis.py index 9ee9849..19acaaf 100644 --- a/case_utils/case_data_analysis.py +++ b/case_utils/case_data_analysis.py @@ -5,8 +5,10 @@ # @Software: PyCharm # @Desc: 分析用例数据是否符合规范 +# 标准库导入 from typing import Text -from config.models import TestCase, TestCaseEnum, Method, RequestType +# 第三方库导入 +from config.models import TestCase, TestCaseEnum, Method, RequestType, Severity class CaseDataCheck: @@ -26,6 +28,15 @@ class CaseDataCheck: 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 def get_request_type(self): return self.check_params_right( @@ -55,7 +66,7 @@ class CaseDataCheck: assert attr.upper() in _member_names_, ( f"用例ID为 {self.case_id} 的用例中 {enum_name}: {attr} 填写不正确," f"当前框架中只支持 {_member_names_} 类型." - f"如需新增 method 类型,请联系管理员." + f"如有疑问,请联系管理员." ) return attr.upper() @@ -81,9 +92,10 @@ class CaseDataCheck: case_data = { 'feature': self.case_data.get(TestCaseEnum.FEATURE.value[0]), 'title': self.case_data.get(TestCaseEnum.TITLE.value[0]), + 'severity': self.get_severity, 'url': self.case_data.get(TestCaseEnum.URL.value[0]), - 'method': self.get_method, 'run': self.case_data.get(TestCaseEnum.RUN.value[0]), + 'method': self.get_method, 'headers': self.case_data.get(TestCaseEnum.HEADERS.value[0]), 'cookies': self.case_data.get(TestCaseEnum.COOKIES.value[0]), 'request_type': self.get_request_type, diff --git a/case_utils/case_fun_handle.py b/case_utils/case_fun_handle.py index 7b1f19a..6d82ce0 100644 --- a/case_utils/case_fun_handle.py +++ b/case_utils/case_fun_handle.py @@ -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_feature": case_common["allure_feature"], "allure_story": case_common["allure_story"], - "case_markers": case_common["case_markers"], + "case_markers": case_markers, "case_data": case_data, "func_title": func_name, "class_title": class_name, diff --git a/case_utils/request_data_handle.py b/case_utils/request_data_handle.py index b470f6a..b30e915 100644 --- a/case_utils/request_data_handle.py +++ b/case_utils/request_data_handle.py @@ -33,6 +33,7 @@ class RequestPreDataHandle: logger.debug(f"\n======================================================\n" \ "-------------Start:处理用例数据前--------------------\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"请求方式(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" \ @@ -61,6 +62,7 @@ class RequestPreDataHandle: logger.debug(f"\n======================================================\n" \ "-------------End:处理用例数据后--------------------\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"请求方式(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" \ diff --git a/config/models.py b/config/models.py index a73ab73..ad64d73 100644 --- a/config/models.py +++ b/config/models.py @@ -6,8 +6,10 @@ # @Software: PyCharm # @Desc: 全局变量 +# 标准库导入 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 @@ -59,10 +61,25 @@ class AllureAttachmentType(Enum): 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): + """ + 测试用例中字段 + """ FEATURE = ("feature", False) TITLE = ("title", True) URL = ("url", True) + SEVERITY = ("severity", False) METHOD = ("method", True) HEADERS = ("headers", True) COOKIES = ("cookies", False) @@ -76,8 +93,12 @@ class TestCaseEnum(Enum): class TestCase(BaseModel): + """ + 测试用例各数据格式要求 + """ feature: Union[None, Text] = None title: Text + severity: Text url: Text method: Text headers: Union[None, Dict, Text] = {} @@ -92,6 +113,9 @@ class TestCase(BaseModel): class Method(Enum): + """ + 请求方式 + """ GET = "GET" POST = "POST" PUT = "PUT" diff --git a/config/settings.py b/config/settings.py index fe50b9e..5f6ca26 100644 --- a/config/settings.py +++ b/config/settings.py @@ -8,13 +8,13 @@ # ------------------------------------ 配置信息 ----------------------------------------------------# # 0代表执行Excel和yaml两种格式的用例, 1 代表 yaml文件,2 用例代表Excel用例, 其他数值将不自动生成用例,仅能执行手动编写的用例 -CASE_FILE_TYPE = 1 +CASE_FILE_TYPE = 2 # 0表示默认不发送任何通知, 1代表钉钉通知,2代表企业微信通知, 3代表邮件通知, 4代表所有途径都发送通知 SEND_RESULT_TYPE = 0 # 指定日志收集级别 -LOG_LEVEL = "INFO" +LOG_LEVEL = None """ 支持的日志级别: None: 表示捕获所有日志 diff --git a/conftest.py b/conftest.py index 3177bbf..eb576f3 100644 --- a/conftest.py +++ b/conftest.py @@ -91,7 +91,8 @@ def pytest_terminal_summary(terminalreporter, config): "-------------测试结果--------------------\n" f"用例总数: {_TOTAL}\n" f"跳过用例数: {_SKIPPED}\n" - f"实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED}\n\n" + f"实际执行用例总数: {_PASSED + _FAILED + _XPASSED + _XFAILED}\n" + f"通过用例数: {_PASSED}\n" f"异常用例数: {_ERROR}\n" f"失败用例数: {_FAILED}\n" f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n" diff --git a/data/gitlink/glcc/test_get_apply_information.yml b/data/gitlink/glcc/test_get_apply_information.yml index 965eead..eaa998b 100644 --- a/data/gitlink/glcc/test_get_apply_information.yml +++ b/data/gitlink/glcc/test_get_apply_information.yml @@ -10,6 +10,7 @@ case_common: case_glcc_demo_01: feature: GLCC title: 获取已报名成功的项目数据 + severity: normal run: True url: ${glcc_host}/api/applyInformation/list?curPage=1&pageSize=10000&round=2 method: GET @@ -28,6 +29,7 @@ case_glcc_demo_01: case_glcc_demo_02: feature: GLCC title: 获取已报名成功的课题数据 + severity: normal run: True url: https://glcc.gitlink.org.cn/api/applyInformation/taskList?curPage=1&pageSize=20&userId=&round=2 method: GET diff --git a/data/gitlink/login_demo.yaml b/data/gitlink/login_demo.yaml index 792ab8e..1cbb735 100644 --- a/data/gitlink/login_demo.yaml +++ b/data/gitlink/login_demo.yaml @@ -13,6 +13,7 @@ case_login_demo_01: feature: 登录 title: 用户名密码正确,登录成功(不校验数据库) run: True + severity: normal url: /api/accounts/login.json method: POST headers: {"Content-Type": "application/json; charset=utf-8;"} @@ -42,6 +43,7 @@ case_login_demo_02: feature: 登录 title: 用户名密码正确,登录成功(校验数据库) run: False + severity: minor url: /api/accounts/login.json method: POST headers: {"Content-Type": "application/json; charset=utf-8;"} @@ -72,6 +74,7 @@ case_login_demo_02: case_login_demo_03: feature: 登录 title: 用户名正确,密码错误,登录失败 + severity: critical run: False url: /api/accounts/login.json method: POST diff --git a/data/gitlink/test_gitlink_demo.xlsx b/data/gitlink/test_gitlink_demo.xlsx index f4e6ffe..1ce5296 100644 Binary files a/data/gitlink/test_gitlink_demo.xlsx and b/data/gitlink/test_gitlink_demo.xlsx differ diff --git a/data/gitlink/test_new_project_demo.yaml b/data/gitlink/test_new_project_demo.yaml index d2f0e37..0a6c953 100644 --- a/data/gitlink/test_new_project_demo.yaml +++ b/data/gitlink/test_new_project_demo.yaml @@ -12,6 +12,7 @@ case_common: case_new_project_demo_01: feature: 新建项目 title: 正确输入各项必填参数,新建项目成功(不校验数据库, header里面传cookies) + severity: critical run: True url: /api/projects.json method: POST @@ -38,7 +39,7 @@ case_new_project_demo_01: case_new_project_demo_02: feature: 新建项目 title: 正确输入各项必填参数,新建项目成功(不校验数据库,单独传cookies) - run: False + run: True url: /api/projects.json method: POST headers: @@ -63,6 +64,7 @@ case_new_project_demo_02: case_new_project_demo_03: feature: 新建项目 title: 正确输入各项必填参数,新建项目成功(校验数据库) + severity: normal run: False url: /api/projects.json method: POST @@ -92,6 +94,7 @@ case_new_project_demo_03: case_new_project_demo_04: feature: 新建项目 title: 正确输入各项必填参数,新建项目成功(04) + severity: normal run: False url: /api/projects.json method: POST @@ -117,6 +120,7 @@ case_new_project_demo_04: case_new_project_demo_05: feature: 新建项目 title: 正确输入各项必填参数,新建项目成功(05) + severity: normal run: False url: /api/projects.json method: POST diff --git a/data/gitlink/test_upload_files.yaml b/data/gitlink/test_upload_files.yaml index 8a014c4..e766f87 100644 --- a/data/gitlink/test_upload_files.yaml +++ b/data/gitlink/test_upload_files.yaml @@ -6,11 +6,13 @@ case_common: case_markers: - gitlink - upload_file + - skip: 跳过该用例 # 用例数据 case_upload_demo_01: feature: 上传文件 title: 测试单文件上传 + severity: run: False url: /api/attachments.json method: POST @@ -31,6 +33,7 @@ case_upload_demo_01: case_upload_demo_02: feature: 上传文件 title: 测试多文件上传(该接口不支持多文件上传,这是一个示例) + severity: normal run: False url: /api/attachments.json method: POST diff --git a/test_case/conftest.py b/test_case/conftest.py index 09ce6b5..4197355 100644 --- a/test_case/conftest.py +++ b/test_case/conftest.py @@ -5,11 +5,15 @@ # @Software: PyCharm # @Desc: +# 标准库导入 import json +# 第三方库导入 import pytest import requests -from config.global_vars import GLOBAL_VARS +import allure from loguru import logger +# 本地应用/模块导入 +from config.global_vars import GLOBAL_VARS from common_utils.base_request import BaseRequest @@ -25,6 +29,26 @@ def case_skip(request): 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") def login_init(): """ diff --git a/test_case/test_manual_case/test_demo.py b/test_case/test_manual_case/test_demo.py index b884f12..6ba1f37 100644 --- a/test_case/test_manual_case/test_demo.py +++ b/test_case/test_manual_case/test_demo.py @@ -14,8 +14,8 @@ import allure from case_utils.allure_handle import allure_title, allure_step # 读取用例数据 -cases = [{"title": "demo用例01", "user": "flora", "age": 17, "run": True}, - {"title": "demo用例02", "user": "lucy", "age": 17, "run": False}] +cases = [{"title": "demo用例01", "severity": "blocker1", "user": "flora", "age": 17, "run": True}, + {"title": "demo用例02", "severity": "TRIVIAL", "user": "flora", "age": 17, "run": True}] @allure.story("demo模块(手动用例)")