Home

Anhang 3: Pytest Assertion Referenz - Beyond the Basics

testing pytest assertions best-practices reference

Einführung: Ihr Testing-Toolkit

Sie schreiben seit Wochen Tests. Sie kennen die Grundlagen: assert, pytest.approx(), pytest.raises(). Aber während Sie komplexere Tests schreiben, bemerken Sie Muster. Ihre Tests werden unübersichtlich. LLM-generierte Tests sehen nicht ganz richtig aus. Sie sind sich nicht sicher, wie man Warnings testet oder Exception-Tests spezifischer macht.

Dieser Anhang ist Ihr Referenzleitfaden. Betrachten Sie ihn als die pytest-Dokumentation, aber fokussiert auf die Muster, die Sie tatsächlich in der Praxis verwenden werden.

Was dieser Anhang behandelt

Dies ist KEIN Tutorial. Dies ist eine praktische Referenz für Assertion-Muster, organisiert nach Anwendungsfall:

  1. Advanced Exception Testing - Über das grundlegende pytest.raises() hinaus
  2. pytest.approx Deep Dive - Fließkomma-Vergleiche für komplexe Datenstrukturen
  3. Warning Testing - Testen von Deprecation Warnings und User Warnings
  4. Test Control - Tests überspringen, fehlschlagen lassen und markieren
  5. Assertion Introspection - pytest’s Assertion-Rewriting-Magie verstehen
  6. Parametrization Patterns - Mehrere Szenarien effizient testen
  7. Common Anti-Patterns - Was man NICHT tun sollte (besonders häufig in LLM-generierten Tests)
  8. Quick Reference - Cheat Sheets und Nachschlagetabellen

Was Sie bereits wissen

Aus Kapitel 03 (Grundlagen des Testens) kennen Sie bereits:

Dieser Anhang baut auf dieser Grundlage mit erweiterten Mustern und Best Practices auf.

Wie Sie diesen Anhang verwenden

Beim Schreiben von Tests:

Beim Review von LLM-generierten Tests:

Beim Debuggen fehlgeschlagener Tests:

Wichtige Referenzen

In diesem Anhang verweisen wir auf:

Los geht’s.


Advanced Exception Testing

Sie kennen bereits die Grundlagen von pytest.raises():

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

Aber was, wenn Sie die Exception-Nachricht verifizieren möchten? Oder auf das Exception-Objekt für detaillierte Assertions zugreifen? Oder Code testen, der mehrere Exceptions auslöst?

Der match-Parameter: Exception-Nachrichten validieren

Die häufigste Verbesserung von pytest.raises() ist der match-Parameter, der die Exception-Nachricht mithilfe eines regulären Ausdrucks validiert.

Grundlegende Syntax:

with pytest.raises(ValueError, match=r"must be positive"):
    validate_input(-5)

Warum match verwenden?

  1. Spezifität - Stellt sicher, dass Sie die RICHTIGE Exception aus dem RICHTIGEN Grund auslösen
  2. Regressionsprävention - Fängt Änderungen an Fehlermeldungen ab
  3. Dokumentation - Macht Testabsicht klarer

Beispiel: Eingabevalidierung testen

# road_profile_viewer/filters.py
from numpy.typing import NDArray
import numpy as np

def apply_lowpass_filter(data: NDArray[np.float64], cutoff_freq: float) -> NDArray[np.float64]:
    """Tiefpassfilter auf Straßenprofil-Daten anwenden.

    Args:
        data: Eingabe-Straßenprofildaten
        cutoff_freq: Grenzfrequenz in Hz (muss positiv sein)

    Raises:
        ValueError: Wenn cutoff_freq nicht positiv ist
    """
    if cutoff_freq <= 0:
        raise ValueError(f"Cutoff frequency must be positive, got {cutoff_freq}")
    # ... Filter-Implementierung

❌ Schlechter Test (keine Nachrichtenvalidierung):

def test_lowpass_filter_negative_cutoff():
    data = np.array([1.0, 2.0, 3.0])
    with pytest.raises(ValueError):  # Zu vage!
        apply_lowpass_filter(data, cutoff_freq=-10.0)

Dieser Test würde auch bestehen, wenn der Code ValueError("Invalid data") statt des Grenzfrequenz-Fehlers auslöst.

✅ Guter Test (validiert Nachricht):

def test_lowpass_filter_negative_cutoff():
    # Arrange
    data = np.array([1.0, 2.0, 3.0])
    invalid_cutoff = -10.0

    # Act & Assert
    with pytest.raises(ValueError, match=r"Cutoff frequency must be positive"):
        apply_lowpass_filter(data, cutoff_freq=invalid_cutoff)

Pro-Tipps für den match-Parameter:

  1. Verwenden Sie Raw-Strings - match=r"pattern", um Backslash-Escaping zu vermeiden
  2. match verwendet re.search() - Pattern kann überall in der Nachricht erscheinen (kein vollständiger String-Match)
  3. Spezielle Regex-Zeichen escapen - Verwenden Sie re.escape() für Literal-Strings:
# Wenn Fehlermeldung Klammern, Brackets oder Sonderzeichen enthält
import re

expected_msg = "Invalid range: [0, 100]"
with pytest.raises(ValueError, match=re.escape(expected_msg)):
    validate_range(200)
  1. Dynamische Teile mit Regex-Gruppen testen - Für Nachrichten mit dynamischen Werten:
# Nachricht: "Cutoff frequency must be positive, got -10.0"
with pytest.raises(ValueError, match=r"must be positive, got -?\d+\.?\d*"):
    apply_lowpass_filter(data, cutoff_freq=-10.0)

Referenz: pytest.raises() Dokumentation

Exception-Details zugreifen: ExceptionInfo

Manchmal müssen Sie das Exception-Objekt selbst inspizieren - nicht nur seine Nachricht. Verwenden Sie pytest.raises() als Context Manager und greifen Sie auf die Exception über .value zu:

Syntax:

with pytest.raises(ValueError) as exc_info:
    risky_operation()

# Exception-Details zugreifen
assert exc_info.type is ValueError
assert exc_info.value.args[0] == "Expected message"
assert "partial message" in str(exc_info.value)

Beispiel: Exception-Attribute testen

# road_profile_viewer/exceptions.py
class ProfileDataError(Exception):
    """Custom Exception für Profildatenfehler."""

    def __init__(self, message: str, data_length: int, expected_length: int):
        super().__init__(message)
        self.data_length = data_length
        self.expected_length = expected_length
def test_profile_data_error_attributes():
    # Arrange
    invalid_data = np.array([1.0, 2.0])  # Zu kurz
    expected_length = 100

    # Act & Assert
    with pytest.raises(ProfileDataError) as exc_info:
        load_profile_data(invalid_data, expected_length=expected_length)

    # Custom Attribute zugreifen
    assert exc_info.value.data_length == 2
    assert exc_info.value.expected_length == 100
    assert "length" in str(exc_info.value).lower()

ExceptionInfo-Attribute:

Wann ExceptionInfo verwenden:

Referenz: ExceptionInfo API

Exception Groups testen (Python 3.11+)

Python 3.11 führte Exception Groups ein - Exceptions, die mehrere Exceptions bündeln. Verwenden Sie pytest.raises() mit ExceptionGroup oder den Helper pytest.raises_group():

Beispiel: Gleichzeitige Operationen testen

def test_multiple_validation_errors():
    """Testen, dass alle Validierungsfehler zusammen gemeldet werden."""
    invalid_config = {
        "cutoff_freq": -10.0,      # Ungültig (negativ)
        "sample_rate": 0,          # Ungültig (null)
        "window_size": "invalid"   # Ungültig (falscher Typ)
    }

    # Python 3.11+ Syntax
    with pytest.raises(ExceptionGroup) as exc_info:
        validate_filter_config(invalid_config)

    # Prüfen, dass Gruppe erwartete Exceptions enthält
    exceptions = exc_info.value.exceptions
    assert len(exceptions) == 3
    assert any(isinstance(e, ValueError) and "cutoff" in str(e) for e in exceptions)
    assert any(isinstance(e, ValueError) and "sample_rate" in str(e) for e in exceptions)
    assert any(isinstance(e, TypeError) and "window_size" in str(e) for e in exceptions)

Hinweis: Exception Groups sind erweiterte Python 3.11+ Features. Für diesen Kurs konzentrieren Sie sich auf Standard-Exception-Testing-Muster oben.

Referenz: PEP 654 - Exception Groups

Wann pytest.raises() NICHT verwenden

Anti-Pattern: Testen, dass Code NICHT auslöst

# ❌ FALSCH - Sinnloser Test
def test_division_does_not_raise():
    with pytest.raises(ZeroDivisionError):
        pass  # Dies wird fehlschlagen, weil nichts auslöst!

    # ... was haben wir getestet?

✅ KORREKT - Rufen Sie einfach die Funktion auf

def test_division_with_nonzero_divisor():
    # Arrange
    numerator = 10.0
    denominator = 2.0

    # Act
    result = numerator / denominator

    # Assert
    assert result == 5.0  # Wenn dies läuft, wurde keine Exception ausgelöst!

Wenn ein Test ohne Exception-Auslösung abgeschlossen wird, besteht er. Sie müssen nicht explizit testen, dass Exceptions NICHT auftreten.

Schnellreferenz: Exception Testing

Muster Syntax Anwendungsfall
Einfache Exception with pytest.raises(ValueError): Testen, dass Code spezifischen Exception-Typ auslöst
Exception-Nachricht with pytest.raises(ValueError, match=r"pattern"): Exception-Nachricht gegen Regex validieren
Exception-Details with pytest.raises(ValueError) as exc_info: Auf Exception-Objekt für detaillierte Assertions zugreifen
Literal-Nachricht match=re.escape("literal [text]") Exakte Nachricht mit Sonderzeichen matchen
Exception Groups with pytest.raises(ExceptionGroup): Gebündelte Exceptions testen (Python 3.11+)

pytest.approx Deep Dive

Sie verwenden bereits pytest.approx() für Fließkomma-Vergleiche:

assert result == pytest.approx(expected, rel=1e-9)

Aber pytest.approx() ist mächtiger als Sie denken. Es funktioniert mit Sequenzen, Dictionaries, NumPy-Arrays und sogar verschachtelten Strukturen.

Wie pytest.approx funktioniert: Relative vs. Absolute Toleranz

Standard-Toleranzen:

Vergleichsregel: Ein Wert wird als gleich betrachtet, wenn er EINE Toleranz erfüllt:

\[ |\text{actual} - \text{expected}| \leq \max(\text{rel} \times \text{expected}, \text{abs}) \]

Beispiel:

import pytest

# Für expected = 1000.0, rel=1e-6, abs=1e-12
# Toleranz = max(1e-6 * 1000.0, 1e-12) = 0.001
assert 1000.001 == pytest.approx(1000.0)  # Innerhalb 0.001
assert 999.999 == pytest.approx(1000.0)   # Innerhalb 0.001
assert 1000.002 != pytest.approx(1000.0)  # Außerhalb 0.001

Warum zwei Toleranzen?

Beispiel: Werte nahe null

# Für expected = 0.0 würde rel=1e-6 0 Toleranz ergeben!
# Daher brauchen wir abs=1e-12
assert 1e-13 == pytest.approx(0.0)  # Verwendet absolute Toleranz
assert 1e-11 != pytest.approx(0.0)  # Außerhalb absoluter Toleranz

Toleranzen wählen:

Aus Kapitel 03 (Grenzwertanalyse) haben Sie über math.ulp() gelernt - die Einheit an letzter Stelle:

import math

# Für Hochpräzisionsanforderungen, ULP-basierte Toleranz verwenden
x = 1.0
tolerance = 10 * math.ulp(x)  # 10 ULPs = 10 * 2.220446049250313e-16

assert result == pytest.approx(expected, abs=tolerance)

Referenz: pytest.approx Dokumentation

pytest.approx mit Sequenzen (Listen, Tupel)

pytest.approx() funktioniert elementweise mit Sequenzen:

import pytest

# Listen
assert [0.1 + 0.2, 0.2 + 0.4] == pytest.approx([0.3, 0.6])

# Tupel
assert (0.1 + 0.2, 0.2 + 0.4) == pytest.approx((0.3, 0.6))

# Gemischt (aber muss gleicher Typ auf beiden Seiten sein!)
result_list = [1.0000001, 2.0000001, 3.0000001]
expected_list = [1.0, 2.0, 3.0]
assert result_list == pytest.approx(expected_list)

Beispiel: Filter-Ausgabewerte testen

def test_lowpass_filter_output_values():
    # Arrange
    input_data = np.array([1.0, 2.0, 3.0, 2.0, 1.0])
    cutoff_freq = 1.0
    sample_rate = 10.0

    # Erwartete Ausgabe (vorberechnet oder von Referenzimplementierung)
    expected_output = [0.98, 1.95, 2.89, 2.05, 1.02]

    # Act
    result = apply_lowpass_filter(input_data, cutoff_freq, sample_rate)

    # Assert - als Liste vergleichen
    assert result.tolist() == pytest.approx(expected_output, rel=1e-2)

Wichtige Hinweise:

  1. Längen müssen übereinstimmen - pytest.approx schlägt fehl, wenn Sequenzen unterschiedliche Längen haben
  2. Typen müssen übereinstimmen - Kann Liste nicht mit Tupel vergleichen (erst konvertieren)
  3. Gilt für jedes Element - Jedes Element mit gleicher Toleranz geprüft

pytest.approx mit Dictionaries

pytest.approx() vergleicht Dictionary-Werte elementweise:

import pytest

result_dict = {
    "mean": 0.1 + 0.2,
    "std": 0.2 + 0.4,
    "max": 1.0000001
}

expected_dict = {
    "mean": 0.3,
    "std": 0.6,
    "max": 1.0
}

assert result_dict == pytest.approx(expected_dict)

Beispiel: Statistische Zusammenfassung testen

def test_profile_statistics():
    # Arrange
    profile_data = np.array([0.5, 1.0, 1.5, 2.0, 2.5])

    # Act
    stats = compute_profile_statistics(profile_data)

    # Assert - als Dictionary vergleichen
    expected_stats = {
        "mean": 1.5,
        "median": 1.5,
        "std": 0.70710678,  # sqrt(0.5)
        "min": 0.5,
        "max": 2.5
    }

    assert stats == pytest.approx(expected_stats, rel=1e-6)

Wichtige Hinweise:

  1. Schlüssel müssen exakt übereinstimmen - pytest.approx prüft keine Schlüssel, nur Werte
  2. Nur numerische Werte verglichen - Nicht-numerische Werte mit == geprüft
  3. Verschachtelte Dictionaries erfordern spezielle Behandlung (siehe unten)

pytest.approx mit NumPy-Arrays

pytest.approx() funktioniert mit NumPy-Arrays (häufigster Anwendungsfall in diesem Kurs):

import numpy as np
import pytest

# 1D-Arrays
result = np.array([0.1 + 0.2, 0.2 + 0.4, 0.3 + 0.6])
expected = np.array([0.3, 0.6, 0.9])
assert result == pytest.approx(expected)

# 2D-Arrays
result_2d = np.array([[1.0000001, 2.0000001],
                      [3.0000001, 4.0000001]])
expected_2d = np.array([[1.0, 2.0],
                        [3.0, 4.0]])
assert result_2d == pytest.approx(expected_2d)

Beispiel: FFT-Ausgabe testen

def test_fft_output_magnitudes():
    # Arrange
    signal = np.sin(2 * np.pi * 5.0 * np.linspace(0, 1, 100))  # 5 Hz Sinuswelle

    # Act
    fft_result = np.fft.fft(signal)
    magnitudes = np.abs(fft_result)

    # Erwartet: Peak bei 5 Hz Frequenz-Bin
    expected_peak_index = 5
    expected_magnitudes = np.zeros(100)
    expected_magnitudes[expected_peak_index] = 50.0  # Erwartete Amplitude
    expected_magnitudes[-expected_peak_index] = 50.0  # Negative Frequenz

    # Assert - approximativer Vergleich des vollständigen Arrays
    assert magnitudes == pytest.approx(expected_magnitudes, rel=1e-1, abs=1e-10)

Wann numpy.testing stattdessen verwenden:

Für erweiterte NumPy-Tests ziehen Sie das numpy.testing-Modul in Betracht:

import numpy.testing as npt

# Mehr Kontrolle über NaN- und Infinity-Behandlung
npt.assert_allclose(result, expected, rtol=1e-6, atol=1e-12)

# Arrays sind exakt gleich (keine Toleranz)
npt.assert_array_equal(result_int, expected_int)

# Arrays haben gleiche Form
npt.assert_array_compare(np.shape, result, expected)

Vergleich:

Feature pytest.approx numpy.testing.assert_allclose
Syntax assert result == pytest.approx(expected) npt.assert_allclose(result, expected)
Fehlermeldungen pytest's Assertion Introspection NumPy-spezifische Fehlermeldungen
NaN-Behandlung Erfordert nan_ok=True Eingebaut mit equal_nan=True
Infinity-Behandlung Funktioniert standardmäßig Funktioniert standardmäßig
Konsistenz Gleiche Syntax für alle pytest-Tests NumPy-spezifisch, getrennt von pytest

Empfehlung: Verwenden Sie pytest.approx() für Konsistenz, außer Sie benötigen NumPy-spezifische Features.

Spezialfälle: NaN und Infinity

NaN-Werte testen:

Standardmäßig gilt NaN != NaN in Fließkomma-Arithmetik. Verwenden Sie nan_ok=True:

import math
import pytest

# ❌ FALSCH - Dies wird fehlschlagen!
result_with_nan = math.nan
assert result_with_nan == pytest.approx(math.nan)  # AssertionError!

# ✅ KORREKT - Verwenden Sie nan_ok=True
assert result_with_nan == pytest.approx(math.nan, nan_ok=True)

Beispiel: Numerischen Algorithmus testen, der NaN produzieren kann

def test_safe_division_returns_nan():
    """Testen, dass sichere Division NaN für 0/0 zurückgibt."""
    # Arrange
    numerator = 0.0
    denominator = 0.0

    # Act
    result = safe_divide(numerator, denominator)  # Gibt NaN zurück statt zu werfen

    # Assert
    assert math.isnan(result)  # Explizite NaN-Prüfung

    # ODER pytest.approx mit nan_ok verwenden
    assert result == pytest.approx(math.nan, nan_ok=True)

Infinity testen:

Infinity funktioniert ohne spezielle Behandlung:

import math
import pytest

assert math.inf == pytest.approx(math.inf)
assert -math.inf == pytest.approx(-math.inf)

# Aber denken Sie daran: inf != sehr große Zahl!
import sys
assert sys.float_info.max != pytest.approx(math.inf)  # Unterschiedliche Werte!

Aus Kapitel 03 (Grenzwertanalyse): Denken Sie an den kritischen Unterschied:

def test_overflow_to_infinity():
    """Testen, dass Overflow Infinity produziert, nicht sys.float_info.max."""
    # Arrange
    huge_number = sys.float_info.max

    # Act
    result = huge_number * 2  # Overflows zu Infinity

    # Assert
    assert result == pytest.approx(math.inf)  # NICHT sys.float_info.max!
    assert math.isinf(result)

Verschachtelte Strukturen und Einschränkungen

pytest.approx() hat begrenzte Unterstützung für verschachtelte Strukturen. Sie können pytest.approx()-Aufrufe nicht direkt verschachteln:

❌ FALSCH - Das funktioniert nicht:

nested_dict = {
    "outer": {
        "inner": 0.1 + 0.2
    }
}

# Dies funktioniert NICHT - pytest.approx rekursiert nicht in verschachtelte Dicts
assert nested_dict == pytest.approx({"outer": {"inner": 0.3}})

✅ WORKAROUND - Flatten oder separat testen:

# Option 1: Flatten und testen
assert nested_dict["outer"]["inner"] == pytest.approx(0.3)

# Option 2: Verschachteltes Dict separat testen
assert nested_dict["outer"] == pytest.approx({"inner": 0.3})

# Option 3: Custom Helper für tiefen Vergleich verwenden
def approx_nested(data, expected, **kwargs):
    """pytest.approx rekursiv auf verschachtelte Strukturen anwenden."""
    if isinstance(expected, dict):
        return {k: approx_nested(data[k], v, **kwargs) for k, v in expected.items()}
    elif isinstance(expected, (list, tuple)):
        return type(expected)(approx_nested(d, e, **kwargs) for d, e in zip(data, expected))
    else:
        return pytest.approx(expected, **kwargs)

# Custom Helper verwenden
assert approx_nested(nested_dict, {"outer": {"inner": 0.3}}) == nested_dict

Empfehlung: Halten Sie Test-Assertions einfach. Wenn Sie tiefe verschachtelte Vergleiche benötigen, erwägen Sie Refactoring Ihrer Datenstrukturen oder Testen auf verschiedenen Ebenen.

Schnellreferenz: pytest.approx-Muster

Datenstruktur Syntax Hinweise
Skalar assert x == pytest.approx(expected) Grundlegender Fließkomma-Vergleich
Liste/Tupel assert [x, y] == pytest.approx([a, b]) Elementweiser Vergleich, Längen müssen übereinstimmen
Dictionary assert {"k": x} == pytest.approx({"k": a}) Schlüssel müssen übereinstimmen, Werte elementweise verglichen
NumPy-Array assert arr == pytest.approx(expected_arr) Funktioniert mit mehrdimensionalen Arrays
NaN-Werte assert x == pytest.approx(nan, nan_ok=True) Muss nan_ok=True aktivieren
Infinity assert x == pytest.approx(math.inf) Funktioniert ohne spezielle Behandlung
Custom-Toleranz pytest.approx(x, rel=1e-9, abs=1e-12) Relative und absolute Toleranzen anpassen

Warning und Deprecation Testing

Nicht alle Probleme im Code lösen Exceptions aus. Manchmal gibt Code Warnings aus - Signale, dass etwas falsch sein könnte, aber die Ausführung fortgesetzt wird.

Häufige Warning-Typen:

Warnings mit pytest.warns() testen

Ähnlich wie pytest.raises(), aber für Warnings:

Grundlegende Syntax:

import pytest
import warnings

def test_deprecated_function_warns():
    with pytest.warns(DeprecationWarning):
        deprecated_function()

Mit Nachrichten-Matching:

def test_deprecated_function_message():
    with pytest.warns(DeprecationWarning, match=r"deprecated.*use new_function instead"):
        deprecated_function()

Beispiel: Eigene Deprecation-Warnings testen

# road_profile_viewer/filters.py
def apply_filter(data, cutoff):
    """Filter auf Daten anwenden.

    .. deprecated:: 2.0
        Verwenden Sie stattdessen apply_lowpass_filter(). Diese Funktion wird in Version 3.0 entfernt.
    """
    warnings.warn(
        "apply_filter() is deprecated, use apply_lowpass_filter() instead",
        DeprecationWarning,
        stacklevel=2
    )
    return apply_lowpass_filter(data, cutoff)
def test_apply_filter_deprecation_warning():
    """Testen, dass apply_filter() Deprecation-Warning auslöst."""
    # Arrange
    data = np.array([1.0, 2.0, 3.0])
    cutoff = 1.0

    # Act & Assert
    with pytest.warns(DeprecationWarning, match=r"deprecated.*apply_lowpass_filter"):
        result = apply_filter(data, cutoff)

    # Kann trotzdem Ergebnis assertieren
    assert len(result) == len(data)

Wichtig: stacklevel-Parameter

Beim Ausgeben von Warnings in Ihrem eigenen Code verwenden Sie stacklevel=2, um das Warning an der Caller-Position zu melden, nicht innerhalb Ihrer Funktion:

# Ohne stacklevel - Warning zeigt auf Inneres von apply_filter()
warnings.warn("deprecated", DeprecationWarning)

# Mit stacklevel=2 - Warning zeigt auf wo apply_filter() aufgerufen wurde
warnings.warn("deprecated", DeprecationWarning, stacklevel=2)

Referenz: pytest.warns Dokumentation

Deprecation-Warnings testen: pytest.deprecated_call()

Speziell für das Testen von Deprecation-Warnings verwenden Sie pytest.deprecated_call():

def test_deprecated_function():
    with pytest.deprecated_call():
        deprecated_function()

Dies ist äquivalent zu:

with pytest.warns((DeprecationWarning, PendingDeprecationWarning)):
    deprecated_function()

Wann jedes verwenden:

Warning-Details zugreifen

Wie pytest.raises() können Sie auf Warning-Details zugreifen:

def test_warning_details():
    with pytest.warns(UserWarning) as warning_info:
        issue_warning()

    # Warning-Details zugreifen
    assert len(warning_info) == 1  # Anzahl Warnings
    assert "specific text" in str(warning_info[0].message)
    assert warning_info[0].category is UserWarning

Beispiel: NumPy-Runtime-Warnings testen

def test_divide_by_zero_warning():
    """Testen, dass Division durch Null in NumPy RuntimeWarning auslöst."""
    # Arrange
    numerator = np.array([1.0, 2.0, 3.0])
    denominator = np.array([1.0, 0.0, 1.0])  # Enthält Null!

    # Act & Assert
    with pytest.warns(RuntimeWarning, match="divide by zero"):
        result = numerator / denominator

    # Ergebnis enthält inf bei Index 1
    assert math.isinf(result[1])

Warning-Filter konfigurieren

Manchmal möchten Sie Warnings während Tests unterdrücken (z.B. Drittanbieter-Bibliotheks-Warnings, die Sie nicht kontrollieren können).

In pytest.ini:

[pytest]
filterwarnings =
    error                           # Warnings in Errors umwandeln (strenger Modus)
    ignore::DeprecationWarning      # Alle Deprecation-Warnings ignorieren
    ignore:.*deprecated.*:DeprecationWarning:numpy.*  # NumPy-Deprecations ignorieren

In einzelnen Tests:

@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_with_suppressed_warnings():
    # Dieser Test schlägt nicht bei Deprecation-Warnings fehl
    deprecated_function()

Häufiger Anwendungsfall: Drittanbieter-Warnings ignorieren, während eigene beibehalten:

[pytest]
filterwarnings =
    error                                        # Bei Warnings fehlschlagen
    ignore::DeprecationWarning:matplotlib.*      # Außer matplotlib-Deprecations
    ignore::PendingDeprecationWarning:numpy.*    # Außer NumPy-Pending-Deprecations

Referenz: Warnings Capture-Konfiguration

Wann Warnings testen

Warnings testen wenn:

  1. Sie Ihre eigene API deprecaten - Sicherstellen, dass Warnings korrekt ausgegeben werden
  2. Sie Bibliotheks-Warnings umgehen - Erwartete Warnings in Tests dokumentieren
  3. Sie numerischen Code testen - Warnings für Edge-Cases verifizieren (Overflow, Underflow, Division durch Null)

Warnings nicht testen wenn:

  1. Sie von Drittanbieter-Bibliotheken stammen - Nicht Ihre Verantwortung (stattdessen filtern)
  2. Sie nicht zur Testabsicht gehören - Auf primäres Verhalten fokussieren

Schnellreferenz: Warning-Testing

Muster Syntax Anwendungsfall
Einfaches Warning with pytest.warns(UserWarning): Testen, dass Code spezifischen Warning-Typ ausgibt
Warning-Nachricht with pytest.warns(UserWarning, match=r"pattern"): Warning-Nachricht gegen Regex validieren
Deprecation with pytest.deprecated_call(): Testen, dass Code Deprecation-Warning ausgibt
Warning-Details with pytest.warns(UserWarning) as w: Auf Warning-Objekt für detaillierte Assertions zugreifen
Warnings unterdrücken @pytest.mark.filterwarnings("ignore") Warnings in spezifischem Test ignorieren

Test Control und Organization

Manchmal müssen Sie kontrollieren, wann Tests laufen, oder sie explizit als fehlschlagend markieren. pytest bietet mehrere Helpers für Test-Control.

Explizites Test-Fehlschlagen: pytest.fail()

Manchmal müssen Sie einen Test explizit mit einer Custom-Nachricht fehlschlagen lassen:

Syntax:

import pytest

def test_something():
    if complex_condition():
        pytest.fail("Custom failure message")

Wann pytest.fail() verwenden:

  1. Komplexe bedingte Logik - Wenn einfaches assert nicht ausdrucksstark genug ist
  2. Platzhalter-Tests - Tests als TODO markieren
  3. Unerreichbarer Code - Fehlschlagen, wenn Code unerwarteten Zustand erreicht

Beispiel: Testen, dass Code-Pfad NICHT genommen wird

def test_error_handling_path_not_taken():
    """Testen, dass Fehlerbehandlungs-Pfad NICHT für gültige Eingabe ausgelöst wird."""
    # Arrange
    valid_data = np.array([1.0, 2.0, 3.0])

    # Act
    try:
        result = process_data(valid_data)
    except ValueError:
        pytest.fail("ValueError should not be raised for valid data")

    # Assert
    assert len(result) == len(valid_data)

Alternative (pythonischer):

def test_error_handling_path_not_taken_v2():
    """Testen, dass Fehlerbehandlungs-Pfad NICHT für gültige Eingabe ausgelöst wird."""
    # Arrange
    valid_data = np.array([1.0, 2.0, 3.0])

    # Act - einfach aufrufen, wenn Exception ausgelöst, schlägt Test automatisch fehl
    result = process_data(valid_data)

    # Assert
    assert len(result) == len(valid_data)

Die zweite Version wird bevorzugt - pytest schlägt automatisch fehl, wenn unerwartete Exception auftritt.

Wann pytest.fail() nützlich IST:

def test_switch_statement_coverage():
    """Alle Zweige von Switch-ähnlicher Logik testen."""
    for case in ["option_a", "option_b", "option_c"]:
        result = handle_option(case)

        if case == "option_a":
            assert result == "handled_a"
        elif case == "option_b":
            assert result == "handled_b"
        elif case == "option_c":
            assert result == "handled_c"
        else:
            pytest.fail(f"Unexpected case: {case}")  # Sollte nie hierher kommen

Referenz: pytest.fail Dokumentation

Tests überspringen: pytest.skip()

Tests bedingt zur Laufzeit überspringen:

Syntax:

import pytest
import sys

def test_windows_only():
    if sys.platform != "win32":
        pytest.skip("Test only runs on Windows")

    # Windows-spezifischer Testcode
    ...

Wann pytest.skip() verwenden:

  1. Plattformspezifische Tests - Auf nicht unterstützten Plattformen überspringen
  2. Abhängigkeitsbasierte Tests - Überspringen, wenn optionale Abhängigkeit fehlt
  3. Langsame Tests - Während schneller Entwicklung überspringen
  4. Externe Ressourcen-Tests - Überspringen, wenn Ressource nicht verfügbar

Beispiel: Überspringen, wenn optionale Abhängigkeit fehlt

def test_with_optional_dependency():
    try:
        import matplotlib.pyplot as plt
    except ImportError:
        pytest.skip("matplotlib not installed")

    # Testcode mit matplotlib
    ...

Besserer Ansatz: Decorator oder importorskip verwenden

# Option 1: Decorator
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_windows_only():
    ...

# Option 2: importorskip
def test_with_matplotlib():
    plt = pytest.importorskip("matplotlib.pyplot")
    # Testcode mit plt
    ...

Empfehlung: Decorators für statische Bedingungen bevorzugen, pytest.skip() für dynamische Laufzeitbedingungen.

Referenz: Tests überspringen

Erwartete Fehlschläge: pytest.xfail()

Tests als “erwartetes Fehlschlagen” markieren - nützlich für bekannte Bugs oder unvollständige Features:

Syntax:

import pytest

def test_known_bug():
    pytest.xfail("Known bug #123 - division by zero not handled")
    buggy_function()

Wann pytest.xfail() verwenden:

  1. Bekannte Bugs - Bugs mit fehlschlagenden Tests dokumentieren (besser als Tests löschen!)
  2. Unvollständige Features - Tests für Features vor Implementierung schreiben (TDD)
  3. Plattformspezifische Fehlschläge - Tests markieren, die auf spezifischen Plattformen fehlschlagen

Beispiel: Bekanntes Bug-Dokumentation

def test_edge_case_known_issue():
    """Test Edge Case mit bekanntem Problem.

    Siehe: https://github.com/user/repo/issues/123
    TODO: In Version 2.1 fixen
    """
    if sys.float_info.max * 2 == math.inf:  # Wird auf manchen Plattformen fehlschlagen
        pytest.xfail("Known issue: overflow handling platform-dependent")

    result = handle_overflow(sys.float_info.max)
    assert result is not None

Decorator-Form:

@pytest.mark.xfail(reason="Known bug #123")
def test_known_bug():
    buggy_function()

# Bedingtes xfail
@pytest.mark.xfail(sys.platform == "win32", reason="Fails on Windows")
def test_unix_specific():
    ...

Unterschied zu skip:

Referenz: Erwartete Fehlschläge

Bedingter Import: pytest.importorskip()

Test überspringen, wenn Modul nicht importiert werden kann:

Syntax:

def test_with_optional_dependency():
    np = pytest.importorskip("numpy", minversion="1.20")
    # Testcode mit numpy
    ...

Vorteile gegenüber try/except:

  1. Klarere Absicht - Offensichtlich Überspringen wegen fehlender Abhängigkeit
  2. Versions-Prüfung - Kann Mindestversion spezifizieren
  3. Bessere pytest-Ausgabe - Als “skipped” mit Grund markiert

Beispiel: Testen mit optionalen Abhängigkeiten

def test_advanced_plotting():
    """Erweiterte Plotting-Features testen (erfordert matplotlib)."""
    plt = pytest.importorskip("matplotlib.pyplot")
    sns = pytest.importorskip("seaborn", minversion="0.11")

    # Arrange
    data = np.array([1, 2, 3, 4, 5])

    # Act
    fig, ax = plt.subplots()
    sns.lineplot(x=range(len(data)), y=data, ax=ax)

    # Assert
    assert len(ax.lines) == 1

Referenz: pytest.importorskip Dokumentation

Decorators vs. imperative Aufrufe

Wann Decorators verwenden:

# Statische Bedingungen (bekannt bevor Test läuft)
@pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
def test_unix_feature():
    ...

@pytest.mark.xfail(reason="Known bug")
def test_buggy_feature():
    ...

Wann imperative Aufrufe verwenden:

# Dynamische Bedingungen (während Test-Ausführung bestimmt)
def test_conditional_skip():
    config = load_config()  # Muss Code ausführen, um Bedingung zu bestimmen
    if not config.feature_enabled:
        pytest.skip("Feature disabled in config")
    ...

Empfehlung: Decorators wenn möglich verwenden (klarer, zeigt Skip-Grund bevor Test läuft).

Schnellreferenz: Test Control

Funktion Syntax Anwendungsfall
pytest.fail() pytest.fail("message") Test explizit mit Custom-Nachricht fehlschlagen lassen
pytest.skip() pytest.skip("reason") Test zur Laufzeit überspringen (dynamische Bedingung)
@pytest.mark.skip @pytest.mark.skip(reason="...") Test überspringen (statische Bedingung)
@pytest.mark.skipif @pytest.mark.skipif(condition, reason="...") Test bedingt überspringen
pytest.xfail() pytest.xfail("reason") Test als erwartetes Fehlschlagen markieren (Laufzeit)
@pytest.mark.xfail @pytest.mark.xfail(reason="...") Test als erwartetes Fehlschlagen markieren (statisch)
pytest.importorskip() pytest.importorskip("module") Test überspringen, wenn Modul nicht verfügbar

Assertion Introspection Mastery

Eines der mächtigsten Features von pytest ist Assertion Introspection - die Fähigkeit, detaillierte Informationen darüber anzuzeigen, warum Assertions fehlschlagen.

Wie Assertion Rewriting funktioniert

Wenn Sie pytest importieren, schreibt es Python’s assert-Statement vor der Ausführung neu. Dies ermöglicht pytest:

  1. Zwischenwerte erfassen - Zeigt Subexpressions in fehlgeschlagenen Assertions
  2. Kontext liefern - Zeigt umgebende Codezeilen
  3. Ausgabe formatieren - Pretty-prints komplexe Datenstrukturen

Beispiel für Introspection-Ausgabe:

def test_list_comparison():
    result = [1, 2, 3, 4]
    expected = [1, 2, 5, 4]
    assert result == expected

Pytest-Ausgabe:

    def test_list_comparison():
        result = [1, 2, 3, 4]
        expected = [1, 2, 5, 4]
>       assert result == expected
E       AssertionError: assert [1, 2, 3, 4] == [1, 2, 5, 4]
E         At index 2 diff: 3 != 5
E         Use -v to get more diff

Beachten Sie, wie pytest automatisch:

Was Introspection für verschiedene Typen zeigt

Strings - Kontext-Diff:

def test_long_string():
    result = "The quick brown fox jumps over the lazy dog"
    expected = "The quick brown cat jumps over the lazy dog"
    assert result == expected

Ausgabe:

E       AssertionError: assert 'The quick br...he lazy dog' == 'The quick br...he lazy dog'
E         - The quick brown cat jumps over the lazy dog
E         ?                 ^^
E         + The quick brown fox jumps over the lazy dog
E         ?                 ^^

Listen - Erstes unterschiedliches Element:

def test_list_diff():
    result = [1, 2, 3, 4, 5]
    expected = [1, 2, 3, 99, 5]
    assert result == expected

Ausgabe:

E       AssertionError: assert [1, 2, 3, 4, 5] == [1, 2, 3, 99, 5]
E         At index 3 diff: 4 != 99

Dictionaries - Unterschiedliche Einträge:

def test_dict_diff():
    result = {"a": 1, "b": 2, "c": 3}
    expected = {"a": 1, "b": 99, "c": 3}
    assert result == expected

Ausgabe:

E       AssertionError: assert {'a': 1, 'b': 2, 'c': 3} == {'a': 1, 'b': 99, 'c': 3}
E         Differing items:
E         {'b': 2} != {'b': 99}

Sets - Extra/fehlende Elemente:

def test_set_diff():
    result = {1, 2, 3, 4}
    expected = {1, 2, 3, 5}
    assert result == expected

Ausgabe:

E       AssertionError: assert {1, 2, 3, 4} == {1, 2, 3, 5}
E         Extra items in the left set:
E         {4}
E         Extra items in the right set:
E         {5}

Custom Assertion Messages

Sie können Custom-Nachrichten zu Assertions hinzufügen:

Syntax:

assert condition, "Custom failure message"

Beispiel:

def test_with_custom_message():
    result = compute_value()
    expected = 42
    assert result == expected, f"Expected {expected}, but got {result}"

Wichtig: Modernes pytest (7.0+) behält Introspection sogar mit Custom-Nachrichten!

Altes pytest (vor 7.0):

Modernes pytest (7.0+):

Beispiel-Ausgabe (pytest 7.0+):

>       assert result == expected, f"Expected {expected}, but got {result}"
E       AssertionError: Expected 42, but got 41
E       assert 41 == 42

Wann Custom-Nachrichten hinzufügen:

  1. Komplexe Bedingungen - Erklären, WARUM die Assertion wichtig ist
  2. Domänenspezifische Prüfungen - Kontext über das Getestete hinzufügen
  3. Debugging-Hinweise - Fixes oder verwandte Tests vorschlagen

Wann KEINE Custom-Nachrichten hinzufügen:

  1. Einfache Vergleiche - Introspection bereits klar
  2. Redundante Information - Nur wiederholt, was Introspection zeigt

Beispiel: Gute Verwendung von Custom-Nachricht

def test_filter_cutoff_frequency():
    """Testen, dass Filter-Cutoff im gültigen Bereich ist."""
    # Arrange
    sample_rate = 100.0  # Hz
    cutoff_freq = 60.0   # Hz

    # Act
    filter_config = create_filter(sample_rate, cutoff_freq)

    # Assert mit hilfreicher Nachricht
    assert filter_config.cutoff_freq < sample_rate / 2, \
        f"Cutoff frequency ({cutoff_freq} Hz) must be less than Nyquist frequency ({sample_rate/2} Hz)"

Custom Assertion Explanations (Erweitert)

Für sehr komplexe Custom-Typen können Sie Custom-Assertion-Erklärungen über den pytest_assertrepr_compare-Hook definieren.

Beispiel: Custom-Vergleich für NumPy-Arrays

# conftest.py
import numpy as np

def pytest_assertrepr_compare(op, left, right):
    """Custom Assertion-Darstellung für NumPy-Arrays."""
    if isinstance(left, np.ndarray) and isinstance(right, np.ndarray) and op == "==":
        return [
            "NumPy array comparison:",
            f"  Shape: {left.shape} vs {right.shape}",
            f"  Dtype: {left.dtype} vs {right.dtype}",
            f"  Max difference: {np.max(np.abs(left - right))}",
            f"  Mean difference: {np.mean(np.abs(left - right))}",
        ]

Ergebnis:

E       AssertionError: NumPy array comparison:
E         Shape: (100,) vs (100,)
E         Dtype: float64 vs float64
E         Max difference: 0.05
E         Mean difference: 0.01

Wann verwenden:

Hinweis: Dies ist erweiterte Verwendung. Für die meisten Tests ist Standard-Introspection ausreichend.

Referenz: Custom Assertion Explanations

Debugging Assertion Rewriting

Manchmal funktioniert Assertion Rewriting nicht. Häufige Ursachen:

1. Modul vor pytest importiert:

# ❌ FALSCH - Importiert Modul bevor pytest Assertions umschreiben kann
import my_module
import pytest

# ✅ KORREKT - pytest zuerst importiert (automatisch in Testdateien)
import pytest
import my_module

2. Assertions in importierten Modulen:

Pytest schreibt nur Assertions in Testdateien um (Dateien, die test_*.py oder *_test.py entsprechen).

Für Assertions in Nicht-Test-Dateien verwenden Sie pytest.register_assert_rewrite():

# conftest.py
pytest.register_assert_rewrite("my_package.helpers")

3. Bytecode-Caching-Probleme:

Wenn Assertion-Introspection nach Code-Änderungen nicht mehr funktioniert, pytest-Cache löschen:

pytest --cache-clear

Referenz: Assertion Rewriting

Schnellreferenz: Assertion Introspection

Typ Introspection zeigt Beispiel-Ausgabe
Zahlen Werte und Vergleich assert 5 == 105 != 10
Strings Kontext-Diff Zeichen-für-Zeichen-Diff mit Markierungen
Listen Erster unterschiedlicher Index At index 3 diff: 4 != 99
Dicts Unterschiedliche Elemente Differing items: {'b': 2} != {'b': 99}
Sets Extra/fehlende Elemente Extra items in left: {4}
Custom-Typen Standard repr() Verwenden Sie pytest_assertrepr_compare für Custom

Parametrisierungsmuster

Eines der mächtigsten pytest-Features ist Parametrisierung - denselben Test mit unterschiedlichen Eingaben ausführen.

Grundlegende Parametrisierung: @pytest.mark.parametrize

Syntax:

import pytest

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    assert double(input) == expected

Dies erstellt drei separate Tests, einen für jeden Parameter-Satz:

test_module.py::test_double[1-2] PASSED
test_module.py::test_double[2-4] PASSED
test_module.py::test_double[3-6] PASSED

Beispiel: Boundary Values testen

Aus Kapitel 03 (Grenzwertanalyse) haben Sie gelernt, Boundary Values zu testen. Parametrisierung macht dies sauberer:

@pytest.mark.parametrize("x,expected", [
    (0.0, 0.0),                    # Null
    (1.0, 1.0),                    # Normaler Wert
    (sys.float_info.max, sys.float_info.max),  # Größte endliche
    (math.inf, None),              # Infinity (gibt None zurück)
    (-math.inf, None),             # Negative Infinity
])
def test_safe_sqrt_boundaries(x, expected):
    """safe_sqrt an Fließkomma-Grenzen testen."""
    result = safe_sqrt(x)

    if expected is None:
        assert result is None
    else:
        assert result == pytest.approx(expected)

Vorteile:

  1. Reduziert Duplikation - Gleiche Testlogik, unterschiedliche Daten
  2. Klare Testnamen - Jeder Parameter-Satz erstellt separaten Test
  3. Granulare Fehlschläge - Wissen genau, welche Eingabe fehlschlug
  4. Einfach Fälle hinzuzufügen - Einfach zur Liste hinzufügen

Mehrere Parameter

Sie können mehrere Argumente parametrisieren:

@pytest.mark.parametrize("x,y,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (sys.float_info.max, 1, math.inf),  # Overflow
])
def test_add(x, y, expected):
    result = x + y
    assert result == pytest.approx(expected)

Fixtures parametrisieren: indirect=True

Manchmal möchten Sie Fixtures statt Test-Parameter parametrisieren:

Beispiel: Mit verschiedenen Datendateien testen

# conftest.py
@pytest.fixture
def profile_data(request):
    """Profildaten aus Datei laden."""
    filename = request.param  # Parameter-Wert erhalten
    return np.loadtxt(f"test_data/{filename}")

# test_processing.py
@pytest.mark.parametrize("profile_data", [
    "smooth_road.txt",
    "rough_road.txt",
    "highway.txt",
], indirect=True)  # Parameter an Fixture übergeben!
def test_process_profile(profile_data):
    result = process_profile(profile_data)
    assert len(result) == len(profile_data)

Wie es funktioniert:

  1. indirect=True sagt pytest, Parameter an Fixture zu übergeben, nicht Test
  2. Fixture erhält Parameter über request.param
  3. Fixture gibt verarbeiteten Wert an Test zurück

Wann verwenden:

Fixtures und Parametrize kombinieren

Sie können reguläre Fixtures mit parametrisierten Werten mischen:

@pytest.fixture
def sample_rate():
    """Sample-Rate-Fixture (nicht parametrisiert)."""
    return 100.0  # Hz

@pytest.mark.parametrize("cutoff_freq,expected_response", [
    (10.0, "lowpass"),
    (50.0, "highpass"),
])
def test_filter_response(sample_rate, cutoff_freq, expected_response):
    """Filter-Antwort bei verschiedenen Cutoff-Frequenzen testen."""
    # sample_rate von Fixture, cutoff_freq von parametrize
    filter = create_filter(sample_rate, cutoff_freq)
    assert filter.response_type == expected_response

ids für lesbare Testnamen verwenden

Standardmäßig generiert pytest Testnamen aus Parameter-Werten. Für komplexe Werte kann dies hässlich sein:

# Standard-Testnamen (schwer zu lesen):
# test_something[<object object at 0x...>-expected0]
# test_something[<object object at 0x...>-expected1]

# Besser: ids-Parameter verwenden
@pytest.mark.parametrize("data,expected", [
    (smooth_road_data, "smooth"),
    (rough_road_data, "rough"),
], ids=["smooth_road", "rough_road"])
def test_classification(data, expected):
    ...

Ergebnis:

test_module.py::test_classification[smooth_road] PASSED
test_module.py::test_classification[rough_road] PASSED

Funktionen für IDs verwenden:

def idfn(val):
    """Test-ID aus Parameter-Wert generieren."""
    if isinstance(val, np.ndarray):
        return f"array_len_{len(val)}"
    return str(val)

@pytest.mark.parametrize("data", [
    np.array([1, 2, 3]),
    np.array([1, 2, 3, 4, 5]),
], ids=idfn)
def test_with_arrays(data):
    ...

Ergebnis:

test_module.py::test_with_arrays[array_len_3] PASSED
test_module.py::test_with_arrays[array_len_5] PASSED

Parametrisierungs-Anti-Patterns

❌ Anti-Pattern 1: Über-Parametrisierung

Nicht verwandte Verhaltensweisen parametrisieren:

# ❌ FALSCH - Unterschiedliche Verhaltensweisen testen, nicht unterschiedliche Eingaben
@pytest.mark.parametrize("operation,x,y,expected", [
    ("add", 1, 2, 3),
    ("subtract", 5, 3, 2),
    ("multiply", 2, 3, 6),
])
def test_calculator(operation, x, y, expected):
    if operation == "add":
        result = x + y
    elif operation == "subtract":
        result = x - y
    elif operation == "multiply":
        result = x * y
    assert result == expected

✅ KORREKT - Separate Tests für unterschiedliche Verhaltensweisen:

@pytest.mark.parametrize("x,y,expected", [(1, 2, 3), (0, 0, 0), (-1, 1, 0)])
def test_add(x, y, expected):
    assert add(x, y) == expected

@pytest.mark.parametrize("x,y,expected", [(5, 3, 2), (0, 0, 0), (-1, -1, 0)])
def test_subtract(x, y, expected):
    assert subtract(x, y) == expected

@pytest.mark.parametrize("x,y,expected", [(2, 3, 6), (0, 5, 0), (-1, -1, 1)])
def test_multiply(x, y, expected):
    assert multiply(x, y) == expected

Warum: Unterschiedliche Verhaltensweisen sollten separate Tests sein. Parametrisieren Sie gleiches Verhalten mit unterschiedlichen Eingaben.

❌ Anti-Pattern 2: Versteckte Testlogik in Parametern

# ❌ FALSCH - Testlogik in Parameter-Liste
@pytest.mark.parametrize("x,expected", [
    (0, 0),
    (1, 1),
    (2, 4),
    (3, 9),
    (4, 16),  # Muster: expected = x**2, aber nicht offensichtlich!
])
def test_square(x, expected):
    assert square(x) == expected

✅ KORREKT - Muster explizit machen:

@pytest.mark.parametrize("x", [0, 1, 2, 3, 4])
def test_square(x):
    expected = x ** 2  # Muster im Test-Body klar
    assert square(x) == expected

Oder Helper-Funktion verwenden:

def square_test_cases():
    """Test-Fälle für Square-Funktion generieren."""
    return [(x, x**2) for x in range(10)]

@pytest.mark.parametrize("x,expected", square_test_cases())
def test_square(x, expected):
    assert square(x) == expected

❌ Anti-Pattern 3: Zu viele Parameter

# ❌ FALSCH - Zu viele Parameter, schwer zu lesen
@pytest.mark.parametrize(
    "sample_rate,cutoff_freq,window_size,overlap,detrend,filter_type,expected_length",
    [
        (100, 10, 256, 128, True, "lowpass", 100),
        (200, 20, 512, 256, False, "highpass", 200),
        # ... 20 weitere Fälle ...
    ]
)
def test_complex_filter(...):  # 7 Parameter!
    ...

✅ KORREKT - Dataclasses oder Dicts verwenden:

from dataclasses import dataclass

@dataclass
class FilterConfig:
    sample_rate: float
    cutoff_freq: float
    window_size: int
    overlap: int
    detrend: bool
    filter_type: str
    expected_length: int

@pytest.mark.parametrize("config", [
    FilterConfig(100, 10, 256, 128, True, "lowpass", 100),
    FilterConfig(200, 20, 512, 256, False, "highpass", 200),
], ids=["config_1", "config_2"])
def test_complex_filter(config):
    result = apply_filter(
        sample_rate=config.sample_rate,
        cutoff_freq=config.cutoff_freq,
        # ...
    )
    assert len(result) == config.expected_length

Schnellreferenz: Parametrisierung

Muster Syntax Anwendungsfall
Einfache Parametrisierung @pytest.mark.parametrize("x,y", [(1,2), (3,4)]) Test mit unterschiedlichen Eingaben ausführen
Einzelner Parameter @pytest.mark.parametrize("x", [1, 2, 3]) Einzelne Eingabe variieren
Fixture-Parametrisierung @pytest.mark.parametrize("fix", [...], indirect=True) Parameter an Fixture übergeben
Custom-Test-IDs @pytest.mark.parametrize(..., ids=[...]) Lesbare Testnamen
ID-Funktion @pytest.mark.parametrize(..., ids=func) IDs aus Parameter-Werten generieren

Referenz: Parametrisierungs-Dokumentation


Häufige Anti-Patterns

LLM-generierte Tests (von ChatGPT, Claude, etc.) enthalten oft Anti-Patterns. Hier sind die häufigsten Fehler, auf die Sie achten sollten.

Anti-Pattern 1: Tests, die Werte zurückgeben

❌ FALSCH:

def test_addition():
    result = 1 + 1
    return result == 2  # ❌ Tests dürfen NICHT zurückgeben!

Warum es falsch ist: pytest ignoriert Rückgabewerte. Dieser Test wird IMMER bestehen, selbst wenn result != 2.

✅ KORREKT:

def test_addition():
    result = 1 + 1
    assert result == 2  # ✅ assert verwenden!

Erkennung: Nach return-Statements in Test-Funktionen suchen (fast immer falsch).

Anti-Pattern 2: Über Test-Fälle schleifen

❌ FALSCH:

def test_multiple_cases():
    test_cases = [(1, 2), (2, 4), (3, 6)]
    for input, expected in test_cases:
        assert double(input) == expected  # ❌ Stoppt bei erstem Fehlschlag!

Warum es falsch ist:

  1. Stoppt bei erstem Fehlschlag - Sie sehen nicht alle fehlschlagenden Fälle
  2. Keine granulare Berichterstattung - Wissen nicht aus Testname, welcher Fall fehlschlug
  3. Schwerer zu debuggen - Müssen Print-Statements hinzufügen, um zu sehen, welche Iteration fehlschlug

✅ KORREKT:

@pytest.mark.parametrize("input,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    assert double(input) == expected  # ✅ parametrize verwenden!

Vorteile:

  1. Alle Fehlschläge gemeldet - Führt alle Fälle aus, selbst wenn einige fehlschlagen
  2. Granulare Testnamen - test_double[1-2], test_double[2-4], etc.
  3. Einfach zu debuggen - Wissen genau, welcher Fall fehlschlug

Anti-Pattern 3: Assertions mit Seiteneffekten

❌ FALSCH:

def test_data_processing():
    data = []
    assert data.append(1) is None  # ❌ Modifiziert data!
    assert len(data) == 1

Warum es falsch ist: Assertions sollten Zustand NICHT modifizieren. Dies macht Tests:

  1. Schwer zu verstehen - Assertion prüft nicht nur, sie tut etwas!
  2. Fragil - Hängt von Assertion-Ausführungsreihenfolge ab
  3. Verwirrend wenn übersprungen - Wenn Assertion nicht läuft, ist Zustand anders

✅ KORREKT:

def test_data_processing():
    data = []
    data.append(1)  # ✅ Aktion von Assertion trennen
    assert len(data) == 1

Anti-Pattern 4: Über-Mocking

LLM-generierte Tests missbrauchen oft Mocking. Aus Kapitel 03 (Grenzwertanalyse):

❌ FALSCH:

def test_data_processing(mocker):
    # ❌ Interne Implementierungsdetails mocken
    mocker.patch("module.internal_helper")
    mocker.patch("module.another_helper")
    mocker.patch("module.yet_another_helper")

    result = process_data([1, 2, 3])

    # ❌ Methodenaufrufe testen, nicht Ergebnisse
    module.internal_helper.assert_called_once()
    module.another_helper.assert_called_with(ANY)

Warum es falsch ist:

  1. Testet Implementierung, nicht Verhalten - Bricht beim Refactoring
  2. Testet eigentliche Logik nicht - Mocks umgehen echten Code
  3. Falsches Vertrauen - Tests bestehen, selbst wenn Code kaputt ist

✅ KORREKT:

def test_data_processing():
    # ✅ Keine Mocks - tatsächliches Verhalten testen
    result = process_data([1, 2, 3])

    # ✅ Ergebnis assertieren (Zustand), nicht Methodenaufrufe (Interaktion)
    assert len(result) == 3
    assert all(isinstance(x, int) for x in result)
    assert result == [2, 4, 6]  # Tatsächlich erwartetes Ergebnis

Wann mocken:

  1. Externe Services - APIs, Datenbanken, Dateisysteme
  2. Langsame Operationen - Netzwerkaufrufe, große Berechnungen
  3. Nicht-deterministisches Verhalten - Random, zeitabhängig

Wann NICHT mocken:

  1. Ihre eigenen Funktionen - Direkt testen
  2. Einfache Helpers - Schneller auszuführen als zu mocken
  3. Business-Logik - Der Kern dessen, was Sie testen

Anti-Pattern 5: Nicht-deterministische Tests

❌ FALSCH:

import random

def test_random_sampling():
    data = random.sample(range(100), 10)  # ❌ Jedes Mal anders!
    result = process_data(data)
    assert len(result) == 10  # Könnte zufällig bestehen oder fehlschlagen

Warum es falsch ist:

  1. Flaky Tests - Bestehen manchmal, schlagen manchmal fehl (zerstört Vertrauen in Tests)
  2. Schwer zu debuggen - Kann Fehlschläge nicht reproduzieren
  3. Verschwendet Zeit - Entwickler führen Tests erneut aus in der Hoffnung auf Bestehen

✅ KORREKT - Option 1: Random-Generator seeden

import random

def test_random_sampling():
    random.seed(42)  # ✅ Konsistente Ergebnisse
    data = random.sample(range(100), 10)
    result = process_data(data)
    assert len(result) == 10

✅ KORREKT - Option 2: Fixture mit festen Daten verwenden

@pytest.fixture
def sample_data():
    """Feste Testdaten (deterministisch)."""
    return [1, 5, 10, 15, 20, 25, 30, 35, 40, 45]

def test_sampling(sample_data):
    result = process_data(sample_data)
    assert len(result) == 10

Andere nicht-deterministische Quellen:

# ❌ FALSCH - Zeitabhängig
import time
def test_timestamp():
    timestamp = time.time()  # Jedes Mal anders!
    ...

# ✅ KORREKT - Zeit mocken oder fixieren
def test_timestamp(mocker):
    mocker.patch("time.time", return_value=1234567890)
    timestamp = time.time()
    assert timestamp == 1234567890

# ❌ FALSCH - Reihenfolgeabhängig (dict/set-Iteration)
def test_keys():
    data = {"a": 1, "b": 2, "c": 3}
    keys = list(data.keys())
    assert keys[0] == "a"  # Reihenfolge nicht garantiert in Python < 3.7!

# ✅ KORREKT - Nicht von Reihenfolge abhängen
def test_keys():
    data = {"a": 1, "b": 2, "c": 3}
    assert "a" in data.keys()
    assert set(data.keys()) == {"a", "b", "c"}

Anti-Pattern 6: Test-Pollution

❌ FALSCH:

# Geteilter globaler Zustand zwischen Tests
global_cache = []

def test_add_to_cache():
    global_cache.append(1)
    assert len(global_cache) == 1  # ✅ Besteht beim ersten Mal

def test_cache_contains_items():
    assert len(global_cache) > 0  # ❌ Hängt von Testreihenfolge ab!

Warum es falsch ist:

  1. Reihenfolgeabhängig - Tests bestehen/schlagen fehl abhängig von Ausführungsreihenfolge
  2. Bricht Isolation - Tests beeinflussen sich gegenseitig
  3. Schwer zu debuggen - Fehlschläge erscheinen nur bei Tests in spezifischer Reihenfolge

✅ KORREKT:

@pytest.fixture
def cache():
    """Frischer Cache für jeden Test."""
    return []

def test_add_to_cache(cache):
    cache.append(1)
    assert len(cache) == 1  # ✅ Isoliert

def test_cache_contains_items(cache):
    cache.append(1)
    cache.append(2)
    assert len(cache) == 2  # ✅ Isoliert

Fixture-Scope:

# Function-Scope (Standard) - Neue Instanz pro Test
@pytest.fixture(scope="function")
def fresh_cache():
    return []

# Module-Scope - Geteilt über Tests im selben Modul
@pytest.fixture(scope="module")
def shared_resource():
    # Für teures Setup verwenden (Datenbank, etc.)
    return expensive_resource()

# Session-Scope - Geteilt über gesamte Test-Session
@pytest.fixture(scope="session")
def global_config():
    return load_config()

Anti-Pattern 7: Zu strikte Assertions

❌ FALSCH:

def test_error_message():
    with pytest.raises(ValueError) as exc_info:
        validate_input(-1)

    # ❌ Zu strikt - bricht bei leichter Nachrichtenänderung
    assert str(exc_info.value) == "Input must be positive integer greater than zero"

Warum es falsch ist: Kleinere Nachrichtenänderungen brechen Tests (z.B. “positive integer” → “positive int”).

✅ KORREKT:

def test_error_message():
    with pytest.raises(ValueError, match=r"must be positive"):  # ✅ Flexibles Regex
        validate_input(-1)

Striktheit balancieren:

Anti-Pattern 8: Eingebaute Funktionalität testen

❌ FALSCH:

def test_list_append():
    """Testen, dass list.append funktioniert."""
    lst = [1, 2]
    lst.append(3)
    assert lst == [1, 2, 3]  # ❌ Testet Python, nicht Ihren Code!

Warum es falsch ist: Sie testen Python’s List-Implementierung, nicht Ihren Code.

✅ KORREKT - IHREN Code testen:

def test_data_processor_uses_append_correctly():
    """Testen, dass DataProcessor Elemente korrekt hinzufügt."""
    processor = DataProcessor()
    processor.add_value(1)
    processor.add_value(2)

    # ✅ IHREN Code's Verhalten testen
    assert processor.get_values() == [1, 2]
    assert processor.count() == 2

Schnellreferenz: Anti-Patterns

Anti-Pattern Warum falsch Fix
Rückgabewerte pytest ignoriert Returns assert verwenden
Über Test-Fälle schleifen Stoppt bei erstem Fehlschlag @pytest.mark.parametrize verwenden
Assertions mit Seiteneffekten Modifiziert Zustand während Prüfung Aktion von Assertion trennen
Über-Mocking Testet Implementierung, nicht Verhalten Zustand testen, nur externe Deps mocken
Nicht-deterministische Tests Flaky, schwer zu debuggen Random seeden, Zeit fixieren, deterministische Daten verwenden
Test-Pollution Tests beeinflussen sich gegenseitig Fixtures für Isolation verwenden
Zu strikt Bricht bei kleinen Änderungen Regex verwenden, nur Schlüsseleigenschaften prüfen
Eingebautes testen Testet nicht Ihren Code Verhalten Ihres Codes testen

Schnellreferenz-Tabellen

Alle pytest-Assertion-Helpers

Helper Zweck Beispiel
pytest.raises() Exceptions testen with pytest.raises(ValueError, match=r"pattern"):
pytest.warns() Warnings testen with pytest.warns(UserWarning, match=r"pattern"):
pytest.deprecated_call() Deprecations testen with pytest.deprecated_call():
pytest.approx() Fließkomma-Vergleich assert x == pytest.approx(expected, rel=1e-6)
pytest.fail() Explizites Fehlschlagen pytest.fail("Custom message")
pytest.skip() Test überspringen pytest.skip("Reason")
pytest.xfail() Erwarteter Fehlschlag pytest.xfail("Known bug")
pytest.importorskip() Überspringen bei Import-Fehlschlag plt = pytest.importorskip("matplotlib.pyplot")
@pytest.mark.parametrize Mit mehreren Eingaben ausführen @pytest.mark.parametrize("x,y", [(1,2), (3,4)])

pytest.approx-Standard-Toleranzen

Parameter Standard-Wert Bedeutung
rel 1e-6 Relative Toleranz (0.0001%)
abs 1e-12 Absolute Toleranz
nan_ok False NaN-Vergleiche erlauben
Toleranz-Formel: \( \text{actual} - \text{expected} \leq \max(\text{rel} \times \text{expected} , \text{abs})\)

Häufige Pytest-Marker

Marker Zweck Beispiel
@pytest.mark.skip Test überspringen @pytest.mark.skip(reason="Not implemented")
@pytest.mark.skipif Bedingtes Überspringen @pytest.mark.skipif(sys.platform == "win32", reason="Unix only")
@pytest.mark.xfail Erwarteter Fehlschlag @pytest.mark.xfail(reason="Known bug #123")
@pytest.mark.parametrize Test parametrisieren @pytest.mark.parametrize("x", [1, 2, 3])
@pytest.mark.filterwarnings Warnings filtern @pytest.mark.filterwarnings("ignore::DeprecationWarning")

Wann welches Muster verwenden

Szenario Verwenden Sie dies Nicht dies
Exceptions testen pytest.raises(ValueError, match=r"...") try/except-Blöcke
Fließkomma-Vergleich pytest.approx() abs(x - y) < epsilon
Mehrere Test-Fälle @pytest.mark.parametrize for-Schleifen in Tests
Ihren Code testen Ergebnisse assertieren (Zustand) Interne Funktionen mocken (Interaktion)
Test-Isolation Fixtures Globale Variablen
Plattformspezifische Tests @pytest.mark.skipif if sys.platform ... im Test
Erwartete Fehlschläge @pytest.mark.xfail Tests auskommentieren

Fazit

Sie haben jetzt eine umfassende Referenz für pytest-Assertion-Muster und Best Practices. Wichtigste Erkenntnisse:

Wichtigste Muster

  1. match-Parameter verwenden - Exception-Nachrichten validieren: pytest.raises(ValueError, match=r"pattern")
  2. pytest.approx() verwenden - Für alle Fließkomma-Vergleiche, nicht nur Skalare
  3. Tests parametrisieren - @pytest.mark.parametrize verwenden, keine Schleifen
  4. Zustand testen, nicht Interaktionen - Ergebnisse assertieren, nicht Methodenaufrufe
  5. Tests deterministisch halten - Random seeden, Zeit fixieren, geteilten Zustand vermeiden

Red Flags in LLM-generierten Tests

Achten Sie auf diese Anti-Patterns:

Im Zweifel

  1. pytest-Docs prüfen - https://docs.pytest.org/
  2. Nach existierenden Mustern suchen - Codebase nach ähnlichen Tests durchsuchen
  3. Fragen: “Teste ich Verhalten oder Implementierung?” - Verhalten testen
  4. Fragen: “Ist dieser Test deterministisch?” - Deterministisch machen
  5. Fragen: “Könnte dies parametrisiert werden?” - Wahrscheinlich ja

Nächste Schritte

Viel Erfolg beim Testen!


Referenzen

Offizielle Dokumentation

Python-Standards

Best Practices und Artikel

Kursmaterialien

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk