怎么才能算对自动化测试框架二次开发(以Pytest为例)

目录

一、先理清:pytest二次开发的核心定义

二、pytest二次开发的3个核心判断标准

[1. 标准1:基于框架原生扩展机制](#1. 标准1:基于框架原生扩展机制)

[2. 标准2:解决框架原生能力不足](#2. 标准2:解决框架原生能力不足)

[3. 标准3:具备可复用的扩展特性](#3. 标准3:具备可复用的扩展特性)

三、pytest二次开发的4个核心实践方向(附实操案例)

方向1:自定义pytest插件开发

[1.1 插件开发的核心原理](#1.1 插件开发的核心原理)

[1.2 实操案例:自定义"用例优先级"插件](#1.2 实操案例:自定义“用例优先级”插件)

代码目录结构

核心代码实现

使用方式

[1.3 注意事项](#1.3 注意事项)

方向2:钩子函数定制测试流程

实操案例:用例执行前后的环境准备与清理

代码目录结构

核心代码实现

执行结

[2.1 注意事项](#2.1 注意事项)

方向3:扩展pytest配置体系

实操案例:自定义环境配置项

代码目录结构

核心代码实现

使用方式与执行结果

[3.1 注意事项](#3.1 注意事项)

方向4:定制测试报告格式

实操案例:生成markdown格式测试报告

代码目录结构

核心代码实现

执行结果

[4.1 注意事项](#4.1 注意事项)

四、最后:二次开发的核心原则


在测试开发领域待久了,发现一个很有意思的现象:很多同学把「框架封装」和「二次开发」混为一谈------比如封装几个测试工具类、封装一套用例模板,就觉得完成了自动化框架的二次开发。但实际落地时会发现,这种"二次开发"既不具备扩展性,也解决不了核心的框架适配问题。

尤其是用pytest这类本身就具备高扩展性的框架时,很多二次开发反而画蛇添足。今天就结合我多年的实操经验,以pytest为例,聊聊真正的自动化测试框架二次开发该怎么定义、怎么判断,以及具体该从哪些方向落地。

一、先理清:pytest二次开发的核心定义

在聊"对不对"之前,得先明确"什么是pytest二次开发"。首先要区分两个核心概念:

  • 框架封装:侧重"使用层"的整合,比如封装请求工具、封装断言方法、整理用例目录规范等,本质是让测试人员更方便地使用框架,没有改变框架本身的核心逻辑和扩展点。

  • 二次开发:侧重"框架层"的扩展,基于框架的原生扩展机制,新增或修改框架的核心能力(比如自定义用例发现规则、新增测试报告格式、扩展配置体系等),本质是让框架适配特定的测试场景,而非单纯简化使用流程。

简单说,判断是不是二次开发,核心看是否触达了框架的"扩展内核"------对于pytest而言,就是是否基于其插件体系、钩子函数、配置系统等原生扩展机制进行开发。

二、pytest二次开发的3个核心判断标准

不是所有基于pytest的开发都叫二次开发,满足以下3个标准,才算真正意义上的二次开发:

1. 标准1:基于框架原生扩展机制

pytest的设计初衷就是"可扩展",其官方提供了完整的扩展机制,二次开发必须基于这些原生机制,而非脱离框架另起炉灶。比如通过setuptools的entry-points注册插件、通过pytest_*系列钩子函数干预测试流程、通过pytest_config扩展配置项等。

反例:直接修改pytest的源码实现自定义功能,这种方式既不具备可移植性,也会随着pytest版本升级而失效,属于错误的"二次开发"。

2. 标准2:解决框架原生能力不足

二次开发的核心目的是"补全场景适配能力",而非重复造轮子。如果pytest原生功能已经能满足需求,再进行所谓的"二次开发"就是冗余的。比如pytest原生支持xml、html格式报告,但如果需要生成符合公司内部规范的markdown报告,这种场景下的扩展才属于有价值的二次开发。

3. 标准3:具备可复用的扩展特性

真正的二次开发成果应该是"可复用"的,比如打包成独立的插件包供团队共享、钩子函数的实现可适配多种测试场景、扩展的配置项可根据不同环境动态调整。如果只是为单个项目写的一次性代码,即使用到了钩子函数,也算不上完整的二次开发。

三、pytest二次开发的4个核心实践方向(附实操案例)

结合上述标准,pytest二次开发主要集中在4个核心方向,每个方向都配套实操案例,所有代码基于Python 3.8.6实现。

方向1:自定义pytest插件开发

pytest的插件是其扩展能力的核心载体,通过插件可以新增命令行参数、自定义用例发现规则、扩展报告格式等。插件的开发和分发遵循setuptools的规范,可独立打包部署。

1.1 插件开发的核心原理

pytest启动时会自动发现并加载符合规范的插件,加载流程如下:

1.2 实操案例:自定义"用例优先级"插件

需求:pytest原生不支持用例优先级配置,需开发插件实现"按优先级执行用例"的功能,支持通过装饰器标记用例优先级,通过命令行参数指定执行的优先级。

代码目录结构
python 复制代码
pytest_priority_plugin/
├── pytest_priority_plugin/
│   ├── __init__.py
│   └── plugin.py
├── setup.py
└── tests/
    ├── conftest.py
    └── test_demo.py
核心代码实现
  1. setup.py(插件注册):
python 复制代码
from setuptools import setup

setup(
    name="pytest-priority-plugin",
    version="1.0.0",
    packages=["pytest_priority_plugin"],
    install_requires=["pytest>=6.0.0"],
    entry_points={
        "pytest11": [
            "priority-plugin = pytest_priority_plugin.plugin",
        ]
    },
)
    
  1. plugin.py(核心逻辑):
python 复制代码
import pytest

# 定义优先级装饰器
def priority(level):
    def decorator(func):
        setattr(func, "_priority", level)
        return func
    return decorator

# 注册命令行参数
def pytest_addoption(parser):
    parser.addoption(
        "--priority-level",
        type=int,
        default=1,
        help="Execute cases with priority level <= specified value"
    )

# 筛选用例
def pytest_collection_modifyitems(config, items):
    target_level = config.getoption("--priority-level")
    # 按优先级排序,优先级数字越小,优先级越高
    items.sort(key=lambda x: getattr(x.function, "_priority", 3), reverse=False)
    # 筛选出符合优先级要求的用例
    filtered_items = []
    for item in items:
        case_priority = getattr(item.function, "_priority", 3)
        if case_priority <= target_level:
            filtered_items.append(item)
    # 更新用例列表
    items[:] = filtered_items
    # 打印筛选结果
    print(f"Filtered cases with priority level <= {target_level}, total: {len(items)}")
   
  1. test_demo.py(测试用例):
python 复制代码
from pytest_priority_plugin.plugin import priority

@priority(1)  # 最高优先级
def test_high_priority():
    assert 1 == 1

@priority(2)  # 中优先级
def test_medium_priority():
    assert 2 == 2

@priority(3)  # 低优先级
def test_low_priority():
    assert 3 == 3

def test_default_priority():  # 无优先级标记,默认3
    assert 4 == 4
    
使用方式
  1. 安装插件:
python 复制代码
cd pytest_priority_plugin
pip install .
  1. 执行测试:
python 复制代码
# 执行优先级<=2的用例(最高+中优先级)
pytest tests/test_demo.py --priority-level 2 -v
    
  1. 执行结果:
python 复制代码
Filtered cases with priority level <= 2, total: 2
collected 4 items / 2 deselected / 2 selected

tests/test_demo.py::test_high_priority PASSED
tests/test_demo.py::test_medium_priority PASSED
    
1.3 注意事项
  • 插件的entry-points必须配置在"pytest11"分组下,pytest才能识别为合法插件;

  • 插件开发完成后,建议通过pip install -e .进行 editable安装,方便调试;

  • 避免在插件中过度依赖第三方库,减少部署成本。

方向2:钩子函数定制测试流程

pytest提供了大量的钩子函数(hook functions),允许开发者干预测试流程的各个阶段(如用例收集、测试执行、报告生成等)。钩子函数是二次开发的核心工具,无需打包插件,也可在项目内的conftest.py中直接使用。

实操案例:用例执行前后的环境准备与清理

需求:所有测试用例执行前初始化数据库连接,执行后关闭连接;指定标签的用例执行前额外清理测试数据。

代码目录结构
python 复制代码
pytest_hook_demo/
├── conftest.py
└── test_db.py
    
核心代码实现
  1. conftest.py(钩子函数实现):
python 复制代码
import pytest

# 定义标签装饰器
def db_cleanup(func):
    setattr(func, "_db_cleanup", True)
    return func

# 测试会话开始前初始化数据库连接
def pytest_sessionstart(session):
    print("Initialize database connection...")
    session.db_conn = {"host": "localhost", "port": 3306, "user": "test", "password": "test123"}

# 测试会话结束后关闭数据库连接
def pytest_sessionfinish(session, exitstatus):
    print("Close database connection...")
    session.db_conn = None

# 单个用例执行前处理
def pytest_before_testitem(item):
    # 检查用例是否需要清理数据
    need_cleanup = getattr(item.function, "_db_cleanup", False)
    if need_cleanup:
        db_conn = item.session.db_conn
        print(f"Clean test data before case: {item.name}, db_conn: {db_conn}")

# 单个用例执行后处理
def pytest_after_testitem(item):
    print(f"Case {item.name} executed successfully")
    
  1. test_db.py(测试用例):
python 复制代码
from conftest import db_cleanup

@db_cleanup
def test_insert_data():
    # 模拟插入数据测试
    print("Execute insert data test")
    assert True

def test_query_data():
    # 模拟查询数据测试
    print("Execute query data test")
    assert True
执行结
python 复制代码
Initialize database connection...
Clean test data before case: test_insert_data, db_conn: {'host': 'localhost', 'port': 3306, 'user': 'test', 'password': 'test123'}
Execute insert data test
Case test_insert_data executed successfully
Execute query data test
Case test_query_data executed successfully
Close database connection...
2.1 注意事项
  • 钩子函数的命名必须严格遵循pytest的规范(如pytest_sessionstart、pytest_before_testitem),否则无法被识别;

  • conftest.py的作用域是其所在的目录及子目录,多个conftest.py会按目录层级叠加生效;

  • 避免在钩子函数中编写过于复杂的逻辑,以免影响测试执行效率。

方向3:扩展pytest配置体系

pytest的配置系统支持通过pytest.ini、tox.ini、setup.cfg等配置文件自定义参数,二次开发可扩展配置项,让框架适配不同的测试环境(如开发环境、测试环境)、不同的测试场景(如接口测试、UI测试)。

实操案例:自定义环境配置项

需求:扩展pytest配置,支持在pytest.ini中配置测试环境的基础URL、超时时间,在测试用例中直接获取这些配置。

代码目录结构
python 复制代码
pytest_config_demo/
├── pytest.ini
├── conftest.py
└── test_api.py
    
核心代码实现
  1. pytest.ini(配置文件):
python 复制代码
[pytest]
testenv = dev
base_url = http://api.dev.com
timeout = 10
    
  1. conftest.py(配置解析):
复制代码
python 复制代码
import pytest

# 解析自定义配置项,封装成fixture供用例使用
@pytest.fixture(scope="session")
def test_config(request):
    # 获取pytest.ini中的配置
    testenv = request.config.getini("testenv")
    base_url = request.config.getini("base_url")
    timeout = int(request.config.getini("timeout"))
    
    # 封装配置信息
    config = {
        "testenv": testenv,
        "base_url": base_url,
        "timeout": timeout
    }
    print(f"Load test config: {config}")
    return config

# 扩展命令行参数,支持通过命令行覆盖配置
def pytest_addoption(parser):
    parser.addoption(
        "--testenv",
        default=None,
        help="Override test environment (dev/test/prod)"
    )
    parser.addoption(
        "--base-url",
        default=None,
        help="Override base url"
    )

# 重写test_config fixture,支持命令行参数覆盖
@pytest.fixture(scope="session")
def test_config(request):
    # 获取ini配置
    config = {
        "testenv": request.config.getini("testenv"),
        "base_url": request.config.getini("base_url"),
        "timeout": int(request.config.getini("timeout"))
    }
    # 获取命令行参数,覆盖ini配置
    cli_testenv = request.config.getoption("--testenv")
    cli_baseurl = request.config.getoption("--base-url")
    if cli_testenv:
        config["testenv"] = cli_testenv
    if cli_baseurl:
        config["base_url"] = cli_baseurl
    print(f"Final test config: {config}")
    return config
    
  1. test_api.py(测试用例):
复制代码
python 复制代码
def test_get_user_info(test_config):
    base_url = test_config["base_url"]
    timeout = test_config["timeout"]
    print(f"Request user info from: {base_url}/user, timeout: {timeout}")
    # 模拟接口请求
    assert base_url.startswith("http")

def test_get_product_list(test_config):
    print(f"Request product list from: {test_config['base_url']}/product")
    assert test_config["testenv"] in ["dev", "test", "prod"]
    
使用方式与执行结果
  1. 直接执行(使用ini配置):

pytest test_api.py -s

执行结果:

复制代码
python 复制代码
Final test config: {'testenv': 'dev', 'base_url': 'http://api.dev.com', 'timeout': 10}
Request user info from: http://api.dev.com/user, timeout: 10
Request product list from: http://api.dev.com/product
    
  1. 命令行覆盖配置执行:

pytest test_api.py -s --testenv test --base-url http://api.test.com

执行结果:

复制代码
python 复制代码
Final test config: {'testenv': 'test', 'base_url': 'http://api.test.com', 'timeout': 10}
Request user info from: http://api.test.com/user, timeout: 10
Request product list from: http://api.test.com/product
    
3.1 注意事项
  • 自定义配置项建议在pytest.ini中集中管理,便于维护;

  • 通过命令行参数覆盖配置时,注意参数类型的一致性(如超时时间需转为int);

  • 配置相关的fixture建议使用session作用域,避免重复解析配置。

方向4:定制测试报告格式

pytest原生支持xml、html等报告格式,但实际场景中可能需要自定义报告内容(如增加用例优先级、测试环境信息)、自定义报告格式(如markdown、Excel)。通过二次开发可扩展pytest的报告生成能力。

实操案例:生成markdown格式测试报告

需求:测试执行完成后,自动生成markdown格式的测试报告,包含测试环境、用例总数、通过率、失败用例详情等信息。

代码目录结构
复制代码
python 复制代码
pytest_report_demo/
├── conftest.py
├── test_demo.py
└── report/  # 报告输出目录
    
核心代码实现
  1. conftest.py(报告生成逻辑):
复制代码
python 复制代码
import pytest
import time
import os

# 存储测试结果
class TestResult:
    def __init__(self):
        self.total = 0
        self.passed = 0
        self.failed = 0
        self.skipped = 0
        self.failed_cases = []
        self.start_time = None
        self.end_time = None

# 初始化测试结果
@pytest.fixture(scope="session")
def test_result():
    result = TestResult()
    result.start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    yield result
    result.end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    # 生成markdown报告
    generate_markdown_report(result)

# 收集测试结果
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()
    # 获取test_result实例
    test_result = item.funcargs.get("test_result")
    if not test_result:
        return
    # 统计用例总数
    if report.when == "call":
        test_result.total += 1
        if report.outcome == "passed":
            test_result.passed += 1
        elif report.outcome == "failed":
            test_result.failed += 1
            # 记录失败用例详情
            failed_info = {
                "case_name": item.name,
                "file_path": item.fspath,
                "line_number": report.lineno,
                "error_msg": str(call.excinfo.value) if call.excinfo else ""
            }
            test_result.failed_cases.append(failed_info)
        elif report.outcome == "skipped":
            test_result.skipped += 1

# 生成markdown报告
def generate_markdown_report(result):
    # 计算通过率
    pass_rate = (result.passed / result.total) * 100 if result.total > 0 else 0
    # 构建报告内容
    report_content = f"""# 测试报告
## 测试概览
- 测试开始时间: {result.start_time}
- 测试结束时间: {result.end_time}
- 用例总数: {result.total}
- 通过用例: {result.passed}
- 失败用例: {result.failed}
- 跳过用例: {result.skipped}
- 通过率: {pass_rate:.2f}%

## 失败用例详情
"""
    if result.failed_cases:
        for idx, case in enumerate(result.failed_cases, 1):
            report_content += f"""### 用例{idx}: {case['case_name']}
- 文件路径: {case['file_path']}
- 行号: {case['line_number']}
- 错误信息: {case['error_msg']}

"""
    else:
        report_content += "无失败用例\n"
    # 保存报告
    report_dir = "report"
    if not os.path.exists(report_dir):
        os.makedirs(report_dir)
    report_path = os.path.join(report_dir, f"test_report_{time.strftime('%Y%m%d%H%M%S')}.md")
    with open(report_path, "w", encoding="utf-8") as f:
        f.write(report_content)
    print(f"Markdown report generated: {report_path}")
    
  1. test_demo.py(测试用例):
python 复制代码
import pytest

def test_pass(test_result):
    assert 1 == 1

def test_fail(test_result):
    assert 1 == 2

@pytest.mark.skip(reason="暂时跳过")
def test_skip(test_result):
    assert True
    
执行结果

执行命令:pytest test_demo.py -s

Markdown report generated: report/test_report_20260107153000.md

生成的markdown报告内容(节选):

复制代码
python 复制代码
# 测试报告
## 测试概览
- 测试开始时间: 2026-01-07 15:30:00
- 测试结束时间: 2026-01-07 15:30:01
- 用例总数: 3
- 通过用例: 1
- 失败用例: 1
- 跳过用例: 1
- 通过率: 33.33%

## 失败用例详情
### 用例1: test_fail
- 文件路径: test_demo.py
- 行号: 7
- 错误信息: 1 != 2
    
4.1 注意事项
  • 使用pytest_runtest_makereport钩子时,需通过hookwrapper=True获取报告结果,注意执行顺序;

  • 报告生成逻辑建议放在session级fixture的yield之后,确保能收集到所有测试结果;

  • 生成报告时注意编码格式(如utf-8),避免中文乱码。

四、最后:二次开发的核心原则

聊完了具体的实践方向,再回到最初的问题:怎么才能算对自动化测试框架二次开发?其实核心就3个原则:

  1. 不破坏框架原生特性:所有二次开发都应基于框架的扩展机制,而非修改源码或绕过原生逻辑;

  2. 以解决实际问题为导向:不要为了"二次开发"而开发,每一个扩展功能都要对应具体的场景需求;

  3. 保证扩展性和可维护性:二次开发的成果应具备复用性,代码结构清晰,便于后续迭代和团队共享。

对于pytest而言,真正的二次开发不是"封装一层又一层",而是学会利用其插件体系、钩子函数、配置系统这些原生能力,让框架更好地适配你的测试场景。从简单的钩子函数定制,到独立插件开发,循序渐进地实践,才能真正掌握二次开发的核心。

相关推荐
小oo呆3 分钟前
【学习心得】Python的Pydantic(简介)
前端·javascript·python
岚天start3 分钟前
【日志监控方案】Python脚本获取关键字日志信息并推送钉钉告警
python·钉钉·日志监控
叫我:松哥5 分钟前
基于 Flask 框架开发的在线学习平台,集成人工智能技术,提供分类练习、随机练习、智能推荐等多种学习模式
人工智能·后端·python·学习·信息可视化·flask·推荐算法
rgeshfgreh5 分钟前
Python环境管理:uv极速对决Conda全能
python
测试一路到黑6 分钟前
端到端测试自动化流水线:Playwright + GitHub Actions + Allure Reports 完整实践
软件测试·功能测试·测试开发·playwright·ai测试
幻云20107 分钟前
Python机器学习:从入门到精通
python
热爱专研AI的学妹14 分钟前
2026世界杯观赛工具自制指南:实时比分推送机器人搭建思路
开发语言·人工智能·python·业界资讯
热心不起来的市民小周18 分钟前
测测你的牌:基于 MobileNetV2 的车牌内容检测
python·深度学习·计算机视觉
BinaryBoss20 分钟前
Python 从Maxcompute导出海量数据到文本文件(txt)或Excel
chrome·python·odps
落羽凉笙22 分钟前
Python基础(4)| 详解程序选择结构:单分支、双分支与多分支逻辑(附代码)
android·服务器·python