Skip to content

第 9 章 · 内置 fixture 工具箱

本章目标:熟练使用 pytest 自带的五大常用 fixture——tmp_path/tmp_path_factorycapsys/capfdcaplogmonkeypatchconfig.cache,不再手写临时文件与输出捕获的样板代码。

9.1 临时目录:tmp_path 与 tmp_path_factory

凡是"被测代码要读写文件系统"的测试,都不该碰真实目录。pytest 内置的 tmp_path每个测试提供一个专属的临时目录,类型是 pathlib.Path

python
# content of test_tmp_path.py
def test_create_file(tmp_path):
    d = tmp_path / "sub"
    d.mkdir()
    p = d / "hello.txt"
    p.write_text("content", encoding="utf-8")

    assert p.read_text(encoding="utf-8") == "content"
    assert len(list(tmp_path.iterdir())) == 1  # sub 目录

几个关键行为值得记住:

  • tmp_path 以测试名命名(如 test_create_file0),同一会话内互不冲突;
  • 默认保留最近 3 次 pytest 运行的临时目录,方便失败后现场取证;
  • 可用配置项 tmp_path_retention_count / tmp_path_retention_policyall/failed/none)控制保留策略。

如果需要在任意 scope 的 fixture 里创建临时目录(比如 session 级共享一份大文件),用 session 级的 tmp_path_factory

python
# contents of conftest.py
import pytest


@pytest.fixture(scope="session")
def image_file(tmp_path_factory):
    # 昂贵的资源整个会话只生成一次
    fn = tmp_path_factory.mktemp("data") / "img.png"
    fn.write_bytes(b"\x89PNG fake image")
    return fn


# contents of test_image.py
def test_histogram(image_file):
    assert image_file.stat().st_size > 0

选型规则

单测用 tmp_path;跨多个测试共享的大资源用 tmp_path_factory.mktemp() 放到 session 级 fixture 里。

9.2 输出捕获:capsys 与 capfd

pytest 默认会捕获测试期间的所有 stdout/stderr(默认方式是文件描述符级的 fd 捕获,连子进程输出也能抓到)。想在断言里检查程序打印了什么,注入 capsys

python
import sys


def greet(name):
    print(f"Hello, {name}!")


def test_greet_output(capsys):          # capsys = capture system
    greet("pytest")
    captured = capsys.readouterr()      # 读出并清空缓冲

    assert captured.out == "Hello, pytest!\n"
    assert captured.err == ""           # stderr 为空


def test_error_to_stderr(capsys):
    print("warning", file=sys.stderr)
    assert "warning" in capsys.readouterr().err

readouterr() 返回的对象有 .out.err 两个属性。如果被测代码绕过 Python 层直接写操作系统文件描述符(例如 C 扩展、os.write(1, ...)),capsys 就无能为力了,需要用 capfd(capture file descriptor)——两者 API 完全一致。

捕获方式可通过命令行调整:pytest -s 完全关闭捕获、--capture=sys 只拦截 Python 层、--capture=tee-sys 一边捕获一边照常透传。

9.3 日志捕获:caplog

对日志的断言不要去抓 stdout——正确姿势是 caplog fixture。它把测试期间的 logging 记录收进内存,提供 recordsLogRecord 列表)、text(格式化全文)、record_tuples 等访问入口:

python
import logging


def process_order(qty: int):
    if qty <= 0:
        logging.getLogger("shop").error("invalid quantity: %d", qty)
        raise ValueError("qty must be positive")
    logging.getLogger("shop").info("processed %d", qty)


def test_negative_qty_logged(caplog):
    with pytest.raises(ValueError):
        process_order(-1)

    # 方式一:按 (logger名, 级别, 消息) 三元组断言
    assert ("shop", logging.ERROR, "invalid quantity: -1") in caplog.record_tuples


def test_positive(caplog):
    process_order(3)
    assert "processed 3" in caplog.text      # 方式二:全文子串

两个必须掌握的控制方法:

python
def test_with_level_control(caplog):
    # 默认 WARNING 级以下可能不进 handler,先抬高级别再触发
    caplog.set_level(logging.INFO, logger="shop")
    process_order(5)
    assert any(r.levelname == "INFO" for r in caplog.records)


def test_temporary_level(caplog):
    with caplog.at_level(logging.DEBUG):     # with 块内生效,退出自动恢复
        logging.debug("only visible inside")
    assert "only visible inside" in caplog.text

9.4 monkeypatch 预览与 cache 缓存

monkeypatch 是内置 fixture 中功能最强的,负责安全地替换属性/环境变量/字典项并在测试结束后自动还原。本章只做概览,第 10 章将完整展开它的 API:

python
def test_home_dir(monkeypatch):
    monkeypatch.setenv("HOME", "/fake/home")   # 测试结束自动还原
    import os
    assert os.environ["HOME"] == "/fake/home"

另一个容易被忽略的内置 fixture 是 cache(插件内部名 cacheprovider)。它就是 --lf/--ff 这类"跨运行记忆"功能的底层:数据以 JSON 形式存在根目录的 .pytest_cache/ 里,你也能用它跨会话存取自己的数据:

更常见的用法是在 fixture 或插件中通过 request.config.cache 存取 JSON 可序列化的数据:

python
def expensive_lookup(request):
    cache = request.config.cache
    key = "myapp/expensive_result"
    value = cache.get(key, None)
    if value is None:
        value = compute_something_slow()   # 仅首次真正计算
        cache.set(key, value)
    return value


def compute_something_slow():
    return [1, 2, 3]

缓存内容可用 pytest --cache-clear 手动清空;.pytest_cache 目录应加入 .gitignore

9.5 本章小结

  • tmp_path(每测试一个 Path)/ tmp_path_factory(session 级造目录)覆盖所有临时文件需求;
  • capsys 断言 Python 层输出,capfd 能抓到文件描述符级输出,API 相同;
  • caplog.records/.text/.record_tuples + set_level/at_level 是日志断言的标准组合拳;
  • monkeypatch 自动还原一切修改;cache 提供跨运行的 JSON 存储,也是 --lf 的实现基础。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. tmp_path fixture 提供的对象类型是?

2. 被测的 C 扩展直接向文件描述符 1 写数据,要断言这段输出应该用?

3. 测试里发出的 INFO 日志没出现在 caplog.records 中,最可能的原因是?

4. 关于 config.cache / --lf 的说法正确的是?

🛠️ 动手实践

  1. 写一个 save_config(path: Path, data: dict) 函数与其测试:用 tmp_path 创建 JSON 文件并验证往返序列化无损。
  2. 给一个 CLI 风格函数编写测试:分别用 capsyscapfd 断言正常输出与错误输出,体会二者差异。
  3. 为一个会打 DEBUG 日志的函数写测试:不调 at_level 时观察结果,再用 with caplog.at_level(logging.DEBUG) 包住调用对比,验证你对日志级别的理解。

下一章深入 mock 的完整方法论——从 monkeypatch 全 API 到 unittest.mockmocker第 10 章