Zum Inhalt springen
Python Fortgeschritten 3 min

Unit Tests mit pytest

Lerne, wie du mit pytest automatisierte Tests schreibst, um die Qualität deines Python-Codes sicherzustellen.

Aktualisiert:

Unit Tests mit pytest

Du hast eine Funktion geschrieben und sie “funktioniert” — aber tut sie das auch in allen Fällen? Was passiert bei leerer Eingabe? Bei negativen Zahlen? Bei riesigen Datenmengen? Automatisierte Tests geben dir Sicherheit. In diesem Tutorial lernst du, wie du mit pytest professionelle Tests schreibst.

Warum testen?

1. Qualität sicherstellen

# Du denkst, die Funktion funktioniert...
def ist_schaltjahr(jahr):
    return jahr % 4 == 0

# Aber was ist mit 1900? (kein Schaltjahr!)
# Und 2000? (doch ein Schaltjahr!)
# Tests decken solche Fehler auf.

2. Vertrauen beim Ändern

Stell dir vor, du änderst eine Funktion, die an 20 Stellen verwendet wird. Ohne Tests: Angst, etwas kaputtzumachen. Mit Tests: Du änderst den Code und führst die Tests aus — grün bedeutet alles okay!

3. Refactoring ermöglichen

Tests sind wie ein Sicherheitsnetz beim Klettern. Du kannst mutig umbauen, weil du weißt, dass die Tests dich auffangen.

4. Dokumentation durch Tests

Gut geschriebene Tests zeigen, wie eine Funktion verwendet werden soll:

def test_addiere_positive_zahlen():
    assert addiere(2, 3) == 5

def test_addiere_negative_zahlen():
    assert addiere(-1, -1) == -2

def test_addiere_mit_null():
    assert addiere(5, 0) == 5

pytest installieren

# pytest installieren
pip install pytest

# Überprüfen, ob es funktioniert
pytest --version

Erste Tests schreiben

pytest hat zwei einfache Regeln:

  1. Testdateien heißen test_*.py oder *_test.py
  2. Testfunktionen beginnen mit test_

Beispiel: Eine einfache Rechenfunktion testen

Erstelle zuerst die Datei rechner.py:

# rechner.py
def addiere(a, b):
    return a + b

def subtrahiere(a, b):
    return a - b

def multipliziere(a, b):
    return a * b

def dividiere(a, b):
    if b == 0:
        raise ValueError("Division durch Null!")
    return a / b

Dann erstelle test_rechner.py:

# test_rechner.py
from rechner import addiere, subtrahiere, multipliziere, dividiere

def test_addiere():
    assert addiere(2, 3) == 5

def test_addiere_negative():
    assert addiere(-1, -1) == -2

def test_addiere_float():
    assert addiere(0.1, 0.2) == pytest.approx(0.3)

def test_subtrahiere():
    assert subtrahiere(10, 3) == 7

def test_multipliziere():
    assert multipliziere(4, 5) == 20

def test_multipliziere_mit_null():
    assert multipliziere(100, 0) == 0

def test_dividiere():
    assert dividiere(10, 2) == 5.0

def test_dividiere_durch_null():
    import pytest
    with pytest.raises(ValueError):
        dividiere(10, 0)

assert-Anweisungen

assert ist das Herzstück von pytest. Es prüft, ob eine Bedingung wahr ist:

# Gleichheit prüfen
assert ergebnis == erwartet

# Ungleichheit prüfen
assert ergebnis != falsch

# Wahrheitswert prüfen
assert ist_gueltig
assert not ist_leer

# Enthaltensein prüfen
assert "Anna" in namen_liste
assert "xyz" not in namen_liste

# Typ prüfen
assert isinstance(ergebnis, int)

# Größenvergleiche
assert laenge > 0
assert alter >= 18
assert preis <= 100.0

# None prüfen
assert ergebnis is not None
assert fehler is None

Aussagekräftige Fehlermeldungen

pytest zeigt bei fehlgeschlagenen Tests automatisch nützliche Details:

def test_begruessung():
    name = "Anna"
    ergebnis = begruesse(name)
    assert ergebnis == "Hallo Anna!"
    # Bei Fehler zeigt pytest:
    # AssertionError: assert 'Hallo, Anna!' == 'Hallo Anna!'
    #                         ^          ^
    #                         Unterschied wird markiert!

Fließkommazahlen vergleichen

Fließkommazahlen können nie exakt verglichen werden:

import pytest

def test_fliesskomma():
    # FALSCH - kann fehlschlagen!
    # assert 0.1 + 0.2 == 0.3

    # RICHTIG - mit Toleranz vergleichen
    assert 0.1 + 0.2 == pytest.approx(0.3)
    assert 1/3 == pytest.approx(0.333, abs=0.001)

pytest ausführen

# Alle Tests im aktuellen Verzeichnis ausführen
pytest

# Mit ausführlicher Ausgabe
pytest -v

# Nur eine Datei testen
pytest test_rechner.py

# Nur einen bestimmten Test ausführen
pytest test_rechner.py::test_addiere

# Tests mit bestimmtem Namensmuster
pytest -k "addiere"

# Beim ersten Fehler stoppen
pytest -x

# Die letzten fehlgeschlagenen Tests erneut ausführen
pytest --lf

# Ausgabe von print()-Anweisungen anzeigen
pytest -s

Ausgabe verstehen

==================== test session starts ====================
collected 8 items

test_rechner.py ........                                [100%]

==================== 8 passed in 0.03s ====================
# Symbole:
# .  = Test bestanden (passed)
# F  = Test fehlgeschlagen (failed)
# E  = Fehler im Test selbst (error)
# s  = Test übersprungen (skipped)
# x  = Erwarteter Fehler (xfail)

Teststruktur: Arrange-Act-Assert

Das AAA-Muster (Arrange-Act-Assert) hilft dir, Tests klar zu strukturieren:

def test_benutzer_vollstaendiger_name():
    # Arrange (Vorbereiten)
    benutzer = Benutzer(vorname="Max", nachname="Mustermann")

    # Act (Ausführen)
    voller_name = benutzer.voller_name()

    # Assert (Überprüfen)
    assert voller_name == "Max Mustermann"


def test_warenkorb_gesamtpreis():
    # Arrange
    warenkorb = Warenkorb()
    warenkorb.hinzufuegen(Artikel("Buch", 19.99))
    warenkorb.hinzufuegen(Artikel("Stift", 2.50))

    # Act
    gesamt = warenkorb.gesamtpreis()

    # Assert
    assert gesamt == pytest.approx(22.49)


def test_passwort_zu_kurz():
    # Arrange
    validator = PasswortValidator(min_laenge=8)
    passwort = "kurz"

    # Act
    ist_gueltig = validator.pruefen(passwort)

    # Assert
    assert ist_gueltig is False

Parametrisierte Tests

Statt viele ähnliche Tests zu schreiben, nutze @pytest.mark.parametrize:

import pytest

# Ohne Parametrisierung - viel Wiederholung
def test_ist_gerade_2():
    assert ist_gerade(2) is True

def test_ist_gerade_4():
    assert ist_gerade(4) is True

def test_ist_gerade_3():
    assert ist_gerade(3) is False


# Mit Parametrisierung - viel eleganter!
@pytest.mark.parametrize("zahl,erwartet", [
    (2, True),
    (4, True),
    (0, True),
    (-2, True),
    (1, False),
    (3, False),
    (7, False),
])
def test_ist_gerade(zahl, erwartet):
    assert ist_gerade(zahl) is erwartet

Komplexere Parametrisierung

@pytest.mark.parametrize("eingabe,erwartet", [
    ("hallo", "Hallo"),
    ("WELT", "Welt"),
    ("pYtHoN", "Python"),
    ("a", "A"),
    ("", ""),
])
def test_titel_format(eingabe, erwartet):
    assert titel_format(eingabe) == erwartet


# Mit IDs für bessere Ausgabe
@pytest.mark.parametrize("a,b,erwartet", [
    pytest.param(2, 3, 5, id="positiv"),
    pytest.param(-1, -1, -2, id="negativ"),
    pytest.param(0, 0, 0, id="null"),
    pytest.param(100, -50, 50, id="gemischt"),
], ids=str)
def test_addiere(a, b, erwartet):
    assert addiere(a, b) == erwartet

Parametrisierte Tests für Fehlerfälle

@pytest.mark.parametrize("ungueltige_eingabe", [
    "",
    None,
    "   ",
    "ab",  # zu kurz
])
def test_benutzername_ungueltig(ungueltige_eingabe):
    with pytest.raises((ValueError, TypeError)):
        benutzername_validieren(ungueltige_eingabe)

