W3docs

Python Unit-Tests mit pytest

pytest von Grund auf lernen: Assertions schreiben, Fixtures verwenden, Tests parametrisieren und eine Test-Suite mit conftest.py organisieren.

pytest ist Pythons beliebtestes Test-Framework. Es ermöglicht das Schreiben kleiner, lesbarer Testfunktionen mit einfachen assert-Anweisungen — keine Boilerplate-Klassen erforderlich — und skaliert dennoch für komplexe Test-Suites mit gemeinsamen Fixtures, Parametrisierung und Plugins.

Dieses Kapitel behandelt alles, was Sie für das Testen von Python-Code mit pytest benötigen: Installation, Ihren ersten Test, Assertions und erwartete Ausnahmen, Fixtures, Parametrisierung, Testorganisation mit conftest.py, nützliche Befehlszeilenoptionen und die häufigsten Fallstricke.

Warum pytest?

Python wird mit dem unittest-Modul ausgeliefert. Warum also stattdessen pytest verwenden?

Merkmalunittestpytest
TestsyntaxKlasse + MethodeEinfache Funktion
Assertionsself.assertEqual(a, b)assert a == b
FixturessetUp / tearDown@pytest.fixture (kombinierbar)
ParametrisierungManuelle Schleife@pytest.mark.parametrize
Plugin-ÖkosystemMinimal1 000+ Plugins (Coverage, Mock usw.)

pytest führt außerdem unittest-Tests unverändert aus, sodass Sie es schrittweise einführen können.

Installation

pytest ist nicht Teil der Standardbibliothek. Installieren Sie es mit pip in einer virtuellen Umgebung:

python -m venv .venv
source .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install pytest

Installation überprüfen:

pytest --version
# pytest 8.x.x

Lesen Sie Python pip, wenn Sie eine Auffrischung zur Paketverwaltung benötigen.

Ihr erster Test

pytest erkennt Testdateien automatisch. Standardmäßig sucht es nach:

  • Dateien mit dem Namen test_*.py oder *_test.py
  • Funktionen, deren Namen mit test_ beginnen

Erstellen Sie math_utils.py mit einer einfachen Funktion:

# math_utils.py

def add(a, b):
    return a + b

Erstellen Sie nun test_math_utils.py im gleichen Verzeichnis:

# test_math_utils.py
from math_utils import add

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-1, 1) == 0

def test_add_zeros():
    assert add(0, 0) == 0

Tests ausführen:

pytest test_math_utils.py

Ausgabe:

collected 3 items

test_math_utils.py ...                                                 [100%]

3 passed in 0.01s

Jeder Punkt steht für einen bestandenen Test. Ein fehlgeschlagener Test gibt F aus und zeigt den vollständigen Assertion-Diff an.

Assertions

pytest schreibt einfache assert-Anweisungen zur Erfassungszeit um, sodass Fehler einen detaillierten Diff anzeigen — keine speziellen Assertion-Methoden erforderlich.

def test_assertion_diff():
    result = [1, 2, 4]
    expected = [1, 2, 3]
    assert result == expected   # pytest shows exactly where lists differ

Eine Fehlerausgabe sieht so aus:

AssertionError: assert [1, 2, 4] == [1, 2, 3]
  At index 2: 4 != 3

Gleitkommazahl-Vergleiche

Vergleichen Sie Gleitkommazahlen niemals mit == — Rundungsfehler machen dies unzuverlässig. Verwenden Sie pytest.approx:

import pytest
import math

def circle_area(r):
    return math.pi * r * r

def test_circle_area():
    assert circle_area(5) == pytest.approx(78.53981633974483)

pytest.approx akzeptiert eine optionale Toleranz abs oder rel:

assert 0.1 + 0.2 == pytest.approx(0.3, abs=1e-9)

Erwartete Ausnahmen testen

Verwenden Sie pytest.raises als Context-Manager, um zu prüfen, ob eine bestimmte Ausnahme ausgelöst wird:

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

Das Argument match ist ein regulärer Ausdruck, der gegen die Ausnahmemeldung geprüft wird. Wird die Ausnahme nicht ausgelöst, schlägt pytest den Test fehl — so stellen Sie sicher, dass Sie Regressionen erkennen, bei denen die Fehlerbehandlung versehentlich entfernt wurde.

Lesen Sie Python Try...Except für einen tieferen Einblick in die Ausnahmebehandlung, und Raising Exceptions dazu, wie Sie Ausnahmen absichtlich auslösen.

Parametrisierung: Einen Test mit vielen Eingaben ausführen

@pytest.mark.parametrize ermöglicht es, dieselbe Testlogik gegen mehrere Datensätze auszuführen, ohne eine Schleife zu schreiben:

import pytest
from math_utils import add

