第 9 章 · 内置 fixture 工具箱
本章目标:熟练使用 pytest 自带的五大常用 fixture——
tmp_path/tmp_path_factory、capsys/capfd、caplog、monkeypatch、config.cache,不再手写临时文件与输出捕获的样板代码。
9.1 临时目录:tmp_path 与 tmp_path_factory
凡是"被测代码要读写文件系统"的测试,都不该碰真实目录。pytest 内置的 tmp_path 为每个测试提供一个专属的临时目录,类型是 pathlib.Path:
# 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_policy(all/failed/none)控制保留策略。
如果需要在任意 scope 的 fixture 里创建临时目录(比如 session 级共享一份大文件),用 session 级的 tmp_path_factory:
# 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:
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().errreadouterr() 返回的对象有 .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 记录收进内存,提供 records(LogRecord 列表)、text(格式化全文)、record_tuples 等访问入口:
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 # 方式二:全文子串两个必须掌握的控制方法:
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.text9.4 monkeypatch 预览与 cache 缓存
monkeypatch 是内置 fixture 中功能最强的,负责安全地替换属性/环境变量/字典项并在测试结束后自动还原。本章只做概览,第 10 章将完整展开它的 API:
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 可序列化的数据:
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 的说法正确的是?
🛠️ 动手实践
- 写一个
save_config(path: Path, data: dict)函数与其测试:用tmp_path创建 JSON 文件并验证往返序列化无损。 - 给一个 CLI 风格函数编写测试:分别用
capsys和capfd断言正常输出与错误输出,体会二者差异。 - 为一个会打 DEBUG 日志的函数写测试:不调
at_level时观察结果,再用with caplog.at_level(logging.DEBUG)包住调用对比,验证你对日志级别的理解。
下一章深入 mock 的完整方法论——从
monkeypatch全 API 到unittest.mock与mocker:第 10 章。