diff --git a/README.md b/README.md index 373e483..8133ae4 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,12 @@ ## 二、实现功能 * 通过session会话方式,解决了登录之后cookie关联处理 +* 数据库断言: 直接在测试用例中写入查询的sql即可断言,无需编写代码 +* 动态多断言: 如接口需要同时校验响应数据和sql校验,支持多场景断言 * 框架天然支持接口动态传参、关联灵活处理 * 支持测试数据分析,测试数据不符合规范有预警机制 * 测试数据隔离, 实现数据驱动 * 自动生成用例代码: 测试人员在yaml/excel文件中填写好测试用例, 程序可以直接生成用例代码,纯小白也能使用 -* 动态多断言: 支持响应断言和数据库断言 * 多种报告随心选择:框架支持pytest-html以及Allure测试报告,可以动态配置所需报告 * 日志模块: 采用loguru管理日志,可以输出更为优雅,简洁的日志 * 钉钉、企业微信通知: 支持多种通知场景,执行成功之后,可选择发送钉钉、或者企业微信、邮箱通知 @@ -46,6 +47,7 @@ │ ├────case_data_analysis 分析用例数据是否符合规范 │ ├────case_fun_handle.py 根据配置文件,从指定类型文件中读取用例数据,并调用生成用例文件方法,生成用例文件 │ ├────data_handle.py 数据处理 +│ ├────extract_data_handle.py 提取数据的一些方法 │ ├────get_results_handle.py 从pytest-html/allure测试报告中获取测试结果 │ ├────platform_handle.py 跨平台的支持allure,用于生成allure测试报告 │ ├────request_data_handle.py 针对用例数据进行请求前后的处理 @@ -177,24 +179,24 @@ pip install pipenv ### 5. 用例中相关字段的介绍 ```yaml -- case_common :公共参数 - - allure_epic:用作于@allure.epic()装饰器中的内容。如果是使用Excel管理用例数据,这个目前是写死的。可在case_utils.case_handle.py中更改。 - - allure_feature:用作于@allure.feature()装饰器中的内容。如果是使用Excel管理用例数据, 这个用的是excel的表单名称。 - - allure_story:用作于@allure.story()装饰器中的内容。如果是使用Excel管理用例数据, 这个用的是excel的表单名称。 -- case_info:用例数据 - - feature:用例所属模块, 类似于@allure.feature()。 - - title:用例标题 - - run:是否执行用例,为空或者True都会执行,为False则不执行。 - - url:请求路径(可填写全路径 或者 资源路径)。通常我们填写资源路径。在用例执行前,会针对路径进行处理。具体可见case_utils.request_data_handle.py.RequestPreDataHandle.url_handle。注:请求路径=基准路径(host/base_url)+资源路径(url)。 - - method:请求方式,例如:GET, POST, DELETE, PUT, PATCH等 - - headers:请求头,注意如果在headers里面防止cookies,其值类型需要是字符串 - - cookies:请求cookies,格式是:DICT, CookieJar对象 - - request_type:请求数据类型:params, json, file, data - - payload:请求参数 - - files:上传附件接口所需的文件绝对路径 - - extract:后置提取参数 - - assert_response:响应断言 - - assert_sql:数据库断言 +case_common :公共参数 + allure_epic:用作于@allure.epic()装饰器中的内容。 + allure_feature:用作于@allure.feature()装饰器中的内容。 + allure_story:用作于@allure.story()装饰器中的内容。 +case_001:用例ID + feature:用例所属模块, 类似于@allure.feature()。 + title:用例标题 + run:是否执行用例,为空或者True都会执行,为False则不执行。 + url:请求路径(可填写全路径 或者 资源路径)。通常我们填写资源路径。在用例执行前,会针对路径进行处理。具体可见case_utils.request_data_handle.py.RequestPreDataHandle.url_handle。注:请求路径=基准路径(host/base_url)+资源路径(url)。 + method:请求方式,例如:GET, POST, DELETE, PUT, PATCH等 + headers:请求头,注意如果在headers里面防止cookies,其值类型需要是字符串 + cookies:请求cookies,格式是:DICT, CookieJar对象 + request_type:请求数据类型:params, json, file, data + payload:请求参数 + files: 需要上传的文件,参考如下传参:{接口中文件参数的名称:”文件路径地址”/[“文件地址1”, “文件地址2”]} + extract:后置提取参数 + assert_response:响应断言 + assert_sql:数据库断言 ``` ### 6. Excel用例单独说明 框架支持excel多表单自动生成测试用例,每一个表单作为一个测试用例模块。 @@ -269,6 +271,8 @@ excel表单2名称是:示例模块 - [如何处理同一套框架测试多套环境的情况?](https://www.gitlink.org.cn/zone/tester/newdetail/215) +- [如何处理用例中需要依赖登录的token/cookies的情况?](https://www.gitlink.org.cn/zone/tester/newdetail/228) + ## 十、初始化项目可能遇到的问题 diff --git a/case_utils/assert_handle.py b/case_utils/assert_handle.py index 262915b..28a5db3 100644 --- a/case_utils/assert_handle.py +++ b/case_utils/assert_handle.py @@ -6,9 +6,11 @@ # @Software: PyCharm # @Desc: 断言 +# 第三方库导入 from loguru import logger from requests import Response -from case_utils.data_handle import json_extractor, re_extract +# 本地应用/模块导入 +from case_utils.extract_data_handle import json_extractor, re_extract from case_utils.request_data_handle import response_type from case_utils.allure_handle import allure_step from common_utils.mysql_handle import MysqlServer @@ -53,19 +55,19 @@ def assert_response(response: Response, expected: dict) -> None: try: if k == "eq": # 预期结果 = 实际结果 assert _v == actual - logger.debug(f"预期结果: {_k}: {_v} == 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_k}: {_v} == 实际结果: {actual}, 断言成功!") elif k == "in": # 实际结果 包含 预期结果 assert _v in actual - logger.debug(f"预期结果: {_k}: {_v} in 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_k}: {_v} in 实际结果: {actual}, 断言成功!") elif k == "gt": # 预期结果 > 实际结果 (值应该为数值型) assert _v > actual - logger.debug(f"预期结果: {_k}: {_v} > 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_k}: {_v} > 实际结果: {actual}, 断言成功!") elif k == "lt": # 预期结果 < 实际结果 (值应该为数值型) assert _v < actual - logger.debug(f"预期结果: {_k}: {_v} < 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_k}: {_v} < 实际结果: {actual}, 断言成功!") elif k == "not": # 预期结果 != 实际结果 assert _v != actual - logger.debug(f"预期结果: {_k}: {_v} != 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_k}: {_v} != 实际结果: {actual}, 断言成功!") else: logger.error(f"判断关键字: {k} 错误!, 目前仅支持如下关键字:eq, in, gt, lt, not") allure_step(step_title=f'判断关键字: {k} 错误!', @@ -129,13 +131,13 @@ def assert_sql(db_info, expected: dict): if k == "eq": # 预期结果 = 实际结果 if _k == "len": assert _v == len(sql_result) - logger.info(f"预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!") + logger.success(f"预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!") allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {len(sql_result)}, 断言成功!') # 如果时$.开头,则从数据库查询结果中提取相应的值作为实际结果 elif _k.startswith("$."): actual = json_extractor(sql_result, _k) assert _v == actual - logger.info(f"预期结果: {_v} == 实际结果: {actual}, 断言成功!") + logger.success(f"预期结果: {_v} == 实际结果: {actual}, 断言成功!") allure_step(step_title=f'数据库断言结果---->预期结果: {_v} == 实际结果: {actual}, 断言成功!') except AssertionError: logger.error(f"数据库断言失败 -|- 预期结果:{_v} {k} 实际结果: {sql_result})") diff --git a/case_utils/request_data_handle.py b/case_utils/request_data_handle.py index 72d84df..b470f6a 100644 --- a/case_utils/request_data_handle.py +++ b/case_utils/request_data_handle.py @@ -5,14 +5,18 @@ # @File : request_data_handle.py # @Software: PyCharm # @Desc: 处理request请求前后的用例数据 + +# 标准库导入 import json import os +# 第三方库导入 from requests import Response from loguru import logger +# 本地应用/模块导入 from common_utils.files_handle import get_file_field from common_utils.base_request import BaseRequest from case_utils.data_handle import eval_data_process, data_handle -from case_utils.data_handle import json_extractor, re_extract +from case_utils.extract_data_handle import json_extractor, re_extract from case_utils.allure_handle import allure_step from config.global_vars import GLOBAL_VARS from config.path_config import FILES_DIR diff --git a/config/case_template.txt b/config/case_template.txt index e84df1f..9e6a579 100644 --- a/config/case_template.txt +++ b/config/case_template.txt @@ -23,7 +23,7 @@ class ${class_title}Auto: @pytest.mark.auto @pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases]) def ${func_title}_auto(self, case, extra): - logger.info("-----------------------------START-开始执行用例-----------------------------") + logger.info("\n-----------------------------START-开始执行用例-----------------------------\n") logger.debug(f"当前执行的用例数据:{case}") # 添加用例标题作为allure中显示的用例标题 allure_title(case.get("title", "")) @@ -41,5 +41,5 @@ class ${class_title}Auto: assert_sql(db_info[GLOBAL_VARS["env_key"]], case_data["assert_sql"]) # 断言成功后进行参数提取 after_request_extract(response, case_data.get("extract", None)) - logger.info("-----------------------------END-用例执行结束-----------------------------") + logger.info("\n-----------------------------END-用例执行结束-----------------------------\n") diff --git a/config/settings.py b/config/settings.py index 21d0add..913e32e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -15,7 +15,17 @@ SEND_RESULT_TYPE = 0 # 指定日志收集级别 LOG_LEVEL = "INFO" - +""" +支持的日志级别: + None: 表示捕获所有日志 + TRACE: 最低级别的日志级别,用于详细追踪程序的执行。 + DEBUG: 用于调试和开发过程中打印详细的调试信息。 + INFO: 提供程序执行过程中的关键信息。 + SUCCESS: 用于标记成功或重要的里程碑事件。 + WARNING: 表示潜在的问题或不符合预期的情况,但不会导致程序失败。 + ERROR: 表示错误和异常情况,但程序仍然可以继续运行。 + CRITICAL: 表示严重的错误和异常情况,可能导致程序崩溃或无法正常运行。 +""" # ------------------------------------ 数据库相关配置 ----------------------------------------------------# db_info = { "test": { diff --git a/conftest.py b/conftest.py index 04078b5..fc5943c 100644 --- a/conftest.py +++ b/conftest.py @@ -6,13 +6,16 @@ # @Software: PyCharm # @Desc: 这是文件的描述信息 +# 标准库导入 +import re import time from time import strftime -import re -import pytest -from py._xmlgen import html # 安装pytest-html,版本最好是2.1.1 -from config.global_vars import ENV_VARS, GLOBAL_VARS +# 第三方库导入 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 # ------------------------------------- START: pytest钩子函数处理---------------------------------------# @@ -76,24 +79,24 @@ def pytest_terminal_summary(terminalreporter, config): _TOTAL = terminalreporter._numcollected _TIMES = time.time() - terminalreporter._sessionstarttime - logger.info(f"\n======================================================\n" - "-------------测试结果--------------------\n" - f"用例总数: {_TOTAL}\n" - f"跳过用例数: {_SKIPPED}\n" - f"实际执行用例总数: {_TOTAL - _SKIPPED}\n\n" - f"异常用例数: {_ERROR}\n" - f"失败用例数: {_FAILED}\n" - f"重跑的用例数 || 重跑次数: {_RERUN} || {reruns_value}\n" - f"意外通过的用例数: {_XPASSED}\n" - f"预期失败的用例数: {_XFAILED}\n\n" - "用例执行时长: %.2f" % _TIMES + " s\n") + logger.success(f"\n======================================================\n" + "-------------测试结果--------------------\n" + f"用例总数: {_TOTAL}\n" + f"跳过用例数: {_SKIPPED}\n" + f"实际执行用例总数: {_TOTAL - _SKIPPED}\n\n" + f"异常用例数: {_ERROR}\n" + f"失败用例数: {_FAILED}\n" + f"重跑的用例数(--reruns的值): {_RERUN}({reruns_value})\n" + f"意外通过的用例数: {_XPASSED}\n" + f"预期失败的用例数: {_XFAILED}\n\n" + "用例执行时长: %.2f" % _TIMES + " s\n") try: _RATE = _PASSED / (_TOTAL - _SKIPPED) * 100 - logger.info( + logger.success( f"\n用例成功率: %.2f" % _RATE + " %\n" "=====================================================") except ZeroDivisionError: - logger.info( + logger.critical( f"用例成功率: 0.00 %\n" "=====================================================") diff --git a/run.py b/run.py index 8e2bc3c..ea36ee9 100644 --- a/run.py +++ b/run.py @@ -31,12 +31,15 @@ pytest相关参数:以下也可通过pytest.ini配置 –-alluredir这个选项用于指定存储测试结果的路径 """ +# 标准库导入 import os import shutil +from datetime import datetime +# 第三方库导入 import pytest from loguru import logger import click -from datetime import datetime +# 本地应用/模块导入 from case_utils.case_fun_handle import generate_cases from case_utils.platform_handle import PlatformHandle from case_utils.send_result_handle import send_result @@ -71,7 +74,7 @@ def capture_all_logs(level=LOG_LEVEL): else: # 捕获所有日志 logger.add( - os.path.join(LOG_DIR, "runtime_{time}_{level}.log"), + os.path.join(LOG_DIR, "runtime_{time}_all.log"), enqueue=True, encoding="utf-8", rotation="00:00", diff --git a/test_case/test_manual_case/test_demo.py b/test_case/test_manual_case/test_demo.py index 6d405c8..b884f12 100644 --- a/test_case/test_manual_case/test_demo.py +++ b/test_case/test_manual_case/test_demo.py @@ -22,7 +22,7 @@ cases = [{"title": "demo用例01", "user": "flora", "age": 17, "run": True}, @pytest.mark.test_demo @pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases]) def test_demo(case, extra): - logger.info("-----------------------------START-开始执行用例-----------------------------") + logger.info("\n-----------------------------START-开始执行用例-----------------------------\n") logger.debug(f"当前执行的用例数据:{case}") # 添加用例标题作为allure中显示的用例标题 allure_title(case.get("title", "")) @@ -31,4 +31,4 @@ def test_demo(case, extra): # 在allure报告中显示请求的用例数据 allure_step(step_title="用例数据", content=f"{case}") assert case["user"] == "flora" - logger.info("-----------------------------END-用例执行结束-----------------------------") + logger.info("\n-----------------------------END-用例执行结束-----------------------------\n") diff --git a/test_case/test_manual_case/test_login_demo.py b/test_case/test_manual_case/test_login_demo.py index 14fd199..10334f4 100644 --- a/test_case/test_manual_case/test_login_demo.py +++ b/test_case/test_manual_case/test_login_demo.py @@ -38,7 +38,7 @@ class TestLoginDemo: @pytest.mark.auto @pytest.mark.parametrize("case", cases, ids=["{}".format(case["title"]) for case in cases]) def test_login_demo_auto(self, case, extra): - logger.info("-----------------------------START-开始执行用例-----------------------------") + logger.info("\n-----------------------------START-开始执行用例-----------------------------\n") logger.debug(f"当前执行的用例数据:{case}") # 添加用例标题作为allure中显示的用例标题 allure_title(case.get("title", "")) @@ -56,4 +56,4 @@ class TestLoginDemo: assert_sql(db_info[GLOBAL_VARS["env_key"]], case_data["assert_sql"]) # 断言成功后进行参数提取 after_request_extract(response, case_data.get("extract", None)) - logger.info("-----------------------------END-用例执行结束-----------------------------") + logger.info("\n-----------------------------END-用例执行结束-----------------------------\n")