Anhang 3: Pytest Assertion Referenz - Beyond the Basics
November 2025 (12043 Words, 67 Minutes)
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:
- Advanced Exception Testing - Über das grundlegende
pytest.raises()hinaus - pytest.approx Deep Dive - Fließkomma-Vergleiche für komplexe Datenstrukturen
- Warning Testing - Testen von Deprecation Warnings und User Warnings
- Test Control - Tests überspringen, fehlschlagen lassen und markieren
- Assertion Introspection - pytest’s Assertion-Rewriting-Magie verstehen
- Parametrization Patterns - Mehrere Szenarien effizient testen
- Common Anti-Patterns - Was man NICHT tun sollte (besonders häufig in LLM-generierten Tests)
- Quick Reference - Cheat Sheets und Nachschlagetabellen
Was Sie bereits wissen
Aus Kapitel 03 (Grundlagen des Testens) kennen Sie bereits:
- Boundary Value Analysis - Testen an Grenzen wie
sys.float_info.max,math.inf,math.ulp() - Basic pytest.approx() - Vergleich von Fließkommazahlen mit Toleranzen
- Basic pytest.raises() - Testen, dass Code Exceptions auslöst
- State Testing - Ergebnisse testen, nicht Methodenaufrufe
- AAA Pattern - Arrange, Act, Assert
Dieser Anhang baut auf dieser Grundlage mit erweiterten Mustern und Best Practices auf.
Wie Sie diesen Anhang verwenden
Beim Schreiben von Tests:
- Springen Sie zum relevanten Kapitel für schnelle Syntax-Nachschlagewerte
- Prüfen Sie den Anti-Patterns-Abschnitt, wenn sich etwas falsch anfühlt
- Verwenden Sie Quick-Reference-Tabellen für Überblicke
Beim Review von LLM-generierten Tests:
- Prüfen Sie Kapitel 7 auf häufige Anti-Patterns
- Verifizieren Sie, dass Exception-Tests den
match-Parameter verwenden (Kapitel 1) - Stellen Sie sicher, dass Parametrisierung angemessen verwendet wird (Kapitel 6)
Beim Debuggen fehlgeschlagener Tests:
- Kapitel 5 erklärt Assertion-Introspection-Ausgaben
- Kapitel 2 behandelt pytest.approx-Toleranzprobleme
- Kapitel 4 behandelt Test-Control für flaky Tests
Wichtige Referenzen
In diesem Anhang verweisen wir auf:
- Offizielle pytest-Dokumentation: https://docs.pytest.org/
- pytest API-Referenz: https://docs.pytest.org/en/stable/reference/reference.html
- Software Engineering at Google (O’Reilly) - Testing-Kapitel
- IEEE 754 Standard - Fließkomma-Arithmetik
- NerdWallet Engineering Blog - pytest Best Practices
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?
- Spezifität - Stellt sicher, dass Sie die RICHTIGE Exception aus dem RICHTIGEN Grund auslösen
- Regressionsprävention - Fängt Änderungen an Fehlermeldungen ab
- 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:
- Verwenden Sie Raw-Strings -
match=r"pattern", um Backslash-Escaping zu vermeiden matchverwendetre.search()- Pattern kann überall in der Nachricht erscheinen (kein vollständiger String-Match)- 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)
- 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:
exc_info.type- Exception-Klasse (z.B.ValueError)exc_info.value- Exception-Instanz (das tatsächliche Exception-Objekt)exc_info.traceback- Traceback-Objekt (für erweitertes Debugging)
Wann ExceptionInfo verwenden:
- Testen von Custom-Exception-Attributen
- Validieren von Exception-Ursachen (über
__cause__oder__context__) - Debuggen komplexer Exception-Ketten
- Testen mehrerer Bedingungen über die Exception
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:
rel=1e-6(relative Toleranz: 0.0001%)abs=1e-12(absolute Toleranz)
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?
- Relative Toleranz skaliert mit Größenordnung (gut für große Zahlen)
- Absolute Toleranz behandelt Werte nahe null (wo relative Toleranz zusammenbricht)
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:
- Längen müssen übereinstimmen - pytest.approx schlägt fehl, wenn Sequenzen unterschiedliche Längen haben
- Typen müssen übereinstimmen - Kann Liste nicht mit Tupel vergleichen (erst konvertieren)
- 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:
- Schlüssel müssen exakt übereinstimmen - pytest.approx prüft keine Schlüssel, nur Werte
- Nur numerische Werte verglichen - Nicht-numerische Werte mit
==geprüft - 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:
sys.float_info.max≈ 1.8 × 10³⁰⁸ - Größte endliche Floatmath.inf- Nicht-endlicher Spezialwert (größer als jede endliche Zahl)
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:
UserWarning- Allgemeine Warnungen an BenutzerDeprecationWarning- Features, die auslaufenFutureWarning- Bevorstehende Breaking ChangesRuntimeWarning- Verdächtiges Laufzeitverhalten (z.B. Division durch Null in NumPy)
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:
pytest.warns(DeprecationWarning)- Wenn Sie Nachricht matchen möchtenpytest.deprecated_call()- Wenn Sie nur Deprecation verifizieren möchten (jedes Deprecation-Warning)
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:
- Sie Ihre eigene API deprecaten - Sicherstellen, dass Warnings korrekt ausgegeben werden
- Sie Bibliotheks-Warnings umgehen - Erwartete Warnings in Tests dokumentieren
- Sie numerischen Code testen - Warnings für Edge-Cases verifizieren (Overflow, Underflow, Division durch Null)
Warnings nicht testen wenn:
- Sie von Drittanbieter-Bibliotheken stammen - Nicht Ihre Verantwortung (stattdessen filtern)
- 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:
- Komplexe bedingte Logik - Wenn einfaches
assertnicht ausdrucksstark genug ist - Platzhalter-Tests - Tests als TODO markieren
- 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:
- Plattformspezifische Tests - Auf nicht unterstützten Plattformen überspringen
- Abhängigkeitsbasierte Tests - Überspringen, wenn optionale Abhängigkeit fehlt
- Langsame Tests - Während schneller Entwicklung überspringen
- 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:
- Bekannte Bugs - Bugs mit fehlschlagenden Tests dokumentieren (besser als Tests löschen!)
- Unvollständige Features - Tests für Features vor Implementierung schreiben (TDD)
- 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:
skip- Test läuft nicht, markiert als “skipped”xfail- Test läuft, erwartet fehlzuschlagen, markiert als “xfail” bei Fehlschlag oder “xpass” bei unerwartetem Bestehen
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:
- Klarere Absicht - Offensichtlich Überspringen wegen fehlender Abhängigkeit
- Versions-Prüfung - Kann Mindestversion spezifizieren
- 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:
- Zwischenwerte erfassen - Zeigt Subexpressions in fehlgeschlagenen Assertions
- Kontext liefern - Zeigt umgebende Codezeilen
- 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:
- Die Werte von
resultundexpectedzeigt - Welcher Index unterschiedlich ist, identifiziert
- Vorschlägt,
-vfür mehr Details zu verwenden
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):
- Custom-Nachricht deaktivierte Introspection
- Sie mussten wählen: Introspection ODER Custom-Nachricht
Modernes pytest (7.0+):
- Custom-Nachricht hängt an Introspection an
- Sie erhalten beides Introspection UND Custom-Nachricht!
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:
- Komplexe Bedingungen - Erklären, WARUM die Assertion wichtig ist
- Domänenspezifische Prüfungen - Kontext über das Getestete hinzufügen
- Debugging-Hinweise - Fixes oder verwandte Tests vorschlagen
Wann KEINE Custom-Nachrichten hinzufügen:
- Einfache Vergleiche - Introspection bereits klar
- 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:
- Custom-Typen mit komplexer Vergleichslogik
- Wenn Standard-Introspection nicht hilfreich ist
- Domänenspezifische Typen (z.B. Straßenprofil-Datenstrukturen)
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 == 10 → 5 != 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:
- Reduziert Duplikation - Gleiche Testlogik, unterschiedliche Daten
- Klare Testnamen - Jeder Parameter-Satz erstellt separaten Test
- Granulare Fehlschläge - Wissen genau, welche Eingabe fehlschlug
- 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:
indirect=Truesagt pytest, Parameter an Fixture zu übergeben, nicht Test- Fixture erhält Parameter über
request.param - Fixture gibt verarbeiteten Wert an Test zurück
Wann verwenden:
- Teures Setup (Daten laden, Datenbank erstellen, etc.)
- Komplexe Test-Datengenerierung
- Geteiltes Setup über mehrere Tests
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:
- Stoppt bei erstem Fehlschlag - Sie sehen nicht alle fehlschlagenden Fälle
- Keine granulare Berichterstattung - Wissen nicht aus Testname, welcher Fall fehlschlug
- 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:
- Alle Fehlschläge gemeldet - Führt alle Fälle aus, selbst wenn einige fehlschlagen
- Granulare Testnamen -
test_double[1-2],test_double[2-4], etc. - 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:
- Schwer zu verstehen - Assertion prüft nicht nur, sie tut etwas!
- Fragil - Hängt von Assertion-Ausführungsreihenfolge ab
- 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:
- Testet Implementierung, nicht Verhalten - Bricht beim Refactoring
- Testet eigentliche Logik nicht - Mocks umgehen echten Code
- 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:
- Externe Services - APIs, Datenbanken, Dateisysteme
- Langsame Operationen - Netzwerkaufrufe, große Berechnungen
- Nicht-deterministisches Verhalten - Random, zeitabhängig
Wann NICHT mocken:
- Ihre eigenen Funktionen - Direkt testen
- Einfache Helpers - Schneller auszuführen als zu mocken
- 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:
- Flaky Tests - Bestehen manchmal, schlagen manchmal fehl (zerstört Vertrauen in Tests)
- Schwer zu debuggen - Kann Fehlschläge nicht reproduzieren
- 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:
- Reihenfolgeabhängig - Tests bestehen/schlagen fehl abhängig von Ausführungsreihenfolge
- Bricht Isolation - Tests beeinflussen sich gegenseitig
- 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:
- Zu locker:
with pytest.raises(Exception)- Fängt JEDE Exception - Zu strikt: Exakter String-Match - Bricht bei kleinen Änderungen
- Genau richtig: Regex-Match für Schlüsselphrasen
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
match-Parameter verwenden - Exception-Nachrichten validieren:pytest.raises(ValueError, match=r"pattern")- pytest.approx() verwenden - Für alle Fließkomma-Vergleiche, nicht nur Skalare
- Tests parametrisieren -
@pytest.mark.parametrizeverwenden, keine Schleifen - Zustand testen, nicht Interaktionen - Ergebnisse assertieren, nicht Methodenaufrufe
- Tests deterministisch halten - Random seeden, Zeit fixieren, geteilten Zustand vermeiden
Red Flags in LLM-generierten Tests
Achten Sie auf diese Anti-Patterns:
return-Statements in Testsfor-Schleifen über Test-Fälle- Exzessives Mocking (besonders Ihres eigenen Codes)
- Kein
match-Parameter inpytest.raises() - Random/zeitabhängige Daten ohne Seeding
Im Zweifel
- pytest-Docs prüfen - https://docs.pytest.org/
- Nach existierenden Mustern suchen - Codebase nach ähnlichen Tests durchsuchen
- Fragen: “Teste ich Verhalten oder Implementierung?” - Verhalten testen
- Fragen: “Ist dieser Test deterministisch?” - Deterministisch machen
- Fragen: “Könnte dies parametrisiert werden?” - Wahrscheinlich ja
Nächste Schritte
- Diese Muster anwenden auf Ihre Road Profile Viewer Tests
- Existierende Tests überprüfen auf Anti-Patterns
- LLM-generierte Tests refactoren mit diesen Best Practices
- Zu TDD übergehen (Kapitel 03 (TDD und CI)) mit soliden Assertion-Fähigkeiten
Viel Erfolg beim Testen!
Referenzen
Offizielle Dokumentation
- pytest-Dokumentation: https://docs.pytest.org/
- pytest API-Referenz: https://docs.pytest.org/en/stable/reference/reference.html
- Assertion Introspection: https://docs.pytest.org/en/stable/how-to/assert.html
- Parametrisierung: https://docs.pytest.org/en/stable/how-to/parametrize.html
- Warning Capture: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
Python-Standards
- IEEE 754 Floating-Point: https://en.wikipedia.org/wiki/IEEE_754
- PEP 654 - Exception Groups: https://peps.python.org/pep-0654/
- What Every Computer Scientist Should Know About Floating-Point: https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
Best Practices und Artikel
- Software Engineering at Google (O’Reilly) - Testing-Kapitel
- Real Python: Effective Python Testing With Pytest: https://realpython.com/pytest-python-testing/
- NerdWallet Engineering: 5 Pytest Best Practices: https://www.nerdwallet.com/blog/engineering/5-pytest-best-practices/
Kursmaterialien
- Kapitel 03 (Testing-Grundlagen): Testing-Grundlagen, AAA-Pattern
- Kapitel 03 (Grenzwertanalyse): Boundary Value Analysis, State Testing, IEEE 754
- Kapitel 03 (TDD und CI): Test-Driven Development (TDD) und CI/CD