- 1 Section
- 10 Lessons
- unbegrenzt
- Python Fortgeschritten10
- 1.1Module und Pakete: import, pip, venv
- 1.2Objektorientierung in Python: Klassen, __init__, self
- 1.3Vererbung und Mehrfachvererbung in Python
- 1.4Decorators und Generatoren
- 1.5Fehlerbehandlung vertieft: eigene Exceptions
- 1.6Reguläre Ausdrücke mit re
- 1.7Arbeiten mit APIs: requests, JSON
- 1.8Automatisierung mit os, subprocess, pathlib
- 1.9Unit-Tests in Python: pytest
- 1.10Praxisprojekt: Automatisierungsskript mit Tests
Unit-Tests in Python: pytest
Tests sind die Versicherung gegen das, was du in sechs Monaten vergisst – und gegen Änderungen, die nebenan etwas kaputt machen. Unit-Tests prüfen einzelne Funktionen und Klassen, isoliert vom Rest. Wenn etwas schiefgeht, weißt du sofort wo. In Python ist pytest der De-facto-Standard – schlanker als die alte unittest-Klassen-Welt, viel mächtiger und sehr lesbar.
Diese Lektion zeigt, wie du Tests mit pytest schreibst und laufen lässt: einfache Assertions, Fixtures für Setup, Parametrisierung für viele Test-Fälle, das Testen von Exceptions, Mocking externer Abhängigkeiten und ein paar Tipps für nachhaltige Test-Suiten.
1) Warum testen?
Drei Gründe, weshalb Tests sich lohnen:
- Vertrauen beim Ändern: refaktorieren ohne Angst, dass etwas Verstecktes zerbricht
- Spezifikation: Tests beschreiben, wie sich der Code verhalten soll – besser als Kommentare
- Schnelles Feedback: ein Fehler wird in Sekunden statt nach Tagen im Produktivbetrieb gefunden
Eine klassische Verteilung der Test-Arten:
2) pytest installieren
pytest ist kein Standardlib-Modul. In deiner virtuellen Umgebung (siehe Module und Pakete: import, pip, venv):
pip install pytest
# Version prüfen
pytest --version
3) Der erste Test
Ein Test in pytest ist eine ganz normale Funktion, deren Name mit test_ beginnt. Sie nutzt das eingebaute assert-Keyword von Python:
# Datei: rechner.py def addiere(a, b): return a + b def teile(a, b): if b == 0: raise ValueError("Division durch Null") return a / b
# Datei: test_rechner.py from rechner import addiere, teile def test_addiere_positive(): assert addiere(2, 3) == 5 def test_addiere_negative(): assert addiere(-1, -1) == -2 def test_addiere_null(): assert addiere(0, 5) == 5
Tests werden in Dateien mit dem Namen test_*.py (oder *_test.py) gesammelt. Innerhalb der Datei muss jede Test-Funktion mit test_ beginnen.
4) Tests laufen lassen
Im Projekt-Verzeichnis einfach pytest aufrufen – das Tool findet automatisch alle Testdateien:
Bei einem Fehler bekommst du eine genaue Fehleranalyse mit Werten:
Praktische Optionen:
pytest # alle Tests pytest test_rechner.py # nur eine Datei pytest -k addiere # nur Tests mit "addiere" im Namen pytest -v # verbose – jeden Test einzeln auflisten pytest -x # beim ersten Fehler abbrechen pytest --tb=short # kürzere Tracebacks pytest -s # print-Ausgaben zeigen (sonst unterdrückt) pytest --lf # nur die zuletzt fehlgeschlagenen
5) Anatomie eines Tests: AAA
Ein guter Test folgt dem AAA-Schema – Arrange, Act, Assert:
kunde = Kunde("Anna", 100)ergebnis = kunde.einzahlen(50)assert kunde.saldo == 150def test_einzahlen_erhoeht_saldo(): # Arrange kunde = Konto("Anna", 100) # Act kunde.einzahlen(50) # Assert assert kunde.saldo == 150
Ein Test sollte genau eine Sache prüfen. Der Test-Name beschreibt das Verhalten klar – nicht test_konto, sondern test_einzahlen_erhoeht_saldo.
6) Mehr als ==
Python's assert kann viel mehr als nur Gleichheit prüfen:
assert ergebnis == 5 # Gleichheit assert ergebnis != 0 # Ungleichheit assert ergebnis > 10 # Vergleich assert name in ["Anna", "Ben"] # Mitgliedschaft assert "hello" in text # Teilstring assert daten is None # Identität assert isinstance(x, Kunde) # Typ assert len(liste) == 3 # Länge assert not ergebnis # falsy # Mit Fehlermeldung assert a == b, f"{a} stimmt nicht mit {b} überein"
pytest hat darüber hinaus „rich assertions": bei Listen, Dicts und Strings zeigt es die genauen Unterschiede an – kein eigener Code für „assertEqual" usw. nötig.
7) Exceptions testen
Manchmal willst du prüfen, ob eine Funktion eine Exception wirft. Dafür pytest.raises als Context Manager:
import pytest def test_teile_durch_null_wirft(): with pytest.raises(ValueError): teile(10, 0) # Mit Prüfung der Nachricht def test_teile_fehlermeldung(): with pytest.raises(ValueError, match="Division durch Null"): teile(10, 0) # Auf das Exception-Objekt zugreifen def test_teile_details(): with pytest.raises(ValueError) as info: teile(10, 0) assert "Division" in str(info.value)
Der Test schlägt fehl, wenn die Funktion keine Exception (oder den falschen Typ) wirft. Mehr zu Exceptions in Fehlerbehandlung vertieft in Python.
8) Fixtures – wiederverwendbares Setup
Wenn mehrere Tests dieselben Daten oder Objekte brauchen, definiert man eine Fixture. Das ist eine Funktion mit dem Decorator @pytest.fixture, die ihren Rückgabewert an Tests übergibt:
import pytest @pytest.fixture def konto_anna(): return Konto("Anna", startbetrag=100) def test_einzahlen(konto_anna): konto_anna.einzahlen(50) assert konto_anna.saldo == 150 def test_abheben(konto_anna): konto_anna.abheben(30) assert konto_anna.saldo == 70
pytest erkennt am Parameter-Namen konto_anna, dass die Fixture gemeint ist – Magie ohne explizite Verbindung. Jeder Test bekommt ein frisches Konto, sodass sie sich nicht gegenseitig beeinflussen.
9) Fixtures mit Cleanup
Fixtures können auch Setup und Teardown machen – wie ein Context Manager. Mit yield statt return:
import pytest from pathlib import Path @pytest.fixture def temp_datei(tmp_path): pfad = tmp_path / "test.txt" pfad.write_text("Inhalt") yield pfad # der Test bekommt diesen Pfad # nach dem Test: Cleanup (z. B. Aufräumen) print("Datei wurde benutzt: ", pfad)
tmp_path ist übrigens eine eingebaute Fixture von pytest – sie liefert ein temporäres Verzeichnis, das nach dem Test automatisch gelöscht wird. Sehr praktisch für Tests mit Dateioperationen.
10) Eingebaute Fixtures
captured = capsys.readouterr()logging-Moduls.Beispiel mit capsys:
def test_gruss_druckt_hallo(capsys): gruesse("Anna") captured = capsys.readouterr() assert captured.out == "Hallo Anna\n"
11) Parametrisierung – ein Test, viele Daten
Statt fünf fast identische Tests zu schreiben, parametrisierst du einen einzigen:
import pytest @pytest.mark.parametrize("a, b, erwartet", [ (2, 3, 5), (-1, 1, 0), (0, 0, 0), (100, 200, 300), (1.5, 2.5, 4.0), ]) def test_addiere(a, b, erwartet): assert addiere(a, b) == erwartet
pytest läuft die Funktion fünfmal mit verschiedenen Werten – im Output erscheinen die Fälle einzeln, fehlgeschlagene siehst du direkt. Bei Strings:
@pytest.mark.parametrize("input, erwartet", [ ("hello", "HELLO"), ("", ""), ("Mixed Case", "MIXED CASE"), ]) def test_upper(input, erwartet): assert input.upper() == erwartet
Parametrisierung macht Test-Suiten dicht und expressiv – ohne Copy-Paste.
12) Marker
Du kannst Tests mit Markern beschriften – etwa „langsam", „nur Linux" oder „skip diesen":
@pytest.mark.slow def test_grosse_berechnung(): # dauert lange ... assert berechne() == True @pytest.mark.skip(reason="noch nicht implementiert") def test_zukuenftiges_feature(): ... @pytest.mark.skipif(sys.platform == "win32", reason="nur Linux") def test_linux_spezifisch(): ... @pytest.mark.xfail(reason="bekannter Bug, Fix in Arbeit") def test_known_bug(): assert funktion_mit_bug() == True
Aufrufen mit Filter:
pytest -m slow # nur slow-Tests pytest -m "not slow" # alles AUSSER slow
13) Mocking mit unittest.mock
Wenn dein Code externe Dienste anspricht (HTTP-API, Datenbank, Dateisystem), willst du das im Test nicht wirklich tun. Du mockst die Abhängigkeit:
from unittest.mock import patch, MagicMock def test_wetter_ruft_api_auf(): with patch("meine_app.requests.get") as mock_get: mock_get.return_value.json.return_value = {"temp": 22} mock_get.return_value.status_code = 200 ergebnis = wetter_holen("Berlin") assert ergebnis == 22 mock_get.assert_called_once_with( "https://api.example.de/wetter", params={"stadt": "Berlin"} )
patch ersetzt während des Tests die echte requests.get durch ein Mock-Objekt. Du kontrollierst, was zurückkommt – und prüfst, dass die Funktion korrekt aufgerufen wurde. So testest du Logik ohne echte Netzwerk-Aufrufe. Mehr zu API-Aufrufen in Arbeiten mit APIs: requests, JSON.
14) Projekt-Struktur und conftest.py
Empfohlenes Layout für ein Python-Projekt mit Tests:
mein_projekt/ ├── pyproject.toml # oder setup.cfg ├── src/ │ └── mein_paket/ │ ├── __init__.py │ └── rechner.py └── tests/ ├── conftest.py # gemeinsame Fixtures ├── test_rechner.py └── test_andere.py
Die Datei conftest.py ist speziell: hier definierst du Fixtures, die in allen Tests des Verzeichnisses verfügbar sind – ohne Import. pytest findet sie automatisch:
# tests/conftest.py import pytest @pytest.fixture def test_kunde(): return Kunde("Anna", email="a@b.de")
Konfiguration für pytest kommt typischerweise in pyproject.toml:
[tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] markers = [ "slow: marks tests as slow", "integration: integration tests", ]
15) Coverage – wie viel ist getestet?
Mit dem Plugin pytest-cov misst du, welche Code-Zeilen von Tests berührt werden:
pip install pytest-cov pytest --cov=mein_paket --cov-report=term-missing
Output:
Coverage ist nützlich als Indikator – aber kein Selbstzweck. 100 % Coverage heißt nicht, dass der Code richtig ist. Wichtig sind die Fälle, die getestet werden, nicht nur die Zeilen, die ausgeführt werden.
16) Häufige Fehler
- Tests rufen externe Dienste auf: langsam, fragil, kein Internet → Test schlägt fehl. Mocken!
- Tests hängen voneinander ab: Test A erzeugt Daten, Test B nutzt sie. Reihenfolge ändert sich → Chaos. Jeder Test muss eigenständig laufen können
- Tests testen Implementierung statt Verhalten:
assert obj._internal_counter == 5bricht bei jedem Refactoring. Lieber das öffentliche Verhalten prüfen - Globaler Zustand zwischen Tests: Klassen-Attribute, Singletons, Umgebungsvariablen. Mit
monkeypatchsicher isolieren - Tests ohne Assert: laufen ohne Fehler – aber prüfen nichts. Mindestens ein
assertpro Test - Test-Name nichts-sagend:
test_1,test_funktion. Beim Lesen unklar. Liebertest_einzahlen_negativ_wirft_value_error - Magic numbers im Test:
assert ergebnis == 42– warum 42? Benannte Konstanten oder Kommentare helfen - Zu lange Tests: 50-Zeilen-Test prüft zehn Dinge. Aufteilen in mehrere kurze Tests
Zusammenfassung
pytest ist das verbreitete Test-Framework für Python: Tests sind Funktionen mit Namen test_* in Dateien test_*.py, Aussagen mit dem normalen assert. Lauf mit pytest; mit -v, -x, -k, --tb=short kontrollierst du das Verhalten. AAA-Muster: Arrange, Act, Assert. Exceptions testen mit pytest.raises(...). Fixtures (Decorator @pytest.fixture) liefern Setup-Daten an Tests; mit yield für Cleanup. Eingebaut: tmp_path, capsys, monkeypatch. Parametrize (@pytest.mark.parametrize) lässt einen Test mit vielen Daten laufen. Marker: skip, skipif, xfail, eigene. Mocking mit unittest.mock.patch für externe Abhängigkeiten. conftest.py für gemeinsame Fixtures. pytest-cov misst Coverage.
