diff --git a/README.md b/README.md index 7f6be13..5aa6aa0 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/case_utils/case_fun_handle.py b/case_utils/case_fun_handle.py index 5337489..7b1f19a 100644 --- a/case_utils/case_fun_handle.py +++ b/case_utils/case_fun_handle.py @@ -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 diff --git a/config/global_vars.py b/config/global_vars.py index 1dd6069..507e827 100644 --- a/config/global_vars.py +++ b/config/global_vars.py @@ -4,9 +4,13 @@ # @File : global_vars.py # @Software: PyCharm # @Desc: + # 定义一个全局变量,用于存储运行过程中相关数据 GLOBAL_VARS = {} +# 定义一个变量。存储自定义的标记markers +CUSTOM_MARKERS = [] + ENV_VARS = { "common": { "report_title": "自动化测试报告", diff --git a/config/settings.py b/config/settings.py index 4251c93..fe50b9e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 diff --git a/conftest.py b/conftest.py index fc5943c..3177bbf 100644 --- a/conftest.py +++ b/conftest.py @@ -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" diff --git a/data/gitlink/glcc/test_get_apply_information.yml b/data/gitlink/glcc/test_get_apply_information.yml index b6a80c6..965eead 100644 --- a/data/gitlink/glcc/test_get_apply_information.yml +++ b/data/gitlink/glcc/test_get_apply_information.yml @@ -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 diff --git a/data/gitlink/login_demo.yaml b/data/gitlink/login_demo.yaml index 31de927..792ab8e 100644 --- a/data/gitlink/login_demo.yaml +++ b/data/gitlink/login_demo.yaml @@ -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;"} diff --git a/data/gitlink/test_new_project_demo.yaml b/data/gitlink/test_new_project_demo.yaml index 797adb1..d2f0e37 100644 --- a/data/gitlink/test_new_project_demo.yaml +++ b/data/gitlink/test_new_project_demo.yaml @@ -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: diff --git a/data/gitlink/test_upload_files.yaml b/data/gitlink/test_upload_files.yaml index 12d3727..8a014c4 100644 --- a/data/gitlink/test_upload_files.yaml +++ b/data/gitlink/test_upload_files.yaml @@ -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: diff --git a/pytest.ini b/pytest.ini index d355842..c786e71 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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 \ No newline at end of file diff --git a/run.py b/run.py index ea36ee9..b7f7c8a 100644 --- a/run.py +++ b/run.py @@ -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) diff --git a/test_case/conftest.py b/test_case/conftest.py index 043930b..09ce6b5 100644 --- a/test_case/conftest.py +++ b/test_case/conftest.py @@ -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 diff --git a/test_case/test_manual_case/test_login_demo.py b/test_case/test_manual_case/test_login_demo.py index 10334f4..c579645 100644 --- a/test_case/test_manual_case/test_login_demo.py +++ b/test_case/test_manual_case/test_login_demo.py @@ -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")