焦小花同学

Python测试实战:30个pytest详解高级方法(详细版)

Python测试实战:30个pytest详解高级方法

嗨,各位摸鱼(啊不,高效)的 Python 开发者们! 作为一名资深 Python 玩家,我深知测试对于项目质量的重要性。 别再让 “代码能跑就行” 的思想害了你!今天,我就来分享 30 个 pytest 的高级用法,带你彻底玩转 Python 测试,让你的代码更加健壮,bug 无处遁形!

开篇:告别低效测试,拥抱pytest

有没有觉得单元测试枯燥乏味? 每次都要写一堆 boilerplate 代码? 别慌,pytest 就是你的救星!它简洁、强大、易于扩展,让你的测试代码像写诗一样优雅。今天,我们就深入探索 pytest 的高级玩法,让你的测试效率直接起飞!

1. 基础篇:Fixture 的魔力

痛点: 测试用例常常需要重复的 setup 和 teardown 代码。

方案: 使用 Fixture 可以优雅地解决这个问题。

import pytest

@pytest.fixture
defsample_data():
"""模拟测试数据"""
    print("\nSetup: preparing sample data...")
    data = {"a": 1, "b": 2}
yield data # 使用 yield 可以让 fixture 在测试用例执行后进行 teardown
    print("\nTeardown: cleanup data...")


deftest_sample(sample_data):
"""测试用例,使用 sample_data fixture"""
assert sample_data["a"] == 1
assert sample_data["b"] == 2


# 执行 pytest 后,你会看到 Setup 和 Teardown 信息
# 输出
# Setup: preparing sample data...
# .
# Teardown: cleanup data...

解释:@pytest.fixture 装饰器将函数标记为 fixture;yield 关键字分割 setup 和 teardown 代码。类比: fixture 就像一个工具箱,每个测试用例都可以从里面拿工具用,用完再放回去。

小任务:  创建一个 fixture,模拟数据库连接,并在测试用例结束后关闭连接。

2.  Fixture 的作用域 (Scope)

痛点: 有些 fixture 只需要在 module 或 session 级别运行一次。

方案:  使用 scope 参数控制 fixture 的作用域。

import pytest

@pytest.fixture(scope="module")
defmodule_setup():
"""在 module 级别执行的 setup fixture"""
    print("\nModule Setup: Before all tests in this module...")
yield
    print("\nModule Teardown: After all tests in this module...")

deftest_one(module_setup):
"""第一个测试用例"""
  print("Test One")

deftest_two(module_setup):
"""第二个测试用例"""
  print("Test Two")

# 执行 pytest 后,你会看到 Module Setup 和 Teardown 只运行一次
# 输出
# Module Setup: Before all tests in this module...
# Test One
# .
# Test Two
# .
# Module Teardown: After all tests in this module...

解释:scope="module" 表示 fixture 只在 module 级别运行一次; 可选值还有  "function", "class", "session"。类比: 想象你有一个公共休息室, session 是整个公司, module 是部门, class 是小组,function 是个人,不同级别的 Fixture 在对应的范围内共享。

小任务: 创建一个 session 级别的 fixture,用于模拟全局配置。

3.  Fixture 的参数化 (Parametrization)

痛点:  测试需要使用不同的输入数据进行多次测试。

方案:  使用 params 参数实现 fixture 的参数化。

import pytest

@pytest.fixture(params=[1, 2, 3])
defparameterized_data(request):
"""参数化 fixture, 根据参数返回不同的数据"""
    print(f"\nParameterized setup: with parameter {request.param}")
yield request.param
    print(f"\nParameterized teardown: with parameter {request.param}")


deftest_parameterized(parameterized_data):
"""测试用例,使用参数化 fixture"""
assert parameterized_data > 0

# 执行 pytest 后,测试用例会使用不同的参数运行三次
# 输出
# Parameterized setup: with parameter 1
# .
# Parameterized teardown: with parameter 1
# Parameterized setup: with parameter 2
# .
# Parameterized teardown: with parameter 2
# Parameterized setup: with parameter 3
# .
# Parameterized teardown: with parameter 3