Fixtures

Fixtures bereiten Testdaten vor und räumen danach auf. Sie vermeiden Wiederholung:

import pytest


# --- Einfache Fixture ---

@pytest.fixture
def beispiel_benutzer():
    """Erstellt einen Testbenutzer."""
    return {
        "name": "Anna",
        "email": "anna@example.de",
        "alter": 25,
    }

def test_benutzer_name(beispiel_benutzer):
    assert beispiel_benutzer["name"] == "Anna"

def test_benutzer_ist_erwachsen(beispiel_benutzer):
    assert beispiel_benutzer["alter"] >= 18


# --- Fixture mit Setup und Teardown ---

@pytest.fixture
def temporaere_datei(tmp_path):
    """Erstellt eine temporäre Testdatei."""
    datei = tmp_path / "testdaten.txt"
    datei.write_text("Zeile 1\nZeile 2\nZeile 3\n", encoding="utf-8")
    yield datei    # Test wird hier ausgeführt
    # Aufräumen (optional, tmp_path räumt automatisch auf)
    print(f"Temporäre Datei aufgeräumt: {datei}")

def test_datei_lesen(temporaere_datei):
    inhalt = temporaere_datei.read_text(encoding="utf-8")
    assert "Zeile 1" in inhalt
    assert inhalt.count("\n") == 3


# --- Fixture die andere Fixtures verwendet ---

@pytest.fixture
def benutzer_datenbank(beispiel_benutzer):
    """Erstellt eine Testdatenbank mit einem Benutzer."""
    db = DatenbankMock()
    db.benutzer_hinzufuegen(beispiel_benutzer)
    yield db
    db.schliessen()

Eingebaute Fixtures

pytest bringt nützliche Fixtures mit:

def test_temporaerer_ordner(tmp_path):
    """tmp_path erstellt einen temporären Ordner."""
    datei = tmp_path / "test.txt"
    datei.write_text("Hallo Welt!")
    assert datei.read_text() == "Hallo Welt!"


def test_ausgabe_pruefen(capsys):
    """capsys fängt print()-Ausgaben ab."""
    print("Hallo Welt!")
    aufgefangen = capsys.readouterr()
    assert aufgefangen.out == "Hallo Welt!\n"


def test_umgebungsvariable(monkeypatch):
    """monkeypatch ändert Umgebungsvariablen temporär."""
    monkeypatch.setenv("API_KEY", "test-schluessel-123")
    import os
    assert os.environ["API_KEY"] == "test-schluessel-123"

Fixtures mit Scope

@pytest.fixture(scope="function")    # Standard: Pro Test neu erstellt
def einmal_pro_test():
    return DatenbankVerbindung()

@pytest.fixture(scope="module")      # Einmal pro Testdatei
def einmal_pro_datei():
    return DatenbankVerbindung()

@pytest.fixture(scope="session")     # Einmal pro Testlauf
def einmal_pro_sitzung():
    return DatenbankVerbindung()

Exceptions testen

Mit pytest.raises prüfst du, ob eine bestimmte Exception ausgelöst wird:

import pytest

def test_division_durch_null():
    with pytest.raises(ZeroDivisionError):
        1 / 0

def test_ungueltige_konvertierung():
    with pytest.raises(ValueError):
        int("abc")

# Die Fehlermeldung prüfen
def test_fehlermeldung():
    with pytest.raises(ValueError, match="ungültig"):
        validiere_email("keine-email")

# Auf die Exception zugreifen
def test_exception_details():
    with pytest.raises(ValueError) as exc_info:
        int("abc")

    assert "invalid literal" in str(exc_info.value)
    assert exc_info.type == ValueError

Eigene Exceptions testen

from meine_app import (
    Bankkonto,
    KontoUeberzeichnetError,
    UngueltigerBetragError,
)

def test_abheben_zu_viel():
    konto = Bankkonto(kontostand=100)
    with pytest.raises(KontoUeberzeichnetError) as exc_info:
        konto.abheben(200)

    assert exc_info.value.betrag == 200
    assert exc_info.value.kontostand == 100

def test_abheben_negativer_betrag():
    konto = Bankkonto(kontostand=100)
    with pytest.raises(UngueltigerBetragError):
        konto.abheben(-50)

Test-Organisation

