6.2 KiB
Tests
Tests ausführen
Voraussetzung ist die Entwicklungsumgebung aus dem README:
pip install -r requirements-dev.txt
Alle Tests ausführen (Dauer etwa 15 s):
pytest
Mit Testabdeckung und Liste der nicht abgedeckten Zeilen:
pytest --cov=sandbox --cov-report=term-missing
Nur die schnellen Tests (ohne Stresstest und Smoke-Test):
pytest -m "not slow"
Lint und Formatierung prüfen:
ruff check src tests
ruff format --check src tests
Ruff prüft neben Stil und typischen Fehlern auch, dass alle Module, Klassen und
öffentlichen Funktionen einen Docstring (D) und Type Hints (ANN) haben und dass in
Vergleichen keine Magic Numbers stehen (PLR2004). Die Konfiguration steht in
pyproject.toml.
Headless-Modus
UI- und Integrationstests laufen ohne sichtbares Fenster. tests/conftest.py setzt vor
dem ersten Import von pygame:
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
SDL rendert dann in einen unsichtbaren Puffer und braucht weder Bildschirm noch Soundkarte. Die Tests laufen so auch in CI oder über SSH.
Die UI-Tests bedienen die Anwendung wie ein Mensch: Sie legen echte pygame-Events
(MOUSEBUTTONDOWN, MOUSEMOTION, KEYDOWN …) in die Event-Queue und rufen dann
App.frame() auf. Die Hilfsfunktionen dafür stehen in tests/app/driver.py. So wird die
ganze Kette getestet: Event → Sidebar/InputHandler → Werkzeuge → Physik → Zeichnen.
Die Anwendung selbst lässt sich ebenfalls headless starten, etwa für Screenshots. PowerShell:
$env:SDL_VIDEODRIVER = "dummy"; python -m sandbox --demo --screenshot bild.png --seconds 2
Bash:
SDL_VIDEODRIVER=dummy python -m sandbox --demo --screenshot bild.png --seconds 2
Was wird getestet?
Die Erwartungswerte der Physik-Tests stammen aus der klassischen Mechanik – Impulserhaltung, Stoßgesetz, Coulomb-Reibung, Rollbedingung – und nicht aus der Implementierung selbst.
Physik (tests/physics/, ohne pygame)
| Datei | Inhalt |
|---|---|
test_vector.py |
alle Vektoroperationen, Nullvektor-Normalisierung ohne NaN, Unveränderlichkeit, Hash/Gleichheit, Kopieren/Pickle |
test_bodies.py |
Fläche und Trägheitsmoment, Masse = Fläche × Dichte, statische Körper, Materialprüfung, Impulse mit/ohne Drehung, Energie und Impuls |
test_collision.py |
jede Formpaarung mit Normalenrichtung, Tiefe und Kontaktpunkt; Randfälle (gleicher Mittelpunkt, Kreismittelpunkt im Rechteck, knapp verfehlte Ecke, Körper hinter der Wand); Zufallstest mit über 500 Kontakten: Verschieben um die Tiefe trennt die Körper |
test_broadphase.py |
Sweep-and-Prune liefert dieselben Paare wie der Brute-Force-Vergleich |
test_solver.py |
elastischer Stoß: Impuls- und Energieerhaltung, Lehrbuchformel; e = 0: gemeinsame Geschwindigkeit; Energieverlust bei e < 1 exakt nach ΔE = ½·μ·(1 − e²)·v²; Restitutionsschwelle; schiefer Stoß ohne Reibung; Coulomb-Reibung (Bremsweg v²/2μg); Rutschen → Rollen mit ⅔·v₀; Rollwiderstand; Auflagekraft = m·g·dt; Impulsgrenzen; Trennen überlappender Körper ohne Geschwindigkeit |
test_world.py |
freier Fall ≈ ½·g·t² (und exakt nach semi-implizitem Euler); Ball bleibt am Boden liegen und verlässt die Welt nie; Sprunghöhe = e² · Fallhöhe (1 %); ein vollkommen elastischer Ball springt über viele Sprünge immer wieder genau auf seine Fallhöhe; Energie steigt ohne äußere Kräfte nie; Stapel bleibt stehen; gemischter Haufen schläft ein; kein Tunneln bei Höchstgeschwindigkeit; Notbremse; Schlafen/Aufwecken, auch wenn die Basis eines schlafenden Stapels weggezogen oder weggestoßen wird (nichts schläft in der Luft weiter); Determinismus; 100 Körper × 1000 Schritte ohne NaN (Stresstest) |
test_timestep.py |
Akkumulator, keine verlorenen Schritte durch Rundung, Obergrenze gegen die "Spiral of Death" |
test_sleep.py |
Union-Find, Inselbildung (Wände verbinden nicht), Einschlafen und Aufwachen nur als ganze Insel |
Interaktion (tests/interaction/, ohne pygame)
| Datei | Inhalt |
|---|---|
test_velocity_tracker.py |
exakte Messung konstanter Bewegung, Unabhängigkeit von der Bildrate, Abklingen beim Stillhalten, Glättung von Zittern |
test_drag_and_tools.py |
Seitenverhältnis mit gleicher Fläche, Mindestgröße, Körper erzeugen, Obergrenze, Vorschaufarbe = echte Farbe; Greifen mit Versatz, Ziel in der Welt, Folgen, Geschwindigkeitsgrenze, gezogener Körper schiebt andere weg, gegen ein Hindernis gedrückter Körper zittert nicht, Werfen, Pause |
test_demo.py |
Beispielszene passt in die Welt, der Turm bleibt stehen |
Anwendung (tests/app/, headless mit pygame)
| Datei | Inhalt |
|---|---|
test_app_integration.py |
Objekt erstellen (Klick, Ziehen aus der Sidebar, Abbruch), greifen, ziehen, werfen, Stillhalten ohne Wurf, gezogener Körper schiebt andere, bleibt in der Welt; löschen (Rechtsklick, Entf, Alles löschen); Pause, Schwerkraft, Regler (Klick, Ziehen, Mausrad), Tastenkürzel, Fenster schließen, Debug-Ansicht, Screenshot, Kommandozeile |
test_widgets.py |
Regler-Abbildung und Rasterung, deaktivierte Regler, Umschalter, Farbwahl, Zeichnen aller Widgets, Viewport, Winkel-Interpolation |
test_entry_points_and_edge_cases.py |
python -m sandbox, CLI ohne Optionen, Ende der Hauptschleife, Randfälle der Eingabe, Schrift-Fallback |
test_smoke.py |
Smoke-Test: Die echte Hauptschleife läuft 3 s headless, während ein zufällig klickender "Affe" Klicks, Ziehen, Rechtsklicks, Mausrad und Tasten sendet – ohne Exception, danach ein gültiger Zustand |
Architektur (tests/test_architecture.py)
- Die Physik-Module und
config.pyimportieren nur erlaubte Module (statische Prüfung über den Syntaxbaum). import sandbox.physicslädt in einem frischen Interpreter kein pygame.
Testabdeckung
Stand der letzten Messung: 219 Tests, 100 % Zeilen- und Branch-Abdeckung über das ganze
Paket sandbox (inklusive UI), davon 100 % für sandbox.physics.
Die hohe Abdeckung ist kein Selbstzweck. Entscheidend sind die physikalischen Erwartungswerte: Ein Test, der nur Code ausführt, ohne das Ergebnis gegen die Physik zu prüfen, wäre wertlos.