解释:params 指定参数列表,request.param 获取当前参数。类比:  参数化 fixture 就像一个可以变身的工具,根据不同的参数,可以变成不同的工具。

小任务: 使用参数化 fixture,测试一个函数在不同输入下的表现。

4.  使用 pytest.mark 标记测试用例

痛点:  需要对测试用例进行分类,方便执行或跳过。

方案:  使用 pytest.mark 装饰器标记测试用例。

import pytest

@pytest.mark.slow
deftest_slow_operation():
"""标记为慢速测试用例"""
    print("This is a slow test")
import time
    time.sleep(1)
assertTrue

@pytest.mark.fast
deftest_fast_operation():
"""标记为快速测试用例"""
  print("This is a fast test")
assertTrue

# 可以通过 pytest -m "slow"  只运行标记为 slow 的测试用例
# 可以通过 pytest -m "not slow" 运行不是 slow 的测试用例
# 输出:
# This is a slow test
# .
# This is a fast test
# .

解释:@pytest.mark.xxx 标记测试用例,可以使用 -m 参数选择执行。类比:  就像给测试用例贴标签,方便快速筛选。

小任务: 创建一个自定义标记,用于标记测试用例的优先级。

5.  使用 pytest.skip 和 pytest.skipif 跳过测试

痛点:  某些测试用例暂时不需要执行或不满足运行条件。

方案:  使用 pytest.skip 或 pytest.skipif 跳过测试。

import pytest

deftest_will_skip():
"""跳过测试用例"""
    pytest.skip("This test is skipped for now")

@pytest.mark.skipif(True, reason="This test is skipped based on condition")
deftest_conditional_skip():
"""有条件的跳过测试用例"""
  print("This test will not run!")
assertFalse

deftest_will_run():
"""正常运行的测试用例"""
  print("This test will run!")
assertTrue


# 输出
# s
# .
# This test will run!
# .

解释:pytest.skip 直接跳过, pytest.skipif 根据条件跳过。类比:  就像给测试用例设置了一个 “免战牌”,可以暂时不执行。

小任务:  根据操作系统,有条件地跳过某些测试用例。

6. 使用 pytest.xfail 标记预期失败的测试

痛点: 某些测试用例已知会失败,但需要保留测试。

方案: 使用 pytest.xfail 标记预期失败的测试。

import pytest

@pytest.mark.xfail(reason="This test is expected to fail")
deftest_expected_fail():
"""标记为预期失败的测试"""
    print("This test is expected to fail")
assertFalse

deftest_normal_test():
"""正常测试用例"""
  print("This is a normal test")
assertTrue
# 输出
# x
# This is a normal test
# .

解释:@pytest.mark.xfail  标记预期失败的测试,如果真的失败,不会报错,如果通过了反而会报错。类比:  就像给测试用例一个“免死金牌”,即使失败也不会影响整体测试结果。

小任务: 创建一个已知会失败,并且最终会修复通过的测试用例。

7.  使用 pytest.raises 测试异常

痛点:  需要测试函数是否抛出正确的异常。

方案:  使用 pytest.raises 上下文管理器测试异常。

import pytest

defdivide(a, b):
"""除法函数,当除数为0时抛出异常"""
if b == 0:
raise ZeroDivisionError("Division by zero")
return a / b

deftest_zero_division():
"""测试除数为0时是否抛出异常"""
with pytest.raises(ZeroDivisionError) as excinfo:
        divide(10, 0)
assert"Division by zero"in str(excinfo.value) # 使用 excinfo.value 可以获取异常信息

deftest_normal_division():
"""测试正常除法"""
assert divide(10,2) == 5

# 输出
# .
# .

解释:pytest.raises  捕获异常,并可以检查异常类型和信息。类比:  就像一个陷阱,用来捕捉预期的错误。

小任务:  测试一个函数是否抛出自定义异常。

8.  使用 capsys 和 caplog 捕获输出和日志

痛点:  需要测试函数的输出和日志信息。

方案:  使用 capsys 和 caplog fixture 捕获输出和日志。

import logging
import pytest

defprint_hello():
"""打印 hello world"""
    print("Hello, world!")

deflog_info():
"""记录日志"""
    logging.info("This is an info message")