Für größere Projekte organisierst du Tests in einem eigenen Verzeichnis:

mein_projekt/
├── src/
│   ├── __init__.py
│   ├── rechner.py
│   ├── benutzer.py
│   └── datenbank.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py          # Gemeinsame Fixtures
│   ├── test_rechner.py
│   ├── test_benutzer.py
│   └── test_datenbank.py
├── pyproject.toml
└── README.md

conftest.py — Gemeinsame Fixtures

# tests/conftest.py
import pytest

@pytest.fixture
def beispiel_daten():
    """Testdaten, die in allen Testdateien verfügbar sind."""
    return {
        "benutzer": [
            {"name": "Anna", "alter": 25},
            {"name": "Bob", "alter": 30},
            {"name": "Clara", "alter": 17},
        ]
    }

@pytest.fixture
def leere_datenbank():
    """Frische Testdatenbank für jeden Test."""
    db = TestDatenbank()
    db.erstellen()
    yield db
    db.loeschen()

pytest-Konfiguration in pyproject.toml

# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"

Test Coverage

Test Coverage zeigt dir, wie viel deines Codes von Tests abgedeckt wird:

# pytest-cov installieren
pip install pytest-cov

# Tests mit Coverage ausführen
pytest --cov=src

# Detaillierten Report in der Konsole
pytest --cov=src --cov-report=term-missing

# HTML-Report erstellen
pytest --cov=src --cov-report=html
# Öffne dann htmlcov/index.html im Browser

Coverage-Ausgabe verstehen

---------- coverage: ... ----------
Name               Stmts   Miss  Cover   Missing
--------------------------------------------------
src/rechner.py        15      2    87%   23-24
src/benutzer.py       30      5    83%   15, 28-31
src/datenbank.py      45     20    56%   12-25, 38-50
--------------------------------------------------
TOTAL                 90     27    70%
  • Stmts: Gesamtanzahl der Anweisungen
  • Miss: Nicht getestete Anweisungen
  • Cover: Prozentsatz der Abdeckung
  • Missing: Zeilennummern ohne Testabdeckung

Ziel: Mindestens 80% Coverage ist ein guter Richtwert. 100% ist selten nötig oder sinnvoll.

TDD-Grundlagen (Test Driven Development)

TDD dreht den normalen Ablauf um: Erst den Test schreiben, dann den Code!

Der TDD-Zyklus: Red-Green-Refactor

1. RED:      Schreibe einen Test, der fehlschlägt
2. GREEN:    Schreibe den minimalen Code, damit der Test besteht
3. REFACTOR: Verbessere den Code, Tests bleiben grün

TDD in Aktion: FizzBuzz

Schritt 1: RED — Erster Test

# test_fizzbuzz.py
from fizzbuzz import fizzbuzz

def test_normale_zahl():
    assert fizzbuzz(1) == "1"
    assert fizzbuzz(2) == "2"

Test ausführen: FEHLER! fizzbuzz existiert noch nicht.

Schritt 2: GREEN — Minimaler Code

# fizzbuzz.py
def fizzbuzz(n):
    return str(n)

Test ausführen: BESTANDEN!

Schritt 3: RED — Nächster Test

def test_durch_drei_teilbar():
    assert fizzbuzz(3) == "Fizz"
    assert fizzbuzz(6) == "Fizz"
    assert fizzbuzz(9) == "Fizz"

Test ausführen: FEHLER!

Schritt 4: GREEN

def fizzbuzz(n):
    if n % 3 == 0:
        return "Fizz"
    return str(n)

Schritt 5: RED

def test_durch_fuenf_teilbar():
    assert fizzbuzz(5) == "Buzz"
    assert fizzbuzz(10) == "Buzz"

Schritt 6: GREEN

def fizzbuzz(n):
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

Schritt 7: RED

def test_durch_drei_und_fuenf_teilbar():
    assert fizzbuzz(15) == "FizzBuzz"
    assert fizzbuzz(30) == "FizzBuzz"

Schritt 8: GREEN

def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

Schritt 9: REFACTOR

def fizzbuzz(n):
    ergebnis = ""
    if n % 3 == 0:
        ergebnis += "Fizz"
    if n % 5 == 0:
        ergebnis += "Buzz"
    return ergebnis or str(n)

Alle Tests laufen weiter: BESTANDEN!

Praxis: Taschenrechner-Modul testen

