The Time-Turner: Marks & Parametrization


pytest Deep-Dives — Hogwarts Edition

⚡ What Are Marks? — Enchanting Your Tests

Just as Hogwarts students receive marks on their O.W.L.s, pytest lets you mark your tests with magical decorators that control when and how they run. Marks are metadata labels attached to test functions.

Built-in Marks — The Unforgivable (Skippable) Curses

import pytest

# Skip — like casting Obliviate on a test
@pytest.mark.skip(reason="Dumbledore said not to use this spell yet")
def test_elder_wand_power():
    assert elder_wand.power_level > 9000

# Skipif — conditional skip, like the Trace on underage wizards
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="No Hogwarts Express on Windows")
def test_platform_nine_three_quarters():
    assert platform.enter() == "Hogwarts Express"

# Xfail — we EXPECT this to fail (like Neville's first potion)
@pytest.mark.xfail(reason="Voldemort's horcrux makes this unstable")
def test_destroy_horcrux_without_sword():
    assert horcrux.destroy(method="bare_hands")

Custom Marks — Creating Your Own Spells

# Register in pytest.ini or pyproject.toml:
# [pytest]
# markers =
#     dark_arts: marks tests involving Dark Arts (deselect with '-m "not dark_arts"')
#     quidditch: marks tests for Quidditch module

@pytest.mark.dark_arts
def test_avada_kedavra_is_unforgivable():
    assert "Avada Kedavra" in UNFORGIVABLE_CURSES

@pytest.mark.quidditch
def test_snitch_catch_ends_game():
    game = QuidditchMatch(seeker="Harry Potter")
    game.catch_snitch()
    assert game.is_over

Run only Quidditch tests: pytest -m quidditch

🔄 Parametrize — The Time-Turner of Testing

Just as Hermione used the Time-Turner to attend multiple classes simultaneously, @pytest.mark.parametrize lets a single test function run multiple scenarios without duplicating code.

Basic Parametrization — Testing Multiple Spells

@pytest.mark.parametrize("spell, damage", [
    ("Expelliarmus", 10),
    ("Stupefy", 25),
    ("Avada Kedavra", 100),
])
def test_spell_damage(spell, damage):
    """Each spell inflicts the correct amount of damage."""
    wand = Wand(owner="Harry Potter", core="phoenix_feather")
    result = wand.cast(spell)
    assert result.damage == damage

This generates 3 separate tests, each with its own pass/fail status.

Stacking Parametrize — Cartesian Product (Every Wizard × Every Spell)

@pytest.mark.parametrize("wizard", ["Harry", "Hermione", "Ron"])
@pytest.mark.parametrize("spell", ["Lumos", "Expelliarmus", "Patronus"])
def test_wizard_can_cast(wizard, spell):
    """Generates 3×3 = 9 test cases automatically."""
    hogwarts_student = Wizard(name=wizard)
    result = hogwarts_student.cast(spell)
    assert result.success is True

Parametrize with Dataclasses — The Pensieve Pattern

from dataclasses import dataclass

@dataclass
class SpellScenario:
    spell_name: str
    caster_house: str
    expected_power: int
    description: str  # used as test ID

SPELL_SCENARIOS = [
    SpellScenario("Expecto Patronum", "Gryffindor", 95, "harry_patronus"),
    SpellScenario("Sectumsempra", "Slytherin", 80, "draco_dark_spell"),
    SpellScenario("Wingardium Leviosa", "Ravenclaw", 30, "flitwick_basics"),
]

@pytest.mark.parametrize("scenario", SPELL_SCENARIOS, ids=lambda s: s.description)
def test_spell_power_by_house(scenario):
    wizard = Wizard(house=scenario.caster_house)
    result = wizard.cast(scenario.spell_name)
    assert result.power >= scenario.expected_power

Marks on Specific Cases — Cursed Parameters

@pytest.mark.parametrize("potion, effect", [
    ("Polyjuice", "transformation"),
    ("Felix Felicis", "luck"),
    pytest.param("Draught of Living Death", "sleep",
                 marks=pytest.mark.xfail(reason="Slughorn's recipe is incomplete")),
    pytest.param("Elixir of Life", "immortality",
                 marks=pytest.mark.skip(reason="Philosopher's Stone destroyed")),
])
def test_potion_effect(potion, effect):
    cauldron = Cauldron(temperature=100)
    result = cauldron.brew(potion)
    assert result.effect == effect

Custom IDs — Readable Test Names in the Marauder's Map

@pytest.mark.parametrize(
    "house, founder",
    [
        ("Gryffindor", "Godric"),
        ("Slytherin", "Salazar"),
        ("Hufflepuff", "Helga"),
        ("Ravenclaw", "Rowena"),
    ],
    ids=["lion_house", "serpent_house", "badger_house", "eagle_house"]
)
def test_house_founder(house, founder):
    """IDs appear in pytest output instead of param values."""
    hogwarts = Hogwarts()
    assert hogwarts.get_founder(house).first_name == founder

Output: test_houses.py::test_house_founder[lion_house] PASSED