Skip to content

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, and config.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:

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  # the sub dir

Key behaviors worth memorizing:

  • tmp_path is 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:

python
# 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 > 0

Selection 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:

python
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().err

The 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:

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)

    # 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 text

Two control methods you must know:

python
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.text

9.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:

python
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:

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()   # 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 (a Path per test) / tmp_path_factory (session-level directory maker) cover all temp-file needs;
  • capsys asserts Python-level output; capfd catches file-descriptor-level output; identical APIs;
  • caplog.records/.text/.record_tuples plus set_level/at_level form the standard log-assertion combo;
  • monkeypatch auto-restores every modification; cache provides 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 ​

  1. Write save_config(path: Path, data: dict) with tests: use tmp_path to create a JSON file and verify lossless round-trip serialization.
  2. Test a CLI-style function: assert normal and error output separately with capsys and capfd to internalize the difference.
  3. Test a function that emits DEBUG logs: observe behavior without at_level, then wrap the call with with caplog.at_level(logging.DEBUG) and compare — validating your level understanding.

Next chapter goes deep on mocking methodology — the full monkeypatch API through to unittest.mock and mocker: Chapter 10.