首页
/ Pytest基础模式与实用技巧详解

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"

选项优先级规则:

  1. 首先加载pytest.ini中的addopts
  2. 然后加载环境变量PYTEST_ADDOPTS
  3. 最后处理命令行显式指定的选项

当选项冲突时,后出现的选项会覆盖前面的选项。

根据命令行选项传递不同测试值

有时我们需要根据不同的命令行选项执行不同的测试逻辑。下面是一个典型实现模式:

  1. 首先定义测试函数,接收fixture参数:
def test_answer(cmdopt):
    if cmdopt == "type1":
        print("first")
    elif cmdopt == "type2":
        print("second")
  1. 在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")
  1. 可以添加输入验证:
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参数

控制测试跳过逻辑

通过自定义标记和选项可以灵活控制测试执行:

  1. 在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)
  1. 标记慢速测试:
@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

增量测试模式

对于步骤式测试,当早期步骤失败时,可以跳过后续步骤:

  1. 在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})")
  1. 标记增量测试类:
@pytest.mark.incremental
class TestUserFlow:
    def test_login(self):
        ...
    
    def test_modify_profile(self):
        ...
    
    def test_logout(self):
        ...

这些技巧可以帮助你更高效地使用pytest框架,构建更灵活、更强大的测试套件。