Hier ist ein vollständiges Beispiel mit Modul und Tests:

Das Modul

# taschenrechner.py
class Taschenrechner:
    """Ein einfacher Taschenrechner mit Verlauf."""

    def __init__(self):
        self.verlauf = []

    def _speichere(self, ausdruck, ergebnis):
        self.verlauf.append({"ausdruck": ausdruck, "ergebnis": ergebnis})

    def addiere(self, a, b):
        ergebnis = a + b
        self._speichere(f"{a} + {b}", ergebnis)
        return ergebnis

    def subtrahiere(self, a, b):
        ergebnis = a - b
        self._speichere(f"{a} - {b}", ergebnis)
        return ergebnis

    def multipliziere(self, a, b):
        ergebnis = a * b
        self._speichere(f"{a} * {b}", ergebnis)
        return ergebnis

    def dividiere(self, a, b):
        if b == 0:
            raise ValueError("Division durch Null ist nicht erlaubt!")
        ergebnis = a / b
        self._speichere(f"{a} / {b}", ergebnis)
        return ergebnis

    def potenz(self, basis, exponent):
        if not isinstance(exponent, (int, float)):
            raise TypeError("Exponent muss eine Zahl sein!")
        ergebnis = basis ** exponent
        self._speichere(f"{basis} ^ {exponent}", ergebnis)
        return ergebnis

    def verlauf_loeschen(self):
        self.verlauf.clear()

    def letztes_ergebnis(self):
        if not self.verlauf:
            return None
        return self.verlauf[-1]["ergebnis"]

Die Tests

# test_taschenrechner.py
import pytest
from taschenrechner import Taschenrechner


# --- Fixture ---

@pytest.fixture
def rechner():
    """Frischer Taschenrechner für jeden Test."""
    return Taschenrechner()


# --- Grundoperationen ---

class TestAddition:
    def test_positive_zahlen(self, rechner):
        assert rechner.addiere(2, 3) == 5

    def test_negative_zahlen(self, rechner):
        assert rechner.addiere(-1, -1) == -2

    def test_mit_null(self, rechner):
        assert rechner.addiere(5, 0) == 5

    @pytest.mark.parametrize("a,b,erwartet", [
        (1, 1, 2),
        (100, 200, 300),
        (-5, 5, 0),
        (0.1, 0.2, pytest.approx(0.3)),
    ])
    def test_verschiedene_werte(self, rechner, a, b, erwartet):
        assert rechner.addiere(a, b) == erwartet


class TestSubtraktion:
    def test_positives_ergebnis(self, rechner):
        assert rechner.subtrahiere(10, 3) == 7

    def test_negatives_ergebnis(self, rechner):
        assert rechner.subtrahiere(3, 10) == -7


class TestMultiplikation:
    def test_grundfall(self, rechner):
        assert rechner.multipliziere(4, 5) == 20

    def test_mit_null(self, rechner):
        assert rechner.multipliziere(100, 0) == 0

    def test_mit_negativer_zahl(self, rechner):
        assert rechner.multipliziere(-3, 4) == -12


class TestDivision:
    def test_ganzzahl_ergebnis(self, rechner):
        assert rechner.dividiere(10, 2) == 5.0

    def test_dezimal_ergebnis(self, rechner):
        assert rechner.dividiere(7, 2) == 3.5

    def test_durch_null(self, rechner):
        with pytest.raises(ValueError, match="Division durch Null"):
            rechner.dividiere(10, 0)


class TestPotenz:
    def test_quadrat(self, rechner):
        assert rechner.potenz(3, 2) == 9

    def test_hoch_null(self, rechner):
        assert rechner.potenz(5, 0) == 1

    def test_negativer_exponent(self, rechner):
        assert rechner.potenz(2, -1) == 0.5

    def test_ungueltiger_exponent(self, rechner):
        with pytest.raises(TypeError):
            rechner.potenz(2, "zwei")


# --- Verlauf ---

