Pytest 固定例程

已完成

固定装置为测试提供定义的可靠上下文。 自定义夹具是用 @pytest.fixture 装饰的 Python 函数。 可以使用它们来排列测试数据、环境变量、数据库连接或测试运行前所需的文件等前置条件。 夹具还可定义清理步骤,在测试或夹具作用域完成后清理资源。

测试通过声明与固定装置同名的参数来请求固定装置。 Pytest 查找装置,运行它,并将返回的值或生成的值传递到测试中。 有关装置生命周期的详细信息,请参阅 固定装置的 pytest 文档。

当设置或清理可复用、复杂或命名后更清晰时,夹具最有用。 对于简单的一次性值,在测试中直接保留设置代码可能更易于阅读。

pytest 装置的主要特征包括:

  • 范围控制:固定装置可以使用 scope 参数来控制 pytest 创建和销毁固定装置值的频率。 更广泛的范围可以提高性能,但它们也会更长时间地共享状态。
  • 设置和清理:夹具可使用 yield 进行清理。 高级夹具还可接收内置 request 夹具(本单元后续介绍)以检查请求测试并调用 request.addfinalizer() 来注册清理回调。
  • 依赖注入:测试和夹具以参数形式请求夹具,使依赖关系明确。
  • 可重用性和模块化性:固定装置可以定义一次,并在测试函数、模块、目录或项目中重复使用。
  • 自动激活:用 @pytest.fixture(autouse=True) 定义的夹具会在其可用作用域内的每个测试中运行,无需列为参数。 请谨慎使用此选项,因为它隐藏了测试依赖项。

创建临时文件固定例程

编写与文件交互的测试时,通常需要不与其他测试冲突或依赖于硬编码位置的独立路径。 Pytest 提供内置临时目录固定装置,这些固定装置通常是新测试的最佳选择,因为 pytest 会创建唯一的目录并管理其保留期。 但是,创建小型自定义装置是了解固定装置请求和返回值的有用方法。

以下装置返回工厂函数。 装置本身不会在安装过程中创建文件。 相反,嵌套 create() 函数会在测试调用 tmp_file()时创建一个文件:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

该装置使用 Python 的 tempfile.NamedTemporaryFile() 函数和 delete=False,以便文件在关闭后仍存在。 在测试收到路径之前,文件句柄会关闭,当测试需要重新打开或删除文件时,这一点很重要,尤其是在Windows上。

以下是测试使用夹具的示例:

def test_file(tmp_file):
    path = tmp_file("ready")
    assert path.read_text(encoding="utf-8") == "ready"

在此设置中, tmp_file 是固定装置名称,并且测试接收 create() 固定装置返回的函数。 在测试内部调用 tmp_file() 将创建文件、写入请求的内容,并为其返回一个 pathlib.Path 对象。

Note

将固定装置与上下文管理器组合在一起时要小心。 如果夹具从 with 块内返回路径或句柄,上下文管理器可能在测试使用资源前就关闭或删除它。 当测试需要托管资源时,让上下文管理器在 yield 期间保持打开,或在返回前故意关闭资源并后续清理。

范围管理

默认情况下,pytest 装置使用 function 范围。 函数级夹具在测试请求时创建,并在测试结束时销毁。 如果装置使用拆解代码,pytest 会在固定装置作用域的末尾运行该拆解代码。

Pytest 还允许更广泛的装置范围来优化昂贵的设置工作。 内置夹具作用域名称:

  • function:默认作用域。 夹具按测试函数创建一次。
  • class:夹具按测试类创建一次。
  • module:夹具按模块创建一次。
  • package:夹具按定义的包创建一次,并在该包内最后一个测试(包括子包和子目录的测试)完成后销毁。
  • session:夹具按测试会话创建一次。

对于高级情况, scope 也可以是返回这些范围名称之一的可调用项。 Pytest 在定义固定装置时调用一次函数,并传递关键字参数fixture_name和config。 有关详细信息,请参阅 动态范围的 pytest 文档。