deftest_capture_output(capsys):
"""测试捕获标准输出"""
    print_hello()
    captured = capsys.readouterr()
assert"Hello, world!\n" == captured.out

deftest_capture_log(caplog):
"""测试捕获日志信息"""
  log_info()
assert"This is an info message"in caplog.text

# 输出
# .
# .

解释:capsys 捕获标准输出和错误,caplog 捕获日志信息。类比: 就像一个录音机,录下函数的所有输出和日志信息。

小任务:  测试一个函数是否输出了正确的错误信息到标准错误输出。

9.  使用 tmp_path 和 tmpdir 创建临时文件

痛点:  需要创建临时文件和目录进行测试。

方案: 使用 tmp_path 和 tmpdir fixture 创建临时文件和目录。

import os
import pytest

deftest_create_temp_file(tmp_path):
"""测试创建临时文件"""
    file_path = tmp_path / "test_file.txt"
with open(file_path, "w") as f:
        f.write("This is a test file")
assert os.path.exists(file_path)
assert os.path.getsize(file_path) > 0

deftest_create_temp_dir(tmpdir):
"""测试创建临时文件夹"""
    dir_path = tmpdir.mkdir("test_dir")
assert os.path.exists(dir_path)
# 输出
# .
# .

解释:tmp_path 返回 pathlib.Path 对象,tmpdir 返回 py.path.local 对象,两者都用于创建临时文件和目录。类比:  就像一个临时工作区,测试用完就会自动清理。

小任务:  创建一个临时文件,读取文件内容并进行断言。

10. 使用 pytest-cov 测试覆盖率

痛点:  不知道哪些代码没有被测试覆盖。

方案: 使用 pytest-cov 插件测试代码覆盖率。

# 安装 pytest-cov 插件
pip install pytest-cov

# 运行 pytest 并生成覆盖率报告
pytest --cov=.

解释:pytest-cov 插件生成代码覆盖率报告,帮助你发现未测试的代码。类比: 就像一个侦探,告诉你哪些代码区域没有被 “调查” 到。

小任务:  为你的项目添加覆盖率测试,并改进测试覆盖率。

11. 更加详细的测试报告

痛点: pytest自带的测试报告太简略

方案: 使用pytest-html插件生成更加详细的HTML测试报告

# 安装 pytest-html 插件
pip install pytest-html

# 运行 pytest 并生成HTML报告
pytest --html=report.html

解释:pytest-html 插件生成详细的HTML测试报告, 报告中包含测试用例的结果,运行时间等信息类比: 把测试的结果变得更加容易解读, 方便进行分析

小任务: 为你的项目添加html测试报告。

12.  使用 pytest.ini 配置 pytest

痛点:  每次运行 pytest 都要敲一遍参数。

方案:  使用 pytest.ini 文件配置 pytest。

# pytest.ini 文件
[pytest]
markers =
    slow: mark test as slow
    fast: mark test as fast
addopts = -v  #  默认使用 verbose 模式

解释:pytest.ini 文件配置 pytest,省去命令行参数。类比: 就像给 pytest 设置一个配置文件,每次都按照里面的配置运行。

小任务:  配置 pytest.ini 文件,设置默认的参数和标记。

13.  使用插件扩展 pytest 功能

痛点:  pytest 的默认功能不够用。

方案: 使用插件扩展 pytest 的功能。解释: pytest 有丰富的插件生态,可以扩展各种功能。类比: 就像给你的 pytest 安装各种工具,让它变得更加强大。

小任务: 探索并使用一个你感兴趣的 pytest 插件。

14.  使用 conftest.py 共享 fixture

痛点:  多个测试模块需要共享 fixture。

方案:  使用 conftest.py 文件共享 fixture。

# conftest.py 文件
import pytest

@pytest.fixture
defshared_data():
"""共享 fixture"""
    print("\nSetup: Preparing shared data...")
    data = {"a": 1, "b": 2}
yield data
    print("\nTeardown: Cleaning shared data...")

解释:conftest.py 文件中的 fixture 可以被多个测试模块共享。类比: 就像一个公共仓库,不同的测试模块都可以从中获取需要的资源。

小任务:  创建一个 conftest.py 文件,在多个测试模块中使用共享的 fixture。

