From 64ae0c0152623ec4074be25607e4366a2e96bff8 Mon Sep 17 00:00:00 2001 From: floraachy <1622042529@qq.com> Date: Tue, 4 Jul 2023 11:44:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=A9=E7=94=A8allure?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=94=A8=E4=BE=8B=E4=BC=98=E5=85=88=E7=BA=A7?= =?UTF-8?q?=EF=BC=8C=E8=BF=90=E8=A1=8C=E6=8C=87=E5=AE=9A=E4=BC=98=E5=85=88?= =?UTF-8?q?=E7=BA=A7=E7=9A=84=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++ case_utils/case_data_analysis.py | 18 ++++++++++-- case_utils/case_fun_handle.py | 2 +- case_utils/request_data_handle.py | 2 ++ config/models.py | 26 +++++++++++++++++- config/settings.py | 4 +-- conftest.py | 3 +- .../glcc/test_get_apply_information.yml | 2 ++ data/gitlink/login_demo.yaml | 3 ++ data/gitlink/test_gitlink_demo.xlsx | Bin 11772 -> 11856 bytes data/gitlink/test_new_project_demo.yaml | 6 +++- data/gitlink/test_upload_files.yaml | 3 ++ test_case/conftest.py | 26 +++++++++++++++++- test_case/test_manual_case/test_demo.py | 4 +-- 14 files changed, 91 insertions(+), 12 deletions(-) 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 f4e6ffe166c375b1cec031789e1aad45ace2f664..1ce5296e12160f6e13e7529551ac97ef418a171a 100644 GIT binary patch delta 4393 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 iUbIK