Kodokon kodokon.com

Testen mit pytest: Fixtures, Parametrisierung, bewährte Praktiken

Strukturiere robuste Tests mit pytest: Fixture-Injektion, Teardown per yield, tabellengetriebene Parametrisierung und die Fallstricke breiter Scopes.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

pytest ruht auf zwei starken Ideen: Erkennung per Konvention (Dateien test_*.py, Funktionen test_*) und das schlichte assert, dessen Introspektion bei einem Fehlschlag die tatsächlichen Werte beider Seiten anzeigt - kein Auswendiglernen von assertEqual und seinen dreißig Varianten mehr. Zwei Werkzeuge, die du sofort kennen solltest: pytest.raises, um zu prüfen, dass eine Ausnahme tatsächlich ausgelöst wird, und pytest.approx, um Floats zu vergleichen, ohne gegen die binäre Rundung zu kämpfen.

PYTHON
import pytest

def divide(a: float, b: float) -> float:
    return a / b

def test_nominal() -> None:
    assert divide(8, 4) == 2.0

def test_zero_division() -> None:
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

def test_floats() -> None:
    assert 0.1 + 0.2 == pytest.approx(0.3)
Ohne approx ist 0.1 + 0.2 == 0.3 bei binären Floats falsch.

Fixtures sind die Dependency Injection von pytest: Deklariere einen Parameter, der den Namen einer Fixture trägt, und pytest baut sie und injiziert sie dann. Das yield trennt Setup vom Teardown - der Code danach läuft, selbst wenn der Test fehlschlägt. Der Parameter scope (function standardmäßig, module, session) steuert die Lebensdauer des Objekts. Leg gemeinsame Fixtures in eine conftest.py: Sie werden im ganzen Ordner verfügbar, ohne einen einzigen Import.

PYTHON
import sqlite3

import pytest

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()

def test_insert(db) -> None:
    db.execute("INSERT INTO users VALUES ('Ada')")
    query = "SELECT name FROM users"
    rows = db.execute(query).fetchall()
    assert rows == [("Ada",)]
conn.close() läuft nach dem Test, egal ob er besteht oder fehlschlägt.

@pytest.mark.parametrize macht aus einem Test eine Tabelle von Fällen: Jedes Tupel wird zu einem eigenständigen Test, einzeln benannt und filterbar. Staple mehrere parametrize-Dekoratoren, um das kartesische Produkt der Parameter zu erhalten. Gib deinen Fällen lesbare ids über pytest.param(..., id='...') - ein Fehlerbericht sollte verständlich sein, ohne die Quelldatei zu öffnen.

PYTHON
import pytest

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello World", "hello-world"),
        ("  Python  rocks ", "python-rocks"),
        pytest.param("", "", id="empty"),
    ],
)
def test_slugify(raw: str, expected: str) -> None:
    assert slugify(raw) == expected
Drei Fälle, drei eigenständige Tests im Bericht.
BASH
pytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5
--lf führt nur die Fehlschläge erneut aus; --durations enthüllt die langsamen Tests.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Woher weiß pytest, welche Fixture es einem Test liefern soll?
    • Dank eines @inject-Dekorators, der auf den Test gesetzt wird
    • Durch die Reihenfolge, in der Fixtures in der Datei deklariert werden
    • Durch die Typ-Annotation des Parameters
    • Durch den Namen des Parameters: Passt er zu einer sichtbaren Fixture (Modul oder conftest.py), baut pytest sie und injiziert sie
  2. Wozu dient in einer Fixture der Code, der nach dem yield steht?
    • Es ist das Teardown: Es läuft nach dem Test, selbst wenn der Test fehlgeschlagen ist
    • Es läuft nie, das yield beendet die Fixture
    • Es liefert einen Ersatzwert, falls das Setup fehlschlägt
    • Es läuft nur, wenn der Test fehlschlägt
  3. Was macht der Befehl pytest --lf?
    • Es listet die im Projekt verfügbaren Fixtures auf
    • Es führt nur die Tests erneut aus, die beim letzten Lauf fehlgeschlagen sind
    • Es stoppt die Sitzung beim ersten aufgetretenen Fehlschlag