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,并保持每个修补尽可能小且明确。