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?
| Merkmal | unittest | pytest |
|---|---|---|
| Testsyntax | Klasse + Methode | Einfache Funktion |
| Assertions | self.assertEqual(a, b) | assert a == b |
| Fixtures | setUp / tearDown | @pytest.fixture (kombinierbar) |
| Parametrisierung | Manuelle Schleife | @pytest.mark.parametrize |
| Plugin-Ökosystem | Minimal | 1 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 pytestInstallation überprüfen:
pytest --version
# pytest 8.x.xLesen 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_*.pyoder*_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 + bErstellen 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) == 0Tests ausführen:
pytest test_math_utils.pyAusgabe:
collected 3 items
test_math_utils.py ... [100%]
3 passed in 0.01sJeder 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 differEine Fehlerausgabe sieht so aus:
AssertionError: assert [1, 2, 4] == [1, 2, 3]
At index 2: 4 != 3Gleitkommazahl-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) == expectedpytest 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] PASSEDDies 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() == 1pytest 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ültigkeitsbereich | Einmal 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— einpathlib.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 vonstdout/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.0Nach 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 TrueKlassenbasierte 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 == 7Klassen 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 TrueBenutzerdefinierte 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 slowAlles 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 outputTest-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-missingDie 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.