The Room of Requirement: Advanced Magic


pytest Deep-Dives β€” Hogwarts Edition

πŸ“œ Yield Fixtures β€” Opening & Closing the Marauder's Map

The Marauder's Map must be activated ("I solemnly swear that I am up to no good") and deactivated ("Mischief managed!"). Yield fixtures follow the same pattern: setup β†’ yield β†’ teardown.

import pytest

class MaraudersMap:
    def __init__(self):
        self.is_active = False
        self.tracking = []

    def activate(self):
        self.is_active = True
        print("πŸ—ΊοΈ I solemnly swear that I am up to no good!")

    def deactivate(self):
        self.is_active = False
        self.tracking.clear()
        print("πŸ—ΊοΈ Mischief managed!")

    def locate(self, person: str) -> str:
        if not self.is_active:
            raise RuntimeError("Map is not active!")
        location = f"{person} β†’ Great Hall"
        self.tracking.append(location)
        return location

@pytest.fixture
def marauders_map():
    """Yield fixture: setup before yield, teardown after."""
    the_map = MaraudersMap()
    the_map.activate()       # ← SETUP (before yield)
    yield the_map            # ← test receives this object
    the_map.deactivate()     # ← TEARDOWN (runs even if test fails!)

def test_locate_snape(marauders_map):
    location = marauders_map.locate("Severus Snape")
    assert "Snape" in location
    assert marauders_map.is_active is True

def test_locate_draco(marauders_map):
    location = marauders_map.locate("Draco Malfoy")
    assert "Draco" in location

The teardown code after yield always runs, even if the test raises an exception β€” like a magical contract!

πŸ”’ Fixture Finalizers β€” request.addfinalizer

An alternative to yield for teardown. Useful when you need multiple cleanup steps or conditional teardown (like destroying multiple Horcruxes).

@pytest.fixture
def horcrux_collection(request):
    """Using addfinalizer β€” can register multiple teardown steps."""
    horcruxes = []

    def destroy_all_horcruxes():
        for h in horcruxes:
            print(f"πŸ’€ Destroying horcrux: {h}")
        horcruxes.clear()

    def notify_dumbledore():
        print("πŸ“¨ Notifying Dumbledore: all horcruxes destroyed.")

    request.addfinalizer(notify_dumbledore)  # runs LAST (LIFO order)
    request.addfinalizer(destroy_all_horcruxes)  # runs FIRST

    # Setup
    horcruxes.extend(["Diary", "Ring", "Locket", "Cup", "Diadem", "Harry", "Nagini"])
    return horcruxes

def test_count_horcruxes(horcrux_collection):
    assert len(horcrux_collection) == 7

🌟 Autouse Fixtures β€” The Trace (Applied Automatically)

Like the Trace on underage wizards, autouse=True fixtures apply to every test in their scope without being explicitly requested.

@pytest.fixture(autouse=True)
def reset_house_points():
    """Automatically resets house points before EVERY test."""
    Hogwarts.reset_points()
    yield
    # After each test, verify no house has negative points
    for house in ["Gryffindor", "Slytherin", "Hufflepuff", "Ravenclaw"]:
        assert Hogwarts.get_points(house) >= 0

def test_award_points():
    Hogwarts.award("Gryffindor", 50)
    assert Hogwarts.get_points("Gryffindor") == 50

def test_deduct_points():
    Hogwarts.deduct("Slytherin", 10)
    # autouse ensures points were reset, so this starts from 0
    # Gryffindor is back to 0, not 50!
    assert Hogwarts.get_points("Gryffindor") == 0

πŸ§ͺ Parametrized Fixtures β€” Multiple Potions from One Cauldron

@pytest.fixture(params=["Polyjuice", "Veritaserum", "Amortentia"])
def potion(request):
    """This fixture runs the test 3 times β€” once per potion."""
    potion_name = request.param
    cauldron = Cauldron()
    brewed = cauldron.brew(potion_name)
    yield brewed
    cauldron.clean()  # teardown

def test_potion_is_liquid(potion):
    """Runs 3 times: Polyjuice, Veritaserum, Amortentia."""
    assert potion.state == "liquid"

def test_potion_has_color(potion):
    """Also runs 3 times! 3 potions Γ— 2 tests = 6 total test cases."""
    assert potion.color is not None

πŸ”€ Indirect Parametrize β€” Casting Through a Proxy

Sometimes you want to parametrize the fixture itself from the test, passing arguments through the fixture (like casting a spell through another wizard's wand).

@pytest.fixture
def wand(request):
    """Fixture receives params indirectly from @pytest.mark.parametrize."""
    wood, core = request.param
    return Wand(wood=wood, core=core, length_inches=11.0)

@pytest.mark.parametrize("wand", [
    ("holly", "phoenix_feather"),
    ("elder", "thestral_tail_hair"),
    ("vine", "dragon_heartstring"),
], indirect=True)  # ← key: tells pytest to pass params TO the fixture
def test_wand_allegiance(wand):
    """Each parametrized tuple is passed to the 'wand' fixture via request.param."""
    assert wand.wood in ["holly", "elder", "vine"]
    assert wand.cast("Lumos") == "Lumos!"

The indirect=True flag redirects parametrize values into request.param inside the fixture, letting you build complex objects from simple test-level parameters.