15.  使用 pytest-ordering 控制测试用例的执行顺序

痛点: 有时候测试用例的执行顺序会影响结果

方案:  使用pytest-ordering插件控制测试用例执行顺序

# 安装 pytest-ordering 插件
pip install pytest-ordering
# test_order.py
import pytest
@pytest.mark.run(order=2)
deftest_two():
    print("\nTest two")
assertTrue

@pytest.mark.run(order=1)
deftest_one():
    print("\nTest one")
assertTrue

解释:pytest-ordering 插件可以通过order参数控制测试用例的执行顺序类比: 就像给测试用例排序,让他们按照你的想法运行

小任务: 使用pytest-ordering插件,让你的测试用例按照指定顺序执行。

16.  使用 pytest-randomly 打乱测试用例的执行顺序

痛点:  有时候测试用例的执行顺序会导致一些潜在的问题难以发现

方案:  使用pytest-randomly插件打乱测试用例的执行顺序

# 安装 pytest-randomly 插件
pip install pytest-randomly
#运行pytest并打乱测试用例的执行顺序
pytest --randomly

解释:pytest-randomly 插件可以打乱测试用例的执行顺序类比: 就像给你的测试用例洗牌,确保他们的执行顺序不会影响结果

小任务: 使用pytest-randomly插件,随机打乱测试用例的执行顺序。

17.  使用 pytest-sugar 让测试结果更美观

痛点:  pytest 的默认输出不够炫酷

方案:  使用pytest-sugar插件让测试结果更美观

# 安装 pytest-sugar 插件
pip install pytest-sugar
#运行pytest,测试结果更美观
pytest

解释:pytest-sugar 插件可以让测试结果输出更美观,并且实时显示进度类比:  给你的测试结果做了个美颜,看起来更舒服

小任务: 使用pytest-sugar插件,让你的测试结果更美观。

18.  使用 pytest-mock 模拟外部依赖

痛点:  测试需要依赖外部系统或 API,难以隔离测试环境

方案:  使用pytest-mock插件可以方便地 mock 外部依赖

# 安装 pytest-mock 插件
pip install pytest-mock
# test_mock.py
import requests
defget_data_from_url(url):
"""从url中获取数据"""
    response = requests.get(url)
return response.text

deftest_get_data_from_url(mocker):
"""使用mocker模拟requests.get"""
    mock_get = mocker.patch('requests.get')
    mock_get.return_value.text = "mocked data"
    result = get_data_from_url('https://example.com')
assert result == "mocked data"

解释:pytest-mock 插件提供了方便地模拟外部依赖的功能类比:  就像给你的外部依赖设置了替身,让你可以随意控制

小任务: 使用pytest-mock插件模拟数据库连接,并进行单元测试。

19.  使用 pytest-xdist 并行运行测试用例

痛点:  测试用例太多,执行时间过长

方案: 使用pytest-xdist插件并行运行测试用例,提高测试效率

# 安装 pytest-xdist 插件
pip install pytest-xdist
#并行运行测试用例,使用4个进程
pytest -n 4

解释:pytest-xdist 插件可以并行运行测试用例,提高测试效率类比: 就像给你的测试开了多个线程,让它们同时运行

小任务: 使用 pytest-xdist 并行运行你的测试用例,观察测试时间的缩短。

20.  使用 parametrize 装饰器参数化测试

痛点:  需要使用不同输入测试同一个函数

方案: 使用 parametrize 装饰器参数化测试

import pytest
@pytest.mark.parametrize("test_input,expected", [
    (1, 2),
    (2, 3),
    (3, 4)
])
deftest_add_one(test_input, expected):
"""测试加一函数"""
assert test_input+1 == expected

# 输出
# .
# .
# .

解释:parametrize 装饰器可以给测试用例提供多组测试数据类比: 就像给你的测试用例准备了一套又一套的 “装备”,让它们在不同的环境下进行测试

小任务: 使用parametrize 装饰器,测试一个函数在不同输入和边界条件下的表现。

21. 使用 pytest.mark.parametrize 参数化Fixture

痛点:  需要使用不同参数来初始化 fixture

