Alltagsgeschichte
Beim TÜV wird nicht nur geschaut, ob das Auto irgendwie fährt. Bremsen, Licht, Reifen und Abgas werden einzeln geprüft. Genau so denkst du beim Testen: Erst die kleinen Bauteile, dann das große Ganze.
Du gehst von Motivation über erste Tests, Red-Green-Refactor, Fixtures und Parametrisierung bis zu pandas-Tests und einem testbaren Taschenrechner.
Unit Tests sind wie eine TÜV-Prüfung für Code: Jedes Teil wird einzeln gecheckt, bevor das ganze System auf die Straße darf.
Beim TÜV wird nicht nur geschaut, ob das Auto irgendwie fährt. Bremsen, Licht, Reifen und Abgas werden einzeln geprüft. Genau so denkst du beim Testen: Erst die kleinen Bauteile, dann das große Ganze.
Eine Funktion wie divide() oder impute() ist ein einzelnes Bauteil. Ein Unit Test prüft: Liefert diese eine Funktion das, was sie soll?
Wähle die drei Prüfpunkte, die du VOR Vertrauen kontrollieren würdest.
Wie bei einem IKEA-Regal hilft eine klare Struktur: Python-Pakete, Module und Tests liegen sauber sortiert.
ds-unit-testing/
├── src/
│ ├── example/
│ │ ├── __init__.py
│ │ └── example_file.py
├── tests/
└── notebooks/
Der Startercode kommt direkt aus dem Packaging-Notebook. Ergänze die Importzeile und prüfe danach, ob die Funktion aus 1 eine 2 macht.
📓 Öffne dein Jupyter Notebook oder Google Colab und probiere es selbst aus.
# ??? DEINE LÖSUNG ???
print(add_one(1))
Im Notebook liegt die Funktion in src.example.example_file. Der Import folgt also dem Paketpfad der Quelldatei, nicht dem Speicherort des Notebooks.
from src.example.example_file import add_one
print(add_one(1))
Erwartete Ausgabe: `2`
Wie bei einem Kuchenrezept werden Prüfungen immer professioneller: erst hinschauen, dann dokumentieren, dann automatisch testen.
def divide(x, y):
if y == 0:
return "Can not divide by zero"
return x / y
Aus genau so einer kleinen Funktion baust du zuerst einzelne Prüfungen auf. assert ist dabei nur eine einzelne Behauptung. pytest ist das Werkzeug, das viele solcher Tests automatisch findet, startet und auswertet.
Die Funktion ist schon da. Ergänze jetzt die Zeile, die prüft, ob divide(6, 2) wirklich 3 ergibt.
📓 Öffne dein Jupyter Notebook oder Google Colab und probiere es selbst aus.
def divide(x, y):
if y == 0:
return "Can not divide by zero"
return x / y
def test_divide():
# ??? DEINE LÖSUNG ???
Ein pytest-Test enthält meist ein klares assert mit Soll und Ist. Der Funktionsname startet oft mit test_, damit pytest ihn automatisch einsammelt.
def divide(x, y):
if y == 0:
return "Can not divide by zero"
return x / y
def test_divide():
assert divide(6, 2) == 3
Erwartete Ausgabe: `1 passed`
TDD plant das Verhalten zuerst. Der Test wird rot, dann grün, dann wird der Code sauberer gemacht.
def test_format_data_for_display():
people = [{"given_name": "Alfonsa", "family_name": "Ruiz", "title": "Senior Software Engineer"}]
assert format_data_for_display(people) == [
"Alfonsa Ruiz: Senior Software Engineer"
]
Der Test beschreibt zuerst, was die Funktion liefern soll. Danach wird die Funktion so klein wie möglich gebaut, bis der Test grün ist. TDD heißt also nicht: erst fertig programmieren und dann prüfen, sondern Verhalten zuerst festzurren.
Fixtures sind wiederverwendbare Testvorlagen. Parametrize ist deine Prüfliste für viele ähnliche Fälle.
@pytest.fixture
def example_people_data():
return [{"given_name": "Alfonsa", "family_name": "Ruiz"}]
@pytest.mark.parametrize("palindrome", ["", "Bob", "Never odd or even"])
def test_is_palindrome(palindrome):
assert is_palindrome(palindrome)
Baue aus mehreren Einzeltests eine parametrisierte Version. Ergänze die fehlende Deko-Zeile und den erwarteten Wert.
📓 Öffne dein Jupyter Notebook oder Google Colab und probiere es selbst aus.
import pytest
def is_palindrome(s):
return s == s[::-1]
# ??? DEINE LÖSUNG ???
def test_is_palindrome(word, expected_result):
assert is_palindrome(word) == # ??? DEINE LÖSUNG ???
Die Notebook-Variante nutzt @pytest.mark.parametrize mit zwei Spalten: Eingabe und erwartetes Ergebnis.
import pytest
def is_palindrome(s):
return s == s[::-1]
@pytest.mark.parametrize("word, expected_result", [
("Bob", True),
("abc", False),
])
def test_is_palindrome(word, expected_result):
assert is_palindrome(word) == expected_result
Erwartete Ausgabe: `2 passed`
Im Data-Science-Alltag testest du nicht nur Logik, sondern auch Datenqualität, Imputation und Transformationen.
Wenn an Jeans im Regal Preisschilder fehlen, fällt das in der Qualitätskontrolle sofort auf. In pandas ist ein fehlender Preis ein NaN. Auch das musst du testen. assert_series_equal vergleicht dabei nicht nur einen einzelnen Wert, sondern die komplette Serie inklusive Reihenfolge und Datentyp.
Dieser Test ist direkt aus dem Data-Science-Notebook vereinfacht. Ergänze die erwartete Serie für die Mittelwert-Imputation.
📓 Öffne dein Jupyter Notebook oder Google Colab und probiere es selbst aus.
import pandas as pd
from pandas.testing import assert_series_equal
def impute(series):
return series.fillna(series.mean())
input_series = pd.Series([1.0, None, 3.0])
expected_result = pd.Series(# ??? DEINE LÖSUNG ???)
assert_series_equal(impute(input_series), expected_result)
Der Mittelwert von 1.0 und 3.0 ist 2.0. Genau dieser Wert ersetzt die Lücke.
import pandas as pd
from pandas.testing import assert_series_equal
def impute(series):
return series.fillna(series.mean())
input_series = pd.Series([1.0, None, 3.0])
expected_result = pd.Series([1.0, 2.0, 3.0])
assert_series_equal(impute(input_series), expected_result)
Erwartete Ausgabe: kein Fehler, der Test läuft grün durch
Zum Schluss führst du alles zusammen: Tests zuerst, Funktionen danach, dann ein kleines CLI-Projekt.
Was nimmst du mit?
Unit Tests geben dir Sicherheit. Sie helfen dir beim Umbauen, beim Datenprüfen und beim gemeinsamen Arbeiten an Projekten.