在此上下文中,创建一次指 pytest 为该作用域缓存夹具值。 例如,模块级夹具可被一个模块中的多个测试请求,但这些测试会收到相同的缓存夹具值。 参数化夹具是例外:pytest 可能为作用域内的每个参数集创建新值。

除非共享设置安全且有用,否则保持夹具为函数级。 可变对象、打开的连接、环境更改和文件如果在更大范围内被缓存,可能导致测试之间状态泄露。

下面是具有模块范围的 tmp_file 固定例程的显示效果:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture(scope="module")
def tmp_file():
    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        path.write_text(contents, encoding="utf-8")
        return path

    return create

由于此装置返回工厂函数, scope="module" 因此缓存模块的工厂函数。 每次测试调用 tmp_file()时,工厂仍会创建一个新文件。 如果装置直接创建并返回了文件路径,则模块中的每个测试都会收到相同的路径。

清理管理

以前版本的 tmp_file 固定装置会创建临时文件,但它们不会删除这些文件。 Pytest 推荐 yield 夹具用于简单清理。 yield 之前的代码执行设置并向测试提供值。 即使在设置完成后测试失败,yield 之后的代码仍将在拆解过程中运行。 如果 yield 夹具在到达 yield 前引发异常,pytest 不会运行 yield 之后的清理代码,但仍会为同一测试清理已成功完成的早期夹具。

以下版本跟踪工厂创建的每个文件,并在测试完成后尝试删除这些文件:

from pathlib import Path
import tempfile

import pytest


@pytest.fixture
def tmp_file():
    paths = []

    def create(contents=""):
        temp = tempfile.NamedTemporaryFile(delete=False)
        try:
            path = Path(temp.name)
        finally:
            temp.close()

        paths.append(path)
        path.write_text(contents, encoding="utf-8")
        return path

    yield create

    for path in paths:
        try:
            path.unlink()
        except FileNotFoundError:
            pass

使用默认 function 作用域,pytest 在请求 tmp_file的每个测试后运行清理代码。 如果将夹具更改为 scope="module",pytest 会在模块清理期间运行清理,即 pytest 完成该模块内测试后。 当多个 yield 装置处于活动状态时,pytest 会按设置的相反顺序运行其拆解代码。

清理使用 try/except FileNotFoundError 是因为测试可能会删除文件本身。 在 Windows 上,如果测试未关闭文件,删除也可能失败并出现 PermissionError,因此务必确保测试关闭创建的所有句柄。 您也可以使用 request.addfinalizer() 进行清理,以便获得更多的控制。 仅在资源创建后注册终结器,因为即使夹具后续引发异常,pytest 也会在清理期间运行已添加的终结器。 对于简单清理,yield 夹具通常更易读。

使用 conftest.py

与其在每个测试文件中定义测试夹具,你可以将共享测试夹具保存在名为 conftest.py 的文件中。 conftest.py 中的夹具自动对同一目录及其子目录中的测试可用,无需显式导入。

可以在测试套件中有多个 conftest.py 文件。 Pytest 从请求测试的角度解析夹具可用性:优先考虑测试类或模块中的本地夹具,然后是同一目录和父目录中 conftest.py 文件的夹具。 测试可向上查找夹具,但不会向下搜索同级或子目录。 插件夹具在本地夹具之后查找。 不要从 conftest.py 测试文件中导入;让 pytest 发现它。

探索内置固定例程

Pytest 具有许多内置装置,旨在简化测试。 这些装置处理常见的设置和清理任务,以便你可以专注于编写测试断言。 要查看完整列表,请参阅 pytest 内置夹具参考。