方案: 使用 pytest.mark.parametrize 参数化 Fixture

import pytest
@pytest.mark.parametrize("initial_value", [10,20,30])
@pytest.fixture()
defparameterized_fixture(request):
"""参数化fixture"""
return request.param

deftest_paramterized_fixture_test(parameterized_fixture):
"""测试使用参数化fixture"""
assert parameterized_fixture> 0
# 输出
# .
# .
# .

解释:@pytest.mark.parametrize 装饰器可以参数化Fixture类比: 就像给你的 fixture 增加了不同的“形态”,让它们根据不同的参数进行初始化

小任务: 使用 @pytest.mark.parametrize 装饰器参数化 Fixture,让它根据不同的参数返回不同的值。

22. 使用 with pytest.warns 测试警告

痛点: 需要测试函数是否会发出指定的警告

方案: 使用with pytest.warns上下文管理器来测试警告

import pytest
import warnings
defsome_func():
"""发出一个警告"""
   warnings.warn("This is a warning", UserWarning)

deftest_warns():
"""测试是否发出指定的警告"""
with pytest.warns(UserWarning) as record:
       some_func()
assert len(record) == 1
assert"This is a warning"in str(record[0].message)
# 输出
# .

解释:with pytest.warns 可以捕获指定类型的警告,并进行断言。类比: 就像给你的代码安装了一个 “警告探测器”,让你及时发现代码中的问题

小任务: 创建一个会发出警告的函数,使用 with pytest.warns 进行测试。

23. 使用pytest.importorskip 根据模块导入情况跳过测试

痛点: 某些测试依赖于特定的模块,如果模块没有安装,则需要跳过测试

方案:  使用 pytest.importorskip 函数根据模块是否安装来跳过测试

import pytest

deftest_something():
    pytest.importorskip("pandas") # 如果没有安装 pandas,则跳过测试
import pandas
assertTrue
# 输出
# (如果没有安装pandas,会显示跳过)
# (如果安装pandas,则会显示通过)

解释:pytest.importorskip 可以根据模块是否成功导入跳过测试用例类比: 就像给你的测试用例设置了一个 “依赖条件”,只有当满足条件时才执行

小任务: 编写一个需要使用 requests 模块的测试用例,使用 pytest.importorskip 来跳过测试。

24.  使用 pytest-rerunfailures 重复运行失败的测试

痛点:  有些测试用例由于环境问题,不稳定

方案: 使用 pytest-rerunfailures 插件重复运行失败的测试用例

# 安装 pytest-rerunfailures 插件
pip install pytest-rerunfailures
#运行测试,如果失败则重复运行三次
pytest --reruns 3

解释:pytest-rerunfailures 插件可以重复运行失败的测试用例类比: 就像给你的测试用例设置了一个 “复活机会”,让它们在失败后重新尝试

小任务: 使用 pytest-rerunfailures 插件,让你的测试用例在失败后重复运行。

25.  使用 pytest.param 给参数化测试用例添加ID

痛点:  参数化测试用例的输出不够清晰

方案:  使用 pytest.param 给参数化测试用例添加ID

import pytest
@pytest.mark.parametrize(
"test_input,expected",
    [
        pytest.param(1, 2, id="test_1"),
        pytest.param(2, 3, id="test_2"),
        pytest.param(3, 4, id="test_3")
    ]
)
deftest_add_one(test_input, expected):
assert test_input+1 == expected
#输出
# test_add_one[test_1] PASSED
# test_add_one[test_2] PASSED
# test_add_one[test_3] PASSED

解释:pytest.param 可以给参数化测试用例添加 ID, 让输出更加清晰类比: 就像给你的参数化测试用例贴上标签,让你可以清楚的知道每个测试用例是什么

小任务: 使用 pytest.param 给你的参数化测试用例添加 ID。

26.  使用 pytest.main 在代码中运行测试

痛点:  需要在代码中控制测试运行

方案:  使用 pytest.main 函数在代码中运行测试

import pytest
defmy_test_run():
"""在代码中运行pytest测试"""
    pytest.main(["-v", "--cov=."])

if __name__ == "__main__":
    my_test_run()

# 输出结果和在命令行中运行 pytest 相同

解释:pytest.main 函数可以在代码中运行 pytest 测试, 并可以传递命令行参数类比: 就像把 pytest 的命令行功能搬到了你的代码里,让你可以在代码中控制测试

小任务: 使用 pytest.main 函数运行测试并传递不同的命令行参数。

27.  使用 pytest_generate_tests 自定义测试用例生成

痛点: 需要动态生成测试用例

方案: 使用 pytest_generate_tests hook 函数自定义测试用例生成

defpytest_generate_tests(metafunc):
"""自定义测试用例生成"""
if"test_input"in metafunc.fixturenames:
        metafunc.parametrize("test_input", [1,2,3])

deftest_dynamic_test(test_input):
"""动态生成的测试用例"""
assert test_input > 0
#输出
# .
# .
# .

解释:pytest_generate_tests hook 函数可以自定义测试用例的生成逻辑类比: 就像一个测试用例的 “生成器”,它可以根据你的需要生成不同的测试用例

小任务: 使用 pytest_generate_tests hook 函数动态生成测试用例。

28. 使用 pytest_collection_modifyitems 修改测试用例的顺序

痛点: 需要在测试收集完成后修改测试用例的执行顺序

方案: 使用 pytest_collection_modifyitems hook函数修改测试用例的执行顺序

defpytest_collection_modifyitems(session, config, items):
"""修改测试用例的执行顺序"""
    items.reverse()
deftest_a():
    print("\nTest a")
assertTrue

deftest_b():
    print("\nTest b")
assertTrue
# 输出
#  Test b
# .
# Test a
# .

解释:pytest_collection_modifyitems hook 函数可以修改测试用例的执行顺序类比: 就像一个 “测试用例排序器”,可以修改测试用例的执行顺序

小任务: 使用 pytest_collection_modifyitems hook 函数,将你的测试用例按照文件名反向排序。

29. 使用 pytest_sessionstart 和 pytest_sessionfinish hook 函数

痛点: 需要在测试会话开始和结束时做一些初始化和清理工作

方案: 使用pytest_sessionstart 和 pytest_sessionfinish hook函数

defpytest_sessionstart(session):
"""测试会话开始时执行"""
    print("\nSession start")

defpytest_sessionfinish(session, exitstatus):
"""测试会话结束时执行"""
    print("\nSession finish")

deftest_something():
"""测试用例"""
assertTrue
#输出
# Session start
# .
# Session finish

解释:pytest_sessionstart 和 pytest_sessionfinish hook函数可以在测试会话开始和结束时执行类比: 就像给你的测试设置了启动仪式和结束仪式

小任务: 使用 pytest_sessionstart 和 pytest_sessionfinish hook 函数,在测试会话开始和结束时记录日志。

30. 使用 pytest_addoption 添加自定义命令行参数

痛点: 需要添加自定义的命令行参数

方案: 使用 pytest_addoption hook函数添加自定义命令行参数

defpytest_addoption(parser):
"""添加自定义命令行参数"""
    parser.addoption("--myoption", action="store", default="default",help="my custom option")

defpytest_configure(config):
"""从config中获取自定义参数"""
    myoption = config.getoption("--myoption")
    print(f"\nYour custom option is {myoption}")
deftest_something():
"""测试用例"""
assertTrue

# 运行 `pytest --myoption=hello`
# 输出: Your custom option is hello
# 运行 `pytest`
# 输出: Your custom option is default

解释:pytest_addoption hook函数可以添加自定义的命令行参数类比: 就像给你的测试命令行增加了新的指令,让你可以进行更加精细的控制

小任务: 使用 pytest_addoption hook 函数添加自定义命令行参数,并在测试中使用。

结尾:pytest 高级用法,让测试更上一层楼

30个 pytest 高级用法,你都掌握了吗? 从 Fixture 的魔力到 hook 函数的灵活运用,pytest 的强大远超你的想象。 掌握这些高级用法,可以让你的测试更加高效、可维护,并且更容易扩展。  希望这些知识能帮助你写出更加健壮的 Python 代码,并成为测试高手! 记得点赞收藏,分享给你的小伙伴哦! 如果有任何问题,欢迎留言,我们一起交流学习!