@pytest.mark.parametrize("a, b, expected", [
    (2,  3,  5),
    (-1, 1,  0),
    (0,  0,  0),
    (10, -5, 5),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

pytest generiert für jedes Tupel einen separaten Testfall und berichtet sie einzeln:

test_math_utils.py::test_add[2-3-5] PASSED
test_math_utils.py::test_add[-1-1-0] PASSED
test_math_utils.py::test_add[0-0-0] PASSED
test_math_utils.py::test_add[10--5-5] PASSED

Dies ist wesentlich übersichtlicher als eine manuelle Schleife — einzelne Fehler sind isoliert und leicht zu identifizieren.

Fixtures

Eine Fixture ist eine Funktion, die mit @pytest.fixture dekoriert ist und gemeinsames Setup (und optionale Bereinigung) für Tests bereitstellt. Anstatt Setup-Code in jedem Test zu wiederholen, deklarieren Sie eine Fixture einmal und injizieren sie namentlich als Testparameter.

Einfache Fixture

import pytest

class UserStore:
    def __init__(self):
        self.users = []

    def add_user(self, name):
        self.users.append(name)

    def count(self):
        return len(self.users)

@pytest.fixture
def store():
    return UserStore()

def test_empty_store(store):
    assert store.count() == 0

def test_add_user(store):
    store.add_user("Alice")
    assert store.count() == 1

pytest erkennt, dass test_add_user einen Parameter namens store hat, sucht nach einer Fixture mit diesem Namen, ruft sie auf und übergibt das Ergebnis. Jeder Test erhält eine frische Fixture-Instanz — Änderungen in einem Test wirken sich nie auf einen anderen aus.

Fixtures mit Bereinigung (yield)

Verwenden Sie yield innerhalb einer Fixture, um sie in Setup (vor yield) und Bereinigung (nach yield) aufzuteilen. Dies stellt sicher, dass die Bereinigung immer ausgeführt wird, auch wenn der Test fehlschlägt:

import pytest
import tempfile
import os

@pytest.fixture
def temp_file():
    fd, path = tempfile.mkstemp(suffix=".txt")
    os.close(fd)
    yield path           # test receives the path here
    if os.path.exists(path):
        os.unlink(path)  # always runs after the test

def test_write_to_temp_file(temp_file):
    with open(temp_file, "w") as f:
        f.write("hello")
    with open(temp_file) as f:
        assert f.read() == "hello"

Fixture-Gültigkeitsbereich

Standardmäßig werden Fixtures für jede Testfunktion erstellt und bereinigt. Sie können den Gültigkeitsbereich erweitern, um aufwändiges Setup zu reduzieren:

GültigkeitsbereichEinmal erstellt pro
"function" (Standard)Jede Testfunktion
"class"Jede Testklasse
"module"Jede Testdatei
"session"Gesamter Testlauf
@pytest.fixture(scope="session")
def database_connection():
    conn = create_db_connection()
    yield conn
    conn.close()

Verwenden Sie den Gültigkeitsbereich "session" für aufwändige Ressourcen wie Datenbankverbindungen oder Serverprozesse. Verwenden Sie den Gültigkeitsbereich "function" (Standard) für alles, was den Zustand verändert.

Eingebaute Fixtures

pytest wird mit mehreren eingebauten Fixtures geliefert, die Sie ohne Import verwenden können:

  • tmp_path — ein pathlib.Path, der auf ein für den Test eindeutiges temporäres Verzeichnis zeigt.
  • monkeypatch — ersetzt Attribute, Umgebungsvariablen oder Dictionary-Einträge für die Dauer eines Tests und setzt sie danach automatisch zurück.
  • capsys — erfasst die Ausgabe von stdout / stderr, sodass Sie Assertions auf ausgegebenen Text durchführen können.
def greet(name):
    print(f"Hello, {name}!")

def test_greet_output(capsys):
    greet("World")
    captured = capsys.readouterr()
    assert captured.out == "Hello, World!\n"

monkeypatch verwenden

monkeypatch ist der idiomatische Weg, externe Abhängigkeiten in Tests zu ersetzen, ohne eine Drittanbieter-Mock-Bibliothek zu verwenden:

import time

def get_timestamp():
    return time.time()

def test_get_timestamp(monkeypatch):
    monkeypatch.setattr(time, "time", lambda: 1_000_000.0)
    assert get_timestamp() == 1_000_000.0

Nach dem Test wird time.time auf seine ursprüngliche Implementierung zurückgesetzt. Lesen Sie Python Decorators, wenn Sie verstehen möchten, wie @pytest.fixture intern funktioniert.

Tests mit conftest.py organisieren

Wenn eine Fixture von Tests in mehreren Dateien benötigt wird, legen Sie sie in conftest.py ab. pytest erkennt conftest.py-Dateien automatisch und macht deren Fixtures für alle Tests im gleichen Verzeichnis und darunter verfügbar — kein Import erforderlich.

project/
├── conftest.py          # shared fixtures live here
├── test_users.py
├── test_orders.py
└── utils/
    ├── conftest.py      # fixtures scoped to this subdirectory
    └── test_helpers.py
# conftest.py
import pytest

@pytest.fixture
def admin_user():
    return {"name": "Admin", "role": "admin", "active": True}
# test_users.py  — no import needed; pytest injects admin_user automatically
def test_admin_is_active(admin_user):
    assert admin_user["active"] is True

Klassenbasierte Tests

Sie können verwandte Tests in einer Klasse gruppieren. Anders als bei unittest.TestCase erfordern pytest-Klassen keine Vererbung:

class TestCalculator:
    def test_add(self):
        assert 2 + 2 == 4

    def test_multiply(self):
        assert 3 * 4 == 12

    def test_subtract(self):
        assert 10 - 3 == 7

Klassen sind nützlich, um Tests zu gruppieren, die einen gemeinsamen logischen Bezug haben. Vermeiden Sie Klassen, wenn die Gruppierung keinen echten Mehrwert bietet — flache Funktionen sind einfacher.

Marks: Überspringen und benutzerdefinierte Labels

Das Mark-System von pytest ermöglicht es, Tests mit Metadaten für selektive Ausführung zu versehen.

Einen Test überspringen

import pytest
import sys

@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
    assert False

@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
def test_linux_feature():
    assert True

Benutzerdefinierte Marks

Registrieren Sie benutzerdefinierte Marks in pytest.ini (oder pyproject.toml), um Tests nach Kategorie zu kennzeichnen:

# pytest.ini
[pytest]
markers =
    slow: marks tests as slow (deselect with -m "not slow")
    integration: marks integration tests
@pytest.mark.slow
def test_large_dataset():
    ...

Nur langsame Tests ausführen:

pytest -m slow

Alles außer langsamen Tests ausführen:

pytest -m "not slow"

Nützliche Befehlszeilenoptionen

pytest                          # run all discovered tests
pytest test_math_utils.py       # run a specific file
pytest test_math_utils.py::test_add  # run one test by name
pytest -v                       # verbose: show each test name
pytest -x                       # stop on first failure
pytest --tb=short               # shorter traceback (default is long)
pytest -k "add"                 # run tests whose name contains "add"
pytest --lf                     # re-run only last-failing tests
pytest -q                       # quiet: minimal output

Test-Coverage

Installieren Sie das Coverage-Plugin, um zu messen, welche Zeilen Ihre Tests abdecken:

pip install pytest-cov
pytest --cov=math_utils --cov-report=term-missing

Die Ausgabe fügt eine Coverage-Spalte hinzu, die zeigt, welche Zeilen nicht getestet wurden:

Name            Stmts   Miss  Cover   Missing
---------------------------------------------
math_utils.py       2      0   100%

Streben Sie eine hohe Coverage für kritische Geschäftslogik an, aber jagen Sie nicht 100% nach — das Testen trivialer Getter fügt oft Rauschen ohne echten Mehrwert hinzu.

Häufige Fallstricke

1. Fixture nicht gefunden. Wenn pytest fixture 'foo' not found meldet, prüfen Sie, ob die Fixture in conftest.py oder in derselben Datei vorhanden ist und ob die Funktion mit @pytest.fixture dekoriert ist.

2. Import-Fehler zur Erfassungszeit. Wenn pytest Ihr Modul nicht importieren kann, tritt ein Fehler auf, bevor ein Test ausgeführt wird. Führen Sie python -c "import your_module" aus, um die Ursache zu diagnostizieren.

3. Veränderliche Standardargumente in Fixtures. Genau wie bei regulären Python-Funktionen sollten Fixtures veränderliche Standardargumente vermeiden. Verwenden Sie den Gültigkeitsbereich "function" (Standard) für jede Fixture, die ein veränderliches Objekt erstellt.

4. assert in Hilfsfunktionen. Wenn Sie einen Helfer aus einem Test aufrufen und dieser Helfer assert enthält, stellen Sie sicher, dass sein Name mit assert_ beginnt (pytest-Konvention), damit pytest die Assertion für eine bessere Fehlermeldung umschreibt.

5. Mischen von unittest.TestCase und pytest-Fixtures. pytest führt unittest.TestCase-Tests aus, aber Sie können keine pytest-Fixtures in TestCase-Methoden injizieren. Verwenden Sie entweder pytest-Klassen oder die unittest-Setup-Methoden — nicht beides gleichzeitig.

Übungen

Übung
Which decorator marks a pytest function as a fixture?
Which decorator marks a pytest function as a fixture?
Übung
What does pytest.approx() help you do in tests?
What does pytest.approx() help you do in tests?
Übung
Where should you put fixtures that need to be shared across multiple test files?
Where should you put fixtures that need to be shared across multiple test files?
Was this page helpful?