Pytest基础模式与实用技巧详解
2025-07-06 03:47:09作者:邵娇湘
如何自定义命令行选项默认值
在日常测试工作中,反复输入相同的命令行选项会显得非常繁琐。pytest提供了两种方式来简化这一过程:
通过pytest.ini配置文件
在项目根目录下创建pytest.ini文件,使用addopts
选项可以设置默认命令行参数:
[pytest]
addopts = -ra -q
-ra
:显示跳过和预期失败测试的详细信息-q
:使用简洁的"点"进度输出
通过环境变量设置
也可以使用PYTEST_ADDOPTS
环境变量临时添加命令行选项:
export PYTEST_ADDOPTS="-v"
选项优先级规则:
- 首先加载pytest.ini中的addopts
- 然后加载环境变量PYTEST_ADDOPTS
- 最后处理命令行显式指定的选项
当选项冲突时,后出现的选项会覆盖前面的选项。
根据命令行选项传递不同测试值
有时我们需要根据不同的命令行选项执行不同的测试逻辑。下面是一个典型实现模式:
- 首先定义测试函数,接收fixture参数:
def test_answer(cmdopt):
if cmdopt == "type1":
print("first")
elif cmdopt == "type2":
print("second")
- 在conftest.py中添加命令行选项和fixture:
def pytest_addoption(parser):
parser.addoption(
"--cmdopt",
action="store",
default="type1",
help="选项类型: type1或type2"
)
@pytest.fixture
def cmdopt(request):
return request.config.getoption("--cmdopt")
- 可以添加输入验证:
parser.addoption(
"--cmdopt",
action="store",
default="type1",
choices=("type1", "type2"), # 限制可选值
help="选项类型: type1或type2"
)
或者使用自定义类型检查器:
def type_checker(value):
if not value.startswith("type"):
raise pytest.UsageError("必须以type开头")
try:
int(value[4:])
except ValueError:
raise pytest.UsageError("type后必须是数字")
return value
动态添加命令行选项
除了静态配置,还可以在运行时动态修改命令行参数:
def pytest_load_initial_conftests(args):
if "xdist" in sys.modules: # 检测是否安装了xdist插件
import multiprocessing
num = max(multiprocessing.cpu_count() / 2, 1)
args[:] = ["-n", str(num)] + args # 动态添加-n参数
控制测试跳过逻辑
通过自定义标记和选项可以灵活控制测试执行:
- 在conftest.py中添加选项和标记:
def pytest_addoption(parser):
parser.addoption(
"--runslow",
action="store_true",
default=False,
help="运行慢速测试"
)
def pytest_configure(config):
config.addinivalue_line("markers", "slow: 标记为慢速测试")
def pytest_collection_modifyitems(config, items):
if not config.getoption("--runslow"):
skip_slow = pytest.mark.skip(reason="需要--runslow选项来运行")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
- 标记慢速测试:
@pytest.mark.slow
def test_func_slow():
pass
编写集成的断言辅助函数
当测试辅助函数中出现问题时,可以通过pytest.fail
直接使测试失败,并使用__tracebackhide__
隐藏辅助函数的调用栈:
def checkconfig(x):
__tracebackhide__ = True # 隐藏调用栈
if not hasattr(x, "config"):
pytest.fail(f"配置错误: {x}")
也可以针对特定异常类型隐藏调用栈:
__tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
检测是否在pytest环境中运行
虽然不推荐,但有时需要区分代码是否在测试环境中运行:
if os.environ.get("PYTEST_VERSION") is not None:
# 测试环境特有逻辑
else:
# 非测试环境逻辑
添加测试报告头信息
可以在测试报告头部添加自定义信息:
def pytest_report_header(config):
return "项目依赖: mylib-1.1"
根据详细级别显示不同信息:
def pytest_report_header(config):
if config.get_verbosity() > 0:
return ["提示信息1", "提示信息2"]
测试耗时分析
对于大型测试套件,可以分析测试耗时:
pytest --durations=3 # 显示最慢的3个测试
输出示例:
=========================== slowest 3 durations ============================
0.30s call test_some_are_slow.py::test_funcslow2
0.20s call test_some_are_slow.py::test_funcslow1
0.10s call test_some_are_slow.py::test_funcfast
增量测试模式
对于步骤式测试,当早期步骤失败时,可以跳过后续步骤:
- 在conftest.py中实现增量测试逻辑:
_test_failed_incremental = {}
def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords and call.excinfo is not None:
cls_name = str(item.cls)
parametrize_index = tuple(item.callspec.indices.values()) if hasattr(item, "callspec") else ()
test_name = item.originalname or item.name
_test_failed_incremental.setdefault(cls_name, {}).setdefault(parametrize_index, test_name)
def pytest_runtest_setup(item):
if "incremental" in item.keywords:
cls_name = str(item.cls)
if cls_name in _test_failed_incremental:
parametrize_index = tuple(item.callspec.indices.values()) if hasattr(item, "callspec") else ()
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
if test_name is not None:
pytest.xfail(f"前置测试失败 ({test_name})")
- 标记增量测试类:
@pytest.mark.incremental
class TestUserFlow:
def test_login(self):
...
def test_modify_profile(self):
...
def test_logout(self):
...
这些技巧可以帮助你更高效地使用pytest框架,构建更灵活、更强大的测试套件。