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.
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")
# 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
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.
@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.
@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
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
@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
@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