关键内置装置包括:

  • cache:在 pytest 运行中存储和检索值。
  • capsys:捕获写入 stdout 和 stderr 的文本。
  • tmp_path:为每个测试函数调用(包括每个参数化用例)提供唯一的临时目录 pathlib.Path 对象。
  • tmpdir:提供临时目录作为旧 py.path.local 对象。 对于新测试,pytest 建议使用 tmp_path。
  • monkeypatch:暂时修改类、函数、字典、环境变量和其他对象。
  • request:提供当前执行测试或夹具的信息并支持终结器。

其他常用的内置功能包括 caplog (捕获日志记录)、 capfd (捕获文件描述符级输出)、 tmp_path_factory (创建会话范围的临时目录)和 pytestconfig (公开当前 pytest 配置)。 完整列表请参阅 pytest 内置夹具参考。

下一个练习使用 tmp_path 和 pathlib.Path 在每个测试目录中创建临时文件。 你可能在现有 pytest 项目中遇到 tmpdir ,但对于新代码, tmp_path首选 。 默认情况下,pytest 会保留最近几次测试运行中的临时目录,因此在一次运行后可以看到最新的 pytest 管理的临时文件夹是正常的。 有关详细信息,请参阅 临时目录和文件的 pytest 文档。

猴子补丁在测试中的作用

上述夹具创建并清理测试资源。 另一种常见的装置用途是在需要将代码与外部状态隔离的测试中 临时状态修改 。 针对这些情况,pytest 提供内置 monkeypatch 夹具,它本身就是 pytest 夹具,也是自动清理的好示例。

测试依赖于外部资源、全局配置或操作系统状态的代码可能很有挑战性。 使用monkeypatch测试夹具可以暂时更改属性、字典值、环境变量、当前工作目录或sys.path,以便进行测试。 Pytest 会在请求的测试函数或固定装置完成后自动撤消这些更改。

下面是一个示例,展示如何在一个用于测试的小项目模块上替换函数属性。 修补范围较窄的应用程序代码(而不是 pytest 本身使用的标准库函数)使测试保持专注,避免意外破坏 pytest 自己的行为:

# project_module.py
def get_user_id():
    # Imagine this calls a remote service.
    return "real-user"
# test_project.py
import project_module


def test_user_id(monkeypatch):
    monkeypatch.setattr(project_module, "get_user_id", lambda: "test-user")
    assert project_module.get_user_id() == "test-user"

还可以将目标指定为虚线导入路径字符串。 Pytest 在应用修补程序时解析并导入命名模块,这在测试文件不需要模块时很方便。 本示例中后续的 import project_module 仅用于测试可引用修补函数进行断言:

# test_project.py
def test_user_id_by_path(monkeypatch):
    monkeypatch.setattr("project_module.get_user_id", lambda: "test-user")

    import project_module
    assert project_module.get_user_id() == "test-user"

除了使用 setattr() 设置属性和重写方法外,monkeypatch 夹具还可使用 delattr() 删除属性、设置和删除环境变量(setenv、delenv)、设置和删除字典项(setitem、delitem)、前置到 sys.path (syspath_prepend)、更改当前工作目录 (chdir),以及使用 context() 创建嵌套修补范围。 setattr() 和 delattr() 的默认 raising=True 行为有助于发现拼写错误的目标。 当修补需要在测试或夹具清理的其余部分继续前撤销时,使用 monkeypatch.context()。 有关详细信息,请参阅 monkeypatching(猴子修补)的 pytest 文档。

虽然 monkeypatch 功能强大,但请仔细使用:

  • 代码清晰和维护:过度使用 monkeypatch 或以复杂方式使用它可以使测试更难理解和维护。
  • 测试有效性:猴子修补可能导致测试在与生产行为差异很大的人工条件下通过。
  • 对实现细节的过度依赖:依赖于内部实现细节的测试在基础代码更改时可能会变得脆弱。
  • 调试复杂性:当修补程序更改基本应用程序行为时,使用 monkeypatch 的测试可能更加困难。

当有助于隔离聚焦行为时使用 monkeypatch,并保持每个修补尽可能小且明确。