Chapter 9 · The Built-in Fixture Toolbox
Chapter goals: become fluent with pytest's five everyday built-in fixtures —
tmp_path/tmp_path_factory,capsys/capfd,caplog,monkeypatch, andconfig.cache— and stop hand-writing temp-file and output-capture boilerplate.
9.1 Temp Directories: tmp_path and tmp_path_factory
Any test whose code under test touches the filesystem should never touch real directories. pytest's built-in tmp_path provides each test its own dedicated temp directory as a 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 # the sub dirKey behaviors worth memorizing:
tmp_pathis named after the test (e.g.test_create_file0), never clashing within a session;- Temp dirs from the last 3 runs are retained by default for post-mortem inspection of failures;
- The
tmp_path_retention_count/tmp_path_retention_policy(all/failed/none) settings control retention.
To create temp directories inside any-scope fixtures (e.g. a session-level shared large file), use the session-scoped tmp_path_factory:
# contents of conftest.py
import pytest
@pytest.fixture(scope="session")
def image_file(tmp_path_factory):
# expensive resource generated once per session
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 > 0Selection Rule
Single test → tmp_path; a large resource shared across tests → tmp_path_factory.mktemp() inside a session-scoped fixture.
9.2 Output Capturing: capsys and capfd
pytest captures all stdout/stderr during tests by default (via file-descriptor-level fd capture, which even catches subprocess output). To assert on what your program printed, inject capsys:
import sys
def greet(name):
print(f"Hello, {name}!")
def test_greet_output(capsys): # capsys = capture system
greet("pytest")
captured = capsys.readouterr() # read and empty the buffers
assert captured.out == "Hello, pytest!\n"
assert captured.err == "" # stderr empty
def test_error_to_stderr(capsys):
print("warning", file=sys.stderr)
assert "warning" in capsys.readouterr().errThe object returned by readouterr() exposes .out and .err. If the code under test bypasses Python and writes to OS file descriptors directly (C extensions, os.write(1, ...)), capsys can't see it — use capfd (capture file descriptor) instead; their APIs are identical.
Capture mode is adjustable on the CLI: pytest -s disables capture entirely, --capture=sys intercepts only the Python layer, and --capture=tee-sys captures while still passing through.
9.3 Log Capturing: caplog
Don't scrape stdout to assert logs — the right tool is the caplog fixture. It collects logging records emitted during the test into memory with accessors records (list of LogRecord), text (formatted full text), and 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)
# style one: assert on (logger name, level, message) tuples
assert ("shop", logging.ERROR, "invalid quantity: -1") in caplog.record_tuples
def test_positive(caplog):
process_order(3)
assert "processed 3" in caplog.text # style two: substring of full textTwo control methods you must know:
def test_with_level_control(caplog):
# below WARNING, records may not reach handlers; raise the level first
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): # effective inside the with block, auto-restored
logging.debug("only visible inside")
assert "only visible inside" in caplog.text9.4 monkeypatch Preview and cache
monkeypatch is the most powerful built-in fixture, safely replacing attributes/env vars/dict entries and restoring everything automatically after the test. This chapter only previews it; Chapter 10 expands the full API:
def test_home_dir(monkeypatch):
monkeypatch.setenv("HOME", "/fake/home") # auto-restored after the test
import os
assert os.environ["HOME"] == "/fake/home"Another easily overlooked built-in fixture is cache (plugin name cacheprovider). It underlies cross-run memory features like --lf/--ff: data lives as JSON under the root .pytest_cache/, and you can use it for your own cross-session data too.
The more common route is request.config.cache inside fixtures or plugins to store JSON-serializable data:
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() # actually computed only the first time
cache.set(key, value)
return value
def compute_something_slow():
return [1, 2, 3]Clear cached content with pytest --cache-clear; add .pytest_cache to .gitignore.
9.5 Chapter Summary
tmp_path(aPathper test) /tmp_path_factory(session-level directory maker) cover all temp-file needs;capsysasserts Python-level output;capfdcatches file-descriptor-level output; identical APIs;caplog.records/.text/.record_tuplesplusset_level/at_levelform the standard log-assertion combo;monkeypatchauto-restores every modification;cacheprovides cross-run JSON storage and powers--lf.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What type of object does the tmp_path fixture provide?
2. A C extension under test writes directly to file descriptor 1. Which fixture asserts that output?
3. INFO logs emitted in a test don’t appear in caplog.records. The most likely cause?
4. Which statement about config.cache / --lf is correct?
🛠️ Hands-On Exercises
- Write
save_config(path: Path, data: dict)with tests: usetmp_pathto create a JSON file and verify lossless round-trip serialization. - Test a CLI-style function: assert normal and error output separately with
capsysandcapfdto internalize the difference. - Test a function that emits DEBUG logs: observe behavior without
at_level, then wrap the call withwith caplog.at_level(logging.DEBUG)and compare — validating your level understanding.
Next chapter goes deep on mocking methodology — the full
monkeypatchAPI through tounittest.mockandmocker: Chapter 10.