Ministry-Issued Spells: Built-in Fixtures


pytest Deep-Dives โ€” Hogwarts Edition

๐Ÿ“ข capsys โ€” Capturing Spell Incantations (stdout/stderr)

When a wizard shouts an incantation, capsys captures everything that goes to stdout and stderr โ€” like an Extendable Ear from Weasleys' Wizard Wheezes.

def cast_spell_aloud(spell_name: str):
    """A wizard who shouts their spells."""
    print(f"๐Ÿช„ {spell_name.upper()}!")

def test_incantation_is_shouted(capsys):
    cast_spell_aloud("Expelliarmus")
    captured = capsys.readouterr()
    assert "EXPELLIARMUS" in captured.out
    assert captured.err == ""  # no errors

def test_multiple_spells(capsys):
    cast_spell_aloud("Lumos")
    cast_spell_aloud("Nox")
    captured = capsys.readouterr()
    assert "LUMOS" in captured.out
    assert "NOX" in captured.out

๐Ÿ“ capfd โ€” Capturing File Descriptors (Low-Level Magic)

While capsys captures Python-level output, capfd works at the file-descriptor level โ€” capturing output from C extensions and subprocesses too. Like intercepting owl post at the Ministry level.

import os

def test_low_level_howler(capfd):
    """capfd captures even os.write() calls."""
    os.write(1, b"RONALD WEASLEY! HOW DARE YOU STEAL THAT CAR!\n")
    captured = capfd.readouterr()
    assert "RONALD WEASLEY" in captured.out

๐Ÿ“‹ caplog โ€” Capturing Logging Output (The Hogwarts Quill)

The Quill of Acceptance records every magical child born in Britain. caplog does the same for Python's logging module.

import logging

logger = logging.getLogger("hogwarts.admissions")

def register_student(name: str, is_magical: bool):
    if is_magical:
        logger.info(f"Quill recorded: {name}")
    else:
        logger.warning(f"Muggle detected: {name}")

def test_magical_student_logged(caplog):
    with caplog.at_level(logging.INFO, logger="hogwarts.admissions"):
        register_student("Harry Potter", is_magical=True)

    assert "Quill recorded: Harry Potter" in caplog.text
    assert len(caplog.records) == 1
    assert caplog.records[0].levelname == "INFO"

def test_muggle_warning(caplog):
    with caplog.at_level(logging.WARNING):
        register_student("Dudley Dursley", is_magical=False)

    assert "Muggle detected" in caplog.text
    assert caplog.records[0].levelname == "WARNING"

๐Ÿงช monkeypatch โ€” The Polyjuice Potion of Testing

Polyjuice Potion lets you temporarily become someone else. monkeypatch lets you temporarily replace attributes, environment variables, and dict items โ€” and it automatically reverts after the test!

import os

class OllivandersShop:
    def get_wand_price(self) -> int:
        base = int(os.environ.get("GALLEON_EXCHANGE_RATE", "7"))
        return base * 2

def test_wand_price_with_custom_exchange(monkeypatch):
    """Temporarily change an environment variable."""
    monkeypatch.setenv("GALLEON_EXCHANGE_RATE", "10")
    shop = OllivandersShop()
    assert shop.get_wand_price() == 20

def test_wand_price_default(monkeypatch):
    """Ensure env var doesn't exist (undo any previous state)."""
    monkeypatch.delenv("GALLEON_EXCHANGE_RATE", raising=False)
    shop = OllivandersShop()
    assert shop.get_wand_price() == 14  # 7 * 2

class MinistryRegistry:
    _minister = "Cornelius Fudge"

    @classmethod
    def get_minister(cls):
        return cls._minister

def test_new_minister(monkeypatch):
    """Temporarily replace a class attribute."""
    monkeypatch.setattr(MinistryRegistry, "_minister", "Kingsley Shacklebolt")
    assert MinistryRegistry.get_minister() == "Kingsley Shacklebolt"
    # After the test, it reverts to "Cornelius Fudge" automatically!

def test_patch_dict(monkeypatch):
    """Patch dictionary items."""
    spell_registry = {"Lumos": "light", "Nox": "darkness"}
    monkeypatch.setitem(spell_registry, "Lumos", "SUPER BRIGHT LIGHT")
    assert spell_registry["Lumos"] == "SUPER BRIGHT LIGHT"

๐Ÿ“‚ tmp_path โ€” Temporary Filesystem (The Vanishing Cabinet)

Like the Vanishing Cabinet that creates a passage and then disappears, tmp_path provides a temporary directory unique to each test โ€” automatically cleaned up.

from pathlib import Path

def save_spell_book(directory: Path, spells: list[str]):
    spell_file = directory / "advanced_potions.txt"
    spell_file.write_text("\n".join(spells))
    return spell_file

def test_save_spell_book(tmp_path):
    """tmp_path is a unique Path object per test invocation."""
    spells = ["Sectumsempra", "Levicorpus", "Liberacorpus"]
    result = save_spell_book(tmp_path, spells)

    assert result.exists()
    content = result.read_text()
    assert "Sectumsempra" in content
    assert len(content.splitlines()) == 3

def test_tmp_path_factory(tmp_path_factory):
    """For session-scoped temporary dirs (shared across tests)."""
    shared_dir = tmp_path_factory.mktemp("hogwarts_cache")
    (shared_dir / "student_list.txt").write_text("Harry\nRon\nHermione")
    assert (shared_dir / "student_list.txt").exists()

๐Ÿ” request โ€” Accessing Test Metadata (The Sorting Hat's Insight)

The Sorting Hat sees into your mind. The request fixture gives you introspection into the current test โ€” its name, markers, params, and more.

@pytest.fixture
def adaptive_fixture(request):
    """Adapts behavior based on test markers โ€” like the Sorting Hat."""
    if request.node.get_closest_marker("dark_arts"):
        return {"defense_level": "maximum", "location": "Room of Requirement"}
    else:
        return {"defense_level": "normal", "location": "Classroom"}

@pytest.mark.dark_arts
def test_practice_patronus(adaptive_fixture):
    assert adaptive_fixture["location"] == "Room of Requirement"

def test_regular_lesson(adaptive_fixture):
    assert adaptive_fixture["location"] == "Classroom"