class TestVerlauf:
    def test_leerer_verlauf(self, rechner):
        assert rechner.verlauf == []

    def test_verlauf_nach_berechnung(self, rechner):
        rechner.addiere(2, 3)
        assert len(rechner.verlauf) == 1
        assert rechner.verlauf[0]["ergebnis"] == 5

    def test_mehrere_berechnungen(self, rechner):
        rechner.addiere(1, 1)
        rechner.multipliziere(3, 3)
        rechner.subtrahiere(10, 5)
        assert len(rechner.verlauf) == 3

    def test_letztes_ergebnis(self, rechner):
        rechner.addiere(2, 3)
        rechner.multipliziere(4, 5)
        assert rechner.letztes_ergebnis() == 20

    def test_letztes_ergebnis_leer(self, rechner):
        assert rechner.letztes_ergebnis() is None

    def test_verlauf_loeschen(self, rechner):
        rechner.addiere(1, 1)
        rechner.multipliziere(2, 2)
        rechner.verlauf_loeschen()
        assert rechner.verlauf == []
        assert rechner.letztes_ergebnis() is None

Tests ausführen

# Alle Tests ausführen
pytest test_taschenrechner.py -v

# Nur Division-Tests
pytest test_taschenrechner.py::TestDivision -v

# Nur den test_durch_null Test
pytest test_taschenrechner.py::TestDivision::test_durch_null -v

# Mit Coverage
pytest test_taschenrechner.py --cov=taschenrechner -v

Übungen

Übung 1: Passwort-Validator testen

Schreibe Tests für diesen Passwort-Validator:

# passwort.py
def passwort_pruefen(passwort):
    """
    Prüft ein Passwort nach folgenden Regeln:
    - Mindestens 8 Zeichen
    - Mindestens ein Großbuchstabe
    - Mindestens ein Kleinbuchstabe
    - Mindestens eine Zahl
    Gibt True zurück wenn gültig, sonst ValueError mit Beschreibung.
    """
    if len(passwort) < 8:
        raise ValueError("Passwort muss mindestens 8 Zeichen haben")
    if not any(c.isupper() for c in passwort):
        raise ValueError("Passwort braucht mindestens einen Großbuchstaben")
    if not any(c.islower() for c in passwort):
        raise ValueError("Passwort braucht mindestens einen Kleinbuchstaben")
    if not any(c.isdigit() for c in passwort):
        raise ValueError("Passwort braucht mindestens eine Zahl")
    return True
# test_passwort.py
"""
Aufgabe: Schreibe Tests für passwort_pruefen()
- Teste gültige Passwörter
- Teste zu kurze Passwörter
- Teste fehlende Großbuchstaben
- Teste fehlende Kleinbuchstaben
- Teste fehlende Zahlen
- Verwende parametrisierte Tests
- Prüfe die Fehlermeldungen
"""

# Dein Code hier!

Übung 2: Einkaufsliste testen

Schreibe eine Einkaufsliste-Klasse mit TDD (erst die Tests!):

"""
Aufgabe: Schreibe erst die Tests, dann die Klasse!

Die Klasse Einkaufsliste soll folgendes können:
- artikel_hinzufuegen(name, menge, preis)
- artikel_entfernen(name)
- gesamtpreis() -> float
- anzahl_artikel() -> int
- ist_leer() -> bool
- artikel_vorhanden(name) -> bool

Spezielle Regeln:
- Artikel mit Menge <= 0 sollen einen ValueError auslösen
- Artikel mit negativem Preis sollen einen ValueError auslösen
- Entfernen eines nicht vorhandenen Artikels soll KeyError auslösen
"""

# Dein Code hier!

Übung 3: Datei-Verarbeitung testen

Teste eine Funktion, die CSV-Daten verarbeitet (nutze tmp_path):

"""
Aufgabe:
1. Schreibe eine Funktion csv_lesen(dateipfad), die eine CSV-Datei
   liest und eine Liste von Dictionaries zurückgibt
2. Schreibe Tests mit tmp_path Fixture:
   - Teste das Lesen einer gültigen CSV
   - Teste leere Dateien
   - Teste nicht existierende Dateien (FileNotFoundError)
   - Teste ungültige CSV-Daten
"""

# Dein Code hier!

Pro-Tipp: Nutze die “Watch”-Funktion von pytest, um Tests automatisch bei Dateiänderungen auszuführen. Installiere dazu pytest-watch:

pip install pytest-watch

# Tests laufen automatisch bei jeder Speicherung
ptw

So bekommst du sofortiges Feedback, ob dein Code noch funktioniert — besonders nützlich beim TDD-Workflow. Kombiniere das mit Coverage-Reports, um immer den Überblick zu behalten:

ptw -- --cov=src --cov-report=term-missing
Zurück zum Python Kurs