Unit-Tests#

Funktionstest#

Unit-Tests sind zum Vermeiden und Diagnostizieren von Fehlern sehr wichtig. Ein Unit-Test testet ein einzelnes Code-Modul, wie z.B. eine Funktion oder eine Klasse.

Zum Testen verwendet man in Python meist spezielle Pakete wie unittest oder nosetest, welche dann automatisch bei Code-Änderungen ausgeführt werden. Einfache Tests können direkt in den Code geschrieben werden. Hierfür ist der assert Befehl da. Er prüft, ob eine Testbedingung erfüllt ist (assert) und löst eine AssertionError Exception aus, wenn dies nicht der Fall ist.

Wir wollen zum Beispiel die zuvor definierte Divisionsfunktion division() testen.

def division(zaehler, nenner):
    if not isinstance(nenner, (int, float)):
        raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
    elif not isinstance(zaehler, (int, float)):
        raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
    elif not nenner:
        print("Warnung Division durch 0")
        return None
    else:
        ergebnis = zaehler / nenner
        print(f"Das Ergebnis von {zaehler}/{nenner} = {ergebnis}")
        return ergebnis

Hierfür schreiben wir als erstes einen Funktionstest. Diese überprüfen die korrekte Funktion eines Moduls mit mehreren Eingaben, für die wir das korrekte Ergebnis kennen. Hier macht man meist mehrere Tests, um sicherzustellen, dass man nicht zufällig das richtige Ergebnis bekommt, sondern für verschiedene Kombinationen.

def test_funktion(): # Die übliche Notation für Tests in Python ist die Funktionsnamen mit `test_` zu beginnen
    assert division(10, 2) == 5
    assert division(10, 5) == 2
    assert division(50, 1) == 50
    assert division(50, 5) == 10
test_funktion()
Das Ergebnis von 10/2 = 5.0
Das Ergebnis von 10/5 = 2.0
Das Ergebnis von 50/1 = 50.0
Das Ergebnis von 50/5 = 10.0

Grenzwerttest#

Ein weiterer wichtiger Test ist der Grenzwerttest. Hier prüft man ob Eingaben im Grenzwertbereich korrekt behandelt werden. Zum Beispiel ob positiv oder negativ Unendlich behandelt werden oder ob die Division durch 0 behandelt wird. Wir haben oben beim Erstellen der Funktion definiert, dass bei einer Division durch 0 die Funktion den Wert None ausgeben soll.

import math # die Zahl unendlich ist in der math bibliothek definiert

def test_grenzwert():
    assert division(math.inf, 1) == math.inf
    assert division(math.inf, 100) == math.inf
    assert math.isnan(division(math.inf, math.inf)) # Das prüfen auf nan (not a number) ist nur durch die funktion `isnan` moeglich
    assert division(1, math.inf) == 0
    assert division(100, 0) is None
test_grenzwert()
Das Ergebnis von inf/1 = inf
Das Ergebnis von inf/100 = inf
Das Ergebnis von inf/inf = nan
Das Ergebnis von 1/inf = 0.0
Warnung Division durch 0

Datentyptest#

Da Python dynamische Datentypen unterstützt ist es sehr wichtig Datentyptest. Hierbei wird getetstet ob die Funktion falsche Datentypen als Eingabe richtig behandelt.

Hierbei können wir nicht direkt assert nutzen da es ja unseren Code zum Absturz bringt.

assert division("falscher typ", 1) is ValueError
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[6], line 1
----> 1 assert division("falscher typ", 1) is ValueError

Cell In[1], line 5, in division(zaehler, nenner)
      3     raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
      4 elif not isinstance(zaehler, (int, float)):
----> 5     raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
      6 elif not nenner:
      7     print("Warnung Division durch 0")

ValueError: Zaehler nicht vom Datentyp `int` oder `float`

Stattdessen nutzen wir im Test einen try-except-Block und die else-Anweisung. Sie wird nur dann ausgeführt, wenn im try-Block keine Exception auftritt, was im Testfall ja unerwünscht ist, also raise wir in diesem Fall ein AssertionError, da der Testfall scheitert. Ferner prüfen wir, ob die geworfene Exception der Funktion auch den richtigen Typen TypeError hat.

def test_datentyp():
    # wir testen den Zähler
    try:  
        division("falscher typ", 1)
    except Exception as e:
        assert isinstance(e, ValueError)
    else:
        raise AssertionError("Datatype")
    # den zweiten Fall des Nenners
    try:
        division(10, "falscher typ")
    except Exception as e:
        assert isinstance(e, ValueError)
    else:
        raise AssertionError("Datatype")
test_datentyp()

Mit Tests bei Code-Änderungen Fehler erkennen#

Diese Unit-Tests haben wir absichtlich als Funktion definiert, so dass wir sie nun bei Code-Änderungen immer wieder durchführen können. So kann man dann testen, ob die Implementation die gestellten Erwartungen überhaupt oder immer noch erfüllt.

Wird zum Beispiel die Divisionsfunktion durch eine triviale Implementation überschrieben.

def division(zaehler, nenner):
    return zaehler / nenner

So funktioniert immer noch unser Funktionstest. Weshalb es meistens nicht hinreichend ist, nur diesen zu implementieren oder bei der Entwicklung nur die Funktion zu prüfen.

test_funktion()

Der Grenzwerttest wird allerdings jetzt eine Ausnahme werfen.

test_grenzwert()
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[11], line 1
----> 1 test_grenzwert()

Cell In[4], line 8, in test_grenzwert()
      6 assert math.isnan(division(math.inf, math.inf)) # Das prüfen auf nan (not a number) ist nur durch die funktion `isnan` moeglich
      7 assert division(1, math.inf) == 0
----> 8 assert division(100, 0) is None

Cell In[9], line 2, in division(zaehler, nenner)
      1 def division(zaehler, nenner):
----> 2     return zaehler / nenner

ZeroDivisionError: division by zero

Auch der Datentyptest ist nicht erfolgreich, weil wir ja definiert haben, dass bei falschen Eingabedatentypen ein ValueError erzeugt werden soll, wir aber einen TypeError erhalten.

test_datentyp()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], line 4, in test_datentyp()
      3 try:  
----> 4     division("falscher typ", 1)
      5 except Exception as e:

Cell In[9], line 2, in division(zaehler, nenner)
      1 def division(zaehler, nenner):
----> 2     return zaehler / nenner

TypeError: unsupported operand type(s) for /: 'str' and 'int'

During handling of the above exception, another exception occurred:

AssertionError                            Traceback (most recent call last)
Cell In[12], line 1
----> 1 test_datentyp()

Cell In[7], line 6, in test_datentyp()
      4     division("falscher typ", 1)
      5 except Exception as e:
----> 6     assert isinstance(e, ValueError)
      7 else:
      8     raise AssertionError("Datatype")

AssertionError: