Testing
Wird ein Software-Projekt etwas grösser, so steigt die Wahrscheinlichkeit, irgendwo einen Fehler einzubauen, rasant an. Zum Beispiel lösen wir irgendwo im Code ein Problem, aber vergessen, dass die Änderung an einer anderen Stelle zu einem neuen Bug führt.
Um dem entgegenzuwirken, schreiben wir Tests: Separater Code, der den eigentlichen Produktiv-Code ausführt und sicherstellt, dass die Resultate unseren Erwartungen entsprechen.
Unit Testing in Python
Ein Unit Test überprüft die Funktionsweise eines Moduls ($\approx$ einer Python-Datei).
Sorting Example
Wir haben folgenden Python-Code, den wir testen wollen:
- sorting.py
def is_sorted(liste): index = 0 while index < len(liste) - 1: a = liste[0] b = liste[1] if b < a: return False index = index + 1 return True
Aber stimmt der Code auch wirklich? Wir schreiben einen ersten Test, wobei Tests normalerweise in Dateien mit dem Präfix test_
gespeichert werden:
- test_sorting.py
from sorting import * assert is_sorted(['Apfel', 'Birne', 'Zwetschge']) assert not is_sorted(['Birne', 'Apfel', 'Zwetschge'])
Die assert
Anweisung (ohne Klammern!) überprüft, ob der folgende Ausdruck wahr ist, also zu True
evaluiert. Die erste Zeile überprüft, ob die Liste als sortiert erkannt wird, die zweite, ob herausgefunden wird, dass die Liste nicht sortiert ist.
Wir führen den Test aus mit python test_sorting.py
- scheint alles in Ordnung zu sein.
Test Driven Development
Du kriegst einen Bug-Report eines erbosten Benutzers deiner sorting
Bibliothek. Die Liste ['Apfel', 'Zwetschge', 'Birne']
sei fälschlicherweise als sortiert erkannt worden! Natürlich könnten wir jetzt direkt das Problem im Code suchen - aber wer stellt sicher, dass das Problem nicht plötzlich wieder auftaucht? Genau, ein Test.
Im Test-Driven-Development suchen wir nicht zuerst den Fehler, sondern schreiben zuerst einen neuen Test-Case, der den Fehler reproduziert (also beim Test-Durchlauf fehlschlägt). Erst dann flicken wir den fehlerhaften Code, bis alle Tests wieder grün sind.
Aufgabe A
Schreibe einen Test-Case für den rapportierten Fehler. Flicke anschliessend die is_sorted
Funktion, bis alle Tests wieder durchlaufen.
Pytest
Um in grösseren Projekten nicht den Überblick zu verlieren, ist es Usus, ein Test-Framework zu verwenden, um alle Tests im Projekt auszuführen. Die beste Wahl für Python ist pytest.
Installation: python -m pip install pytest
Pytest sucht überall im Ordner nach Dateien, die mit test_
beginnen, und führt darin alle Funktionen aus, die mit dem gleichen Präfix test_
anfangen.
Wir müssen dafür also unseren Test leicht verändern:
- test_sorting.py
from sorting import * def test_sorted(): assert is_sorted(['Apfel', 'Birne', 'Zwetschge']) assert not is_sorted(['Birne', 'Apfel', 'Zwetschge']) def test_sorted_bug(): assert not is_sorted(['Apfel', 'Zwetschge', 'Birne'])
Die Ausführung von pytest
findet die Datei und die zwei Testfunktionen und fasst die Resultate zusammen:
$ pytest ============================================= test session starts ============================================== platform darwin -- Python 3.11.7, pytest-8.1.0, pluggy-1.4.0 rootdir: /Users/tom/git/ksr_talit_jupyter collected 2 items test_sorted.py .. [100%] ============================================== 2 passed in 0.00s ===============================================
Pytest wird auch von VSCode unterstützt (ev. muss in der Sidebar die Tests-Ansicht geöffnet werden:
Aufgabe B
Ändere deinen Unit-Test, damit er von pytest gefunden wird, und führe die Tests im Terminal und auch in VSCode aus.
Integration Tests
Integration Tests sind eine Stufe höher als die Unit Tests angesiedelt; sie testen das Zusammenspiel von mehreren Modulen oder eines ganzen Programms. Weil diese meist länger dauern als Unit Tests, werden Sie seltener ausgeführt.
Test Coverage
Die Test-Coverage (oder Testabdeckung) ist ein Wert zwischen 0 und 100%, der beschreibt, wie gross der Anteil unseres Codes ist, der von den Tests überprüft wird.
Test Automation
Statt nach jeder Änderung die Tests laufen zu lassen, kann dies auch automatisiert werden: z.B. werden bei jedem Commit die Tests ausgeführt und der Commit erst ins Repository genommen, wenn sie erfolgreich sind. Auf github kann dies über Continuous Integration und github actions erreicht werden.