Schlüssel zum Mitnehmen
- Pytest-Grundlagen decken Einheitentest ab. Fixtures, Parametrisierung und
pytest.approxsind Ihr Ausgangspunkt, nicht die Ziellinie. - Eigenschaftenbasierte Tests mit Hypothese fangen Randfälle ab, die manuelle Tests häufig übersehen. Es wird von wichtigen wissenschaftlichen Python-Projekten wie Numpy, Jax und Pytorch verwendet.
- Numerische Konvergenztests erfordern Solver-spezifische Toleranzstrategien. Standard-Behauptungen erfassen kein Konvergenzverhalten.
- Integrationstests für Simulations-Workflows erfordern Spottmuster, die externe Abhängigkeiten isolieren und gleichzeitig die numerische Logik beibehalten.
- Reproduzierbarkeitstests gehen über Unit-Tests hinaus. Sie benötigen ein deterministisches Saatgutmanagement und eine plattformübergreifende Gleitkommaverifizierung.
Was Sie zuerst wissen sollten
Wenn Sie die Grundlagen des Unit-Testings für wissenschaftlichen Code bereits kennen, verstehen Sie wahrscheinlich, wie man eine test_-Funktion schreibt, wann Fixtures verwendet werden und wie pytest.approx Gleitkommavergleiche handhabt. Das ist die Grundlinie.
Der wissenschaftliche Kodex stellt Herausforderungen vor, die die Standard-Einheitentests nicht vollständig angehen. Numerische Algorithmen konvergieren oder konvergieren nicht. Simulationen hängen von externen Lösern und I/O-Systemen ab. Fließkommaarithmetik kann sich über Plattformen hinweg unterschiedlich verhalten.
Randfälle, die in normalen Daten niemals auftreten, wie z.
Dieser Artikel behandelt Testmuster für wissenschaftlichen Python-Code über die Grundlagen hinaus.
Die Testpyramide für wissenschaftlichen Code
Jede Teststrategie hat einen Platz auf der Pyramide. Für den wissenschaftlichen Code sieht die Struktur wie folgt aus:
┌─────────────────────────────┐
│ System / Simulation Tests │ ← Integration-level workflows
│ (3-5 tests) │
├─────────────────────────────┤
│ Integration / Workflow │ ← Multi-step simulations
│ Tests (10-20 tests) │ ← Data pipelines
├─────────────────────────────┤
│ Property-Based Tests │ ← Hypothesis with numerical properties
│ (50-100 tests) │ ← Convergence testing
├─────────────────────────────┤
│ Unit Tests │ ← Numerical assertions
│ (50-200 tests) │ ← Fixtures for solvers and meshes
└─────────────────────────────┘
Unit-Tests bilden die Grundlage. Über ihnen befinden sich eigenschaftsbasierte Tests, Integrationstests und Simulationstests auf Systemebene. Diese höheren Schichten machen das Testen für echte wissenschaftliche Arbeitsabläufe nützlich.
Eigenschaftenbasiertes Testen mit Hypothese
Warum eigenschaftsbasierte Tests für wissenschaftlichen Code wichtig sind
Hypothese ist nicht nur ein Lehrmittel. Es wird von großen wissenschaftlichen Python-Bibliotheken verwendet, um Fehler in numerischen Kernroutinen zu finden.
Eigenschaftenbasierte Tests verändern das mentale Modell des Tests. Anstatt Tests mit einer bestimmten Eingabe und einer erwarteten Ausgabe zu schreiben, schreiben Sie Tests, die die mathematischen Eigenschaften überprüfen. Die Hypothese generiert dann automatisch viele Eingaben, einschließlich Kantenfällen, die Sie möglicherweise nicht manuell testen möchten.
Konvertieren eines handgeschriebenen Tests in eigenschaftsbasierte
Betrachten Sie diesen Test für einen numerischen Solver-Wrapper:
# Before: handwritten test with single case
def test_solver_preserves_mass():
"""Check mass conservation for one mesh size."""
result = solver_solve(mass_mesh, dt=0.01)
mass_before = sum(result[0])
mass_after = sum(result[-1])
assert mass_after / mass_before == pytest.approx(1.0, rel=1e-3)
Dieser Test überprüft eine Maschengröße und einen Zeitschritt. Es kann sogar passieren, wenn der Solver für größere Netze oder unterschiedliche Zeitschrittwerte ausfällt.
Hier eine Hypothesenversion:
from hypothesis import given, strategies as st
from hypothesis import settings, health_check, suppress
@given(
st.integers(min_value=5, max_value=200),
st.floats(min_value=1e-6, max_value=1.0)
)
@settings(max_examples=100, deadline=None)
@health_check("init_consistency_suppressor")
def test_solver_mass_conservation(n_cells, dt):
"""Mass should be preserved across valid mesh sizes and timesteps."""
mesh = create_mesh(n_cells)
result = solver_solve(mesh, dt=dt)
mass_before = float(sum(result[0]))
mass_after = float(sum(result[-1]))
ratio = mass_after / mass_before
assert abs(ratio - 1.0) < 1e-2
Dies fängt Probleme auf, die ein handgeschriebener Test übersehen kann:
- Eckfälle in der Netzgenerierung, einschließlich dünner Domänen und extremer Seitenverhältnisse.
- Zeitschrittsensitivität, insbesondere wenn große
dt-Werte explizite Schemata destabilisieren. - Gleitkomma-Kantenfälle, einschließlich Rundungsgrenzen und ungewöhnliche numerische Bereiche.
Die Eigenschafts-Checkliste für wissenschaftliche Funktionen
Identifizieren Sie vor dem Schreiben eines Eigenschaftstests, welche Eigenschaften die Funktion erfüllen soll.
| Eigenschaftstyp | Beispiel | Warum es wichtig ist |
|---|---|---|
| Erhaltung | Masse, Energie oder Impuls bleiben erhalten | Verhindert numerische Drift |
| Symmetrie | f(a, b) == f(b, a) für symmetrische Operatoren |
Fängt asymmetrische Implementierungsfehler ab |
| Skalieren | f(k*a, k*b) == k^n * f(a, b) für homogene Funktionen |
Tests die dimensionale Konsistenz |
| Monotonie | Die Temperatur sinkt mit der Entfernung in einem einfachen Hitzeproblem | Überprüft die körperliche Korrektheit |
| Grenzen | Werte bleiben in einem gültigen Bereich | Verhindert nicht-physische Ergebnisse |
| Null-Verhalten | f(0) Gibt den erwarteten Flanken-Case-Wert zurück |
Schutz gegen Division durch null und einzelne Fälle |
Numerische Konvergenzprüfung
Warum Konvergenztests anders ist
Einheitentests prüfen, ob Code eine bestimmte Antwort erzeugt. Konvergenztests prüfen, ob der Code bei der Verfeinerung der Parameter der richtigen Antwort nähert.
Dies ist entscheidend für iterative Löser, Zeitschrittschemata und Netzverfeinerungsstudien. Standard-Behauptungen wie assert result == expected erfassen kein Konvergenzverhalten. Sie müssen Konvergenzmuster testen.
Testen der Solver-Konvergenz
Verschiedene Solver-Suiten verwenden unterschiedliche Konvergenzkriterien. Der entscheidende Punkt ist, dass die Konvergenzkriterien solverspezifisch sind und die Standardtoleranz möglicherweise nicht für jedes Problem geeignet ist.
import numpy as np
def test_solver_convergence():
"""Test that the solver converges with mesh refinement."""
residuals = []
for n in [16, 32, 64, 128, 256]:
mesh = create_mesh(n)
result = solve_with_convergence(
mesh,
criterion="default",
tolerance=1e-8
)
residuals.append(result.residual)
residuals = np.array(residuals)
# Test that residuals decrease monotonically with tolerance
for i in range(1, len(residuals)):
if residuals[i] > residuals[i-1] * 1.01:
raise AssertionError(
f"Residual increased at refinement level {i}: "
f"{residuals[i-1]:.2e} → {residuals[i]:.2e}"
)
# Test convergence rate
rate = np.log(residuals[0] / residuals[-1]) / np.log(16)
assert rate >= 1.8, f"Convergence rate too slow: {rate:.2f}"
Toleranzstrategien
Tests sollten die gängigsten Toleranzkriterien abdecken, die vom Solver-Stack verwendet werden.
@pytest.mark.parametrize("criterion", [
"default",
"unscaled",
"preconditioned",
"natural",
])
def test_convergence_criteria(criterion):
"""Verify each criterion produces reasonable residuals."""
mesh = create_mesh(64)
result = solve_with_convergence(mesh, criterion=criterion)
assert result.status in (
"convergence",
"absolute_tolerance_convergence",
"relative_tolerance_convergence",
"happy_breakdown"
) or result.residual < 1e-2
Divergenzfälle testen
Sie sollten auch testen, dass der Code die Abweichung ordnungsgemäß behandelt.
def test_divergence_handling():
"""Code should not crash on divergent cases."""
mesh = create_mesh(64)
result = solve_with_convergence(
mesh,
criterion="default",
divergence_tolerance=100
)
assert hasattr(result, "status")
assert hasattr(result, "residual")
Integrationstests für Simulationsworkflows
Warum Integrationstests wichtig sind
Unit-Tests überprüfen einzelne Funktionen. Integrationstests stellen sicher, dass die Funktionen korrekt zusammenarbeiten.
Für den wissenschaftlichen Code bedeutet dies das Testen von mehrstufigen Simulationen, Datenpipelines, Solver-Kopplungsmustern und E / A-Workflows.
Muster 1: Verspotten von externen Abhängigkeiten im wissenschaftlichen Kodex
Beim Testen von Funktionen, die externe APIs, Datenbanken oder Dateisysteme aufrufen, verwenden Sie Spott, um die numerische Logik zu isolieren.
from unittest.mock import patch, MagicMock
def test_simulation_io():
"""Test simulation I/O without actually writing to disk."""
with patch("h5py.File") as mock_file:
mock_dataset = MagicMock()
mock_file.return_value = MagicMock()
mock_file.return_value.__enter__ = MagicMock(return_value=mock_file)
mock_file.return_value.__exit__ = MagicMock(return_value=None)
data = prepare_simulation_output(mesh_size=(100, 100, 100))
assert "simulation_data" in mock_file.return_value.keys()
assert data.shape == (100, 100, 100)
Dies isoliert die Datenaufbereitungslogik aus der HDF5-Datei Write. Der Test überprüft, ob Ihre Code-Formate und -pakete korrekt sind, ohne auf E / A zu warten.
Muster 2: Parametrisierte Fixtures für Simulations-Sweeps
Anstatt einige Netzgrößen manuell zu testen, verwenden Sie parametrisierte Vorrichtungen für systematische Sweeps.
@pytest.fixture(params=[10, 20, 40, 80, 160])
def mesh_sizes(request):
"""Systematic mesh refinement series."""
return request.param
@pytest.fixture(params=[
(1e-3, "fine"),
(1e-2, "medium"),
(1e-1, "coarse")
])
def timesteps(request):
"""Test different timestep regimes."""
dt, label = request.param
return dt, label
def test_convergence_with_sweeps(mesh_sizes, timesteps):
"""Test convergence across mesh and timestep refinement."""
dt, label = timesteps
result = solve_convergence_study(
mesh_sizes,
dt=dt,
quantity="mass_conservation"
)
assert len(result.residuals) == len(result.mesh_sizes)
assert all(r > 0 for r in result.residuals)
Muster 3: Testen von mehrstufigen Workflows
Wissenschaftliche Simulationen verfügen häufig über mehrstufige Pipelines: Generierung, Lösung, Nachbearbeitung und Schreibausgabe.
def test_full_workflow():
"""Test the complete simulation pipeline."""
# Step 1: Generate mesh
mesh = generate_mesh(geometry="block", resolution=40)
# Step 2: Solve
solution = solve(mesh, solver="scipy", tolerance=1e-8)
# Step 3: Post-process
metrics = compute_metrics(solution, quantities=["mass", "energy"])
# Step 4: Verify outputs
assert metrics["mass"] > 0
assert metrics["energy"] < 1e6
# Step 5: Write output
output_path = write_output(solution, path="/tmp/test_output.h5")
# Verify output is readable
readback = h5py.File(output_path, "r")
assert "solution" in readback.keys()
Fortgeschrittenes Verspotten für wissenschaftliche Bibliothek
Herausforderung
Das Testen von wissenschaftlichem Code kann langsam sein, da numpy-, scipy- und fipy-Funktionen komplexe Rückgabetypen und teure Ausführungspfade haben können. Wenn Sie sie verspotten, können Sie die Wrapper-Logik schnell testen.
import numpy as np
from unittest.mock import patch
def test_solver_wrapper():
"""Test that your wrapper calls scipy.sparse.linalg correctly."""
with patch("scipy.sparse.linalg.spsolve") as mock_solve:
mock_solve.return_value = np.zeros(100)
result = your_solver_wrapper(mesh_size=100)
mock_solve.assert_called_once()
call_args = mock_solve.call_args
assert isinstance(call_args[0], np.ndarray)
assert isinstance(call_args[1], np.ndarray)
Dadurch wird die Wrapper-Logik überprüft, ohne dass der tatsächliche Solver ausgeführt wird. Der Test kann in Millisekunden statt Sekunden beendet werden.
Testen von Hin- und Rückfahrten für wissenschaftliche Daten
Das Testen, dass wissenschaftliche Daten einen Lese-Schreib-Lese-Zyklus überstehen, ist ein leistungsstarkes Muster.
from hypothesis import given, strategies as st
@given(st.integers(min_value=10, max_value=200))
def test_hdf5_roundtrip(n_cells):
"""Writing and reading HDF5 data should preserve values."""
mesh = create_mesh(n_cells)
solution = solve(mesh, dt=0.01)
path = write_to_hdf5(solution, path="/tmp/test_roundtrip.h5")
readback = read_from_hdf5(path)
assert np.allclose(readback, solution, rtol=1e-6, atol=1e-8)
Dadurch werden Datenserialisierungsfehler, DType-Fehlanpassungen und HDF5-Chunking-Probleme erfasst, die die Ergebnisse stillschweigend beschädigen könnten.
Reproduzierbarkeit testen
Deterministisches Saatgutmanagement
Gleitkommaoperationen können deterministisch sein, wenn der Seed festgelegt ist, aber die Reproduzierbarkeit erfordert auch konsistente Bibliotheksversionen, Kompilierungsflags und MPI-Dekompositionsmuster.
def test_reproducible_seed():
"""Fixed seed should produce identical results."""
np.random.seed(42)
mesh1 = generate_mesh(seed=42)
solution1 = solve(mesh1)
np.random.seed(42)
mesh2 = generate_mesh(seed=42)
solution2 = solve(mesh2)
assert np.allclose(solution1, solution2)
Plattformübergreifende Gleitkomma-Tests
Verschiedene Plattformen können aufgrund von Hardware- und Bibliotheksunterschieden leicht unterschiedliche Gleitkommaergebnisse erzeugen.
def test_floating_point_stability():
"""Results should be consistent across platforms within tolerance."""
mesh = create_mesh(64)
solution = solve(mesh)
# All values should be finite
assert np.all(np.isfinite(solution))
# Values should not be suspiciously small
assert np.all(np.abs(solution) > np.finfo(np.float64).tiny * 1e-3)
Was wir empfehlen: Die Teststrategie
Verwenden Sie für ein produktionswissenschaftliches Python-Projekt eine geschichtete Teststrategie.
Haben müssen
- Unit-Tests mit
pytest.approxfür numerische Behauptungen. - Parametrisierte Geräte für systematische Tests über Parameter hinweg.
- Konvergenztests, die das Solververhalten während der Verfeinerung überprüfen.
Sollte haben
- Hypotheseneigenschaftsbasierte Tests für Randfälle und numerische Robustheit.
- Integrationstests für mehrstufige Simulationsworkflows.
- Mocking-Strategien für externe Abhängigkeiten und Solver-Schnittstellen.
Schön zu haben
- Plattformübergreifende Reproduzierbarkeitstests.
- Daten-Rundtrip-Tests, die wissenschaftliche Daten schreiben, lesen und überprüfen.
- Leistungsbenchmarks mit
pytest-benchmark.
Häufige Fehler
Fehler 1: Toleranzabhängige Aussagen falsch verwenden
Schreiben Sie keine numerischen Tests wie assert result == expected. Verwenden Sie pytest.approx oder toleranzbewusste Zusicherungen für numerischen Code. Die Toleranz hängt von der Problemskala ab, so dass eine feste Toleranz bei größeren Problemen fehlschlägt.
Fehler 2: Konvergenz ignorieren
Wenn der Code iterative Solver verwendet, überspringen Sie keine Konvergenztests. Ein Solver, der auf einem Netz arbeitet, kann auf einem anderen divergieren. Testen Sie das Konvergenzverhalten, nicht nur das Endergebnis.
Fehler 3: Nur den glücklichen Weg testen
Property-basierte Tests zwingen Sie dazu, Randfälle zu testen, die manuelle Tests häufig übersehen. Fügen Sie mindestens einen Hypothesentest für jede wichtige numerische Funktion hinzu. Es kann Fehler aufdecken, die normale beispielbasierte Tests niemals abfangen.
Zusammenfassung
Unit-Tests sind notwendig, aber sie reichen für den wissenschaftlichen Kodex nicht aus. Property-basierte Tests, Konvergenztests, Integrationsworkflows und Reproduzierbarkeitsprüfungen befassen sich mit den spezifischen Herausforderungen, die numerische Simulationen mit sich bringen.
Die wichtigste Erkenntnis ist, dass es bei wissenschaftlichen Tests nicht nur darum geht, zu überprüfen, ob Code eine richtige Antwort liefert. Es geht darum zu überprüfen, ob sich der Code richtig verhält. Es sollte elegant konvergieren, konservieren, skalieren und bearbeiten.
Beginnen Sie mit den Must-Have-Mustern und fügen Sie dann die Muster hinzu, wenn die Testsuite ausgereift ist. Verwenden Sie die schönen Muster, um die Codebasis im Laufe der Zeit zu härten.
Property-basierte Tests mit Hypothese sind eine der hochwertigsten Ergänzungen, die Sie vornehmen können, da sie Fehler auffangen, die manuelle Tests häufig übersehen.
Verwandte Anleitungen
- Einheitentest für wissenschaftlichen Kodex: PYTest-Strategien für Forschungsprojekte – Die Grundlage, die Sie vor diesem Artikel benötigen.
- Wann verwendet werden FEM, FVM oder FDM: Ein praktischer Vergleich für Anfänger – Kontext zu numerischen Methoden.
- Mesh-Qualitäts- und Konvergenzstudien: Ein praktischer Leitfaden für wissenschaftliche Simulationen – Verwandter Konvergenztestkontext.
Was macht man als nächstes
Wenn Sie gerade angefangen haben, Tests für wissenschaftlichen Code zu schreiben, beginnen Sie mit den oben genannten Mustern. Sobald diese vorhanden sind, fügen Sie Hypothesentests für die wichtigsten numerischen Funktionen hinzu, insbesondere Funktionen, die in Simulationen wiederholt aufgerufen werden.
Für Teams, die an produktiven wissenschaftlichen Software arbeiten, kann sich die Investition in objektbasierte Tests schnell auszahlen. Es handelt sich um dieselbe Strategie, die von großen wissenschaftlichen Python-Bibliotheken verwendet wird, und ist eine der effektivsten Möglichkeiten, um Edge-Case-Fehler im numerischen Code zu finden.
Wenn Sie Hilfe beim Entwerfen einer Teststrategie für ein Simulationsprojekt benötigen, sollten Sie eine Beratung buchen. Eine praktische Überprüfung kann Ihre Codestruktur auf Einheitentests, Konvergenztests, Integrationstests und Reproduzierbarkeitsprüfungen abbilden.