目录
[1. 标准1:基于框架原生扩展机制](#1. 标准1:基于框架原生扩展机制)
[2. 标准2:解决框架原生能力不足](#2. 标准2:解决框架原生能力不足)
[3. 标准3:具备可复用的扩展特性](#3. 标准3:具备可复用的扩展特性)
[1.1 插件开发的核心原理](#1.1 插件开发的核心原理)
[1.2 实操案例:自定义"用例优先级"插件](#1.2 实操案例:自定义“用例优先级”插件)
[1.3 注意事项](#1.3 注意事项)
[2.1 注意事项](#2.1 注意事项)
[3.1 注意事项](#3.1 注意事项)
[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
核心代码实现
- 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",
]
},
)
- 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)}")
- 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
使用方式
- 安装插件:
python
cd pytest_priority_plugin
pip install .
- 执行测试:
python
# 执行优先级<=2的用例(最高+中优先级)
pytest tests/test_demo.py --priority-level 2 -v
- 执行结果:
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
核心代码实现
- 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")
- 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
核心代码实现
- pytest.ini(配置文件):
python
[pytest]
testenv = dev
base_url = http://api.dev.com
timeout = 10
- 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
- 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"]
使用方式与执行结果
- 直接执行(使用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
- 命令行覆盖配置执行:
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/ # 报告输出目录
核心代码实现
- 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}")
- 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个原则:
-
不破坏框架原生特性:所有二次开发都应基于框架的扩展机制,而非修改源码或绕过原生逻辑;
-
以解决实际问题为导向:不要为了"二次开发"而开发,每一个扩展功能都要对应具体的场景需求;
-
保证扩展性和可维护性:二次开发的成果应具备复用性,代码结构清晰,便于后续迭代和团队共享。
对于pytest而言,真正的二次开发不是"封装一层又一层",而是学会利用其插件体系、钩子函数、配置系统这些原生能力,让框架更好地适配你的测试场景。从简单的钩子函数定制,到独立插件开发,循序渐进地实践,才能真正掌握二次开发的核心。