Lerne Coding
Wie Unit Testing in Python funktioniert?
11.04.2024

Durchstarten mit Unit Tests in Python

Inhaltsverzeichnis
[[TABLE OF CONTENTS]]
access_timeGeschätzte Lesezeit ca. Minuten

In den letzten Jahren wurden Unit-Tests immer relevanter, um die Qualität von Software zu Testen auch in Python ist das Thema relevant und sollte auf deiner Agenda sein, wenn du hochwertigen Quelltext schreiben möchtest, der möglichst alle Grenzfälle abdeckt. Es gibt den Entwickler, der gar keine Tests schreibt oder den, der zuerst die Tests schreibt. Dabei spricht man von Test Driven Development, auch TDD gennant. Die Wage liegt für die meisten irgendwo zwischen den beiden Extremen. In diesem Artikel wollen wir klären, wie Tests in Python funktionieren und die wichtigsten Begriffe wie zum Beispiel:

  • Assertion
  • Test-Coverage
  • Unit-Test

Wo für sind Tests Relevant?

In der entwicklungstestfreien Entwicklung schreibt der Entwickler einen Codeabschnitt, testet ihn manuell mit verschiedenen Eingaben und hält ihn bei zufriedenstellenden Ergebnissen für fertig. Diese Methode mag für Einzelentwickler funktionieren, skaliert aber schlecht in Teams. Ohne automatisierte Tests ist es unpraktisch, zu erwarten, dass jeder Entwickler manuelle Tests von Grund auf durchführt. Automatisierte Tests erleichtern die erneute Durchführung und helfen zu überprüfen, dass neuer Code bestehende nicht beeinträchtigt.

Angenommen, ein Projekt besteht aus Tausenden von Klassen und Methoden, und ein Entwickler soll eine neue Klasse für ein Feature entwickeln. Dann wird diese sehr schwierig, wenn er an bestehenden Methoden eine Veränderung vornehmen muss und nicht alle Zusammenhänge erkennen kann und testen kann, da das manuelle Testen einfach viel zu lange dauern würde. In so einem Fall ist es zum Beispiel sehr gut, wenn von Anfang an auf Tests geachtet wurde. Dadurch kann der Entwickler sehr einfach herausfinden, ob alles Bestehenden noch funktioniert wie erwartet, auch nach den Änderungen.

Wichtig, die Tests müssen immer gut durchdacht werden, dass auch Grenzfälle getestet werden und nicht nur der Happy Path (der Verfall den man sich immer wünscht). Beim Schreiben von Tests sollte man immer davon ausgehen, dass jemand etwas komplett Unerwartetes in die Methode hinein gibt, der Unhappy Path; genau auf diese Fälle gilt es auch zu reagieren.

Der erste Unit-Test mit Python

Beginnen wollen wir mit einer ganz einfachen Methode für einen Test, dieser nimmt eine Liste von Zahlen entgegen und gibt einen Wert zurück. Falls andere Eingaben erfolgen als Zahlen, soll ein Fehler geworfen werden. Für die Tests verwenden wir das Modul unittests diese ist ein internes von Python und muss nicht zusätzlich installiert werden.

def sum(values : list) -> int:
    if not values:
        return 0

    if not all(isinstance(value, int) for value in values):
        raise TypeError("All values must be integers")

    result = 0
    for value in values:
        result += value
    return result

Um nun einen Test zu implementieren, brauchen wir eine Testklasse. Typisch beginnt der Name mit Test und erbte von unittest.TestCase, jede Methode ist ein eigener Test. Wir implementieren zuerst eine Methode, die mit einer Liste funktioniert. Anschließend implementieren wir noch einen Test mit einem Tuple. Zusätzlich schreiben wir noch einen Test für den Fall, dass wir eine leere Liste herausgeben.

import unittest
from sum import sum

class TestSum(unittest.TestCase):
    def test_sum(self):
        self.assertEqual(sum([1, 2, 3]), 6, "Should be 6")

    def test_sum_tuple(self):
        self.assertEqual(sum((1, 2, 2)), 5, "Should be 5")

    def test_sum_no_values(self):
        self.assertEqual(sum([]), 0, "Should be 0")

if __name__ == '__main__':
    unittest.main()

Durch das Ausführen der test_sum.py bekommen wir die Information, ob unsere Funktionen funktionieren. Der folgende Screenshot zeigt einmal zwei verschiedene Ausführungen. Dort hatte sich im zweiten Durchlauf ein Fehler eingeschlichen, wodurch die Tests nicht mehr erfolgreich sind. Alternative kannst du auch den Befehl python -m unittest discover verwenden, um alle Tests im aktuellen Projekt ausfindig zu machen und auszuführen.

Was sind Assert Methoden und welche gibt es im Unittest Modul?

Dein erster Test ist geschrieben, aber wofür steht eigentlich das assertEqual? Dabei handelt es sich um eine Methode des Unittest Modul von Python, um zu prüfen, ob ein Eingabewert auch dem Ausgabewert entspricht. Neben dieser assertEqual Methode gibt es noch einige weitere Methoden, die genutzt werden können, um etwas zu vergleichen und einen Fehler auszulösen.

Noch deutlicher wird der Verwendungszweck der assert Methoden, wenn wir es ins Deutsche übersetzen "behaupten", bei Test möchten wir eine Behauptung aufstellen und diese soll bestätigt werden. Falls diese Behauptung nicht wahr ist, soll ein Fehler erzeugt werden. Die Assert Methoden lassen sich theoretisch durch if-Kontrollstruktur ersetzen, allerdings kann man über die Verwendung von assert Methoden ganz klar die Logik von der Testlogik unterscheiden.

Thema der Tabelle "assert Methoden"
assert Methode Code / Lösung
assertEqual(x, y) x == y
assertNotEqual(x, y) x != y
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(x, y) x is y
assertRaises(Error, Funktion, *Argumente) Prüft ob der Error aufgetreten ist beim Abruf der Funktion mit den gegebenden Argumenten

Wie können Ausnahmen getestet werden? (Exceptions)

Wie dir vielleicht schon aufgefallen ist, hat unsere sum Funktion auch einen TypeError der geworfen werden kann. Auch dieser sollte getestet werden, um sicherzustellen, dass die sum Funktion keine unerwarteten Rückgabewerte erzeugt. Eine mögliche Methode für den Test haben wir bereits kennengelernt assertRaises also zu Deutsch: "Ich behaupte, dass eine Ausnahme vom Typen TypeError ausgelöst wird.".

def test_raise_exception(self):
  self.assertRaises(TypeError, sum, [1, "second", 3])

Alternative zur Verwendung der assertRaises Methode gibt es auch noch die Möglichkeit, über den Context Manger von Python zu gehen. Diese eignet sich, wenn man nicht nur eine Funktion testen möchte, sondern komplexe Zusammenhänge.

def test_raise_exception(self):
  with self.assertRaises(TypeError):
    sum([1, 2, '3'])    

Erstellung von Test Coverage Berichten in Python

Je nach Programm gibt es verschiedene Qualitätsrichtlinien, die eingehalten werden sollen, um die Qualität der Software zu gewährleisten. Ein Faktor, der gerne hierfür genutzt wird, ist die Test Coverage. Diese beschreibt, wie viel Prozent des Codes getestet wurden. Häufig kann man daran bereits erkennen, wie gut die Tests aufgebaut wurden, zu bedenken gilt, dass es auch auf die Test-Cases selbst ankommt, wenn nur der Happy Path getestet wurde, ist diese nicht zielführend, auch der Unhappy Path muss getestet werden.

Dafür gibt es zum Beispiel Coverage.py, das nicht nur mit dem internen unittest Modul funktioniert, sondern auch mit pytest und nosetest, wobei nosetest nicht mehr genutzt werden sollte.

Wie kann ich einen Report erstellen?

Mit dem Befehl coverage run -m unittest discover können vom Hauptverzeichnis aus alle Dateien erfasst werden und die Tests ausgeführt werden. Anschließend kann coverage report -m verwendet werden, um den Report zu erhalten.

-> % coverage report -m
Name          Stmts   Miss  Cover   Missing
-------------------------------------------
sum.py            9      0   100%
test_sum.py      14      1    93%   19
-------------------------------------------
TOTAL            23      1    96%
  • Stmts steht für die Zeilen, die ausgeführt werden können.
  • Miss steht für die Zeilen, die nicht getestet werden.
  • Cover ist die entsprechende Prozentermittlung aus beiden Werten.
  • Missing ist eine Zeile, die nicht ausgeführt wurde. In diesem Fall ist in der Testdatei selbst Zeile 19 nicht getestet, diese ist die Zeile, wo unittest.main() ausgeführt wurde, diese war nicht notwendig, da wir coverage verwenden.

Konventionen für Testing in Python

Es gibt verschiedene Konventionen, die üblich sind beim Testen mit Python. Diese sind zum einen dazu da, einen besseren Überblick neuen Entwickler:innen im Team zu geben, zum anderen sind Sie dazu da, den Quelltext besser lesbar zu halten und zum anderen um dem Test Runner das auffinden, via python -m unittest discover zu vereinfachen.

  • Bei der Benennung von Testdateien ist darauf zu achten, dass diese mit dem Namen test_<name_des_moduls>.py beginnen und so über das Präfix erkennbar sind.
  • Die Testklassen innerhalb einer Testdatei sollten immer nach dem Schema Test<Klassennamen> benannt sein.
  • Die Testmethoden und Funktionen werden immer nach dem Schema test_<funktions/methodennamen benannt und sollten entsprechend mindestens eine Behauptung (assert) beinhalten.
  • Tests sollten keine externen Daten von APIs oder Datenbanken laden, falls Daten zum Testen notwendig sind, sollten diese statisch definiert werden, dass sie unabhängig von der Entwicklungsumgebung funktionieren können.

Ein Alternative zu "unittest" das Modul "pytest"

Neben dem eigenen Modul von Python unittest gibt es auch noch pytest diese ist ein Projekt von Dritten, um das Testen in Python noch einfacher zu gestalten, mit weniger Boilerplate Code und besseren Testausgaben.

Auf dem Bild ist die Ausgabe zusehen von Pytest, 4 Test waren erfolgreich innerhalb von 0.02 Sekunden und es gab eine Test Datei.
Auf dem Bild ist die Ausgabe zusehen von Pytest, 4 Test waren erfolgreich innerhalb von 0.02 Sekunden und es gab eine Test Datei.

In Python gibt es ein assert-Keyword, das eine AssertionError-Ausnahme auslösen kann, wenn eine Bedingung nicht erfüllt ist. Pytest nutzt dieses Verhalten und fängt AssertionError-Ausnahmen ab, um eine detaillierte Analyse und Rückmeldung über den Fehler zu geben, ohne dass Entwickler:innen auf spezifische Methoden zurückgreifen müssen. Dies unterscheidet sich vom unittest-Modul, das eine Reihe spezieller Methoden für Assertions bereitstellt und bei dem die Verwendung des assert-Keywords zwar möglich, aber nicht die empfohlene Vorgehensweise ist. Somit ermöglicht Pytest einen direkteren und einfacheren Ansatz für Assertions in Tests, während unittest einen strukturierteren Ansatz mit spezialisierten Methoden bevorzugt.

import pytest
from sum import sum

def test_sum():
    assert sum([1, 2, 3]) == 6, "Should be 6"

def test_sum_tuple():
    assert sum((1, 2, 2)) == 5, "Should be 5"

def test_sum_no_values():
    assert sum([]) == 0, "Should be 0"

def test_raise_exception():
    with pytest.raises(TypeError):
        sum([1, 2, '3'])

Innerhalb von Pytest kann man die Tests deutlich kürzer und simpler schreiben als innerhalb von unittest, da wir auf viele Boilerplate Informationen verzichten können. Da die Tests von pytest automatisch erkannt werden, brauchen wir nicht spezielle Klassenkonstrukte zur Erkennung.

Zusätzlich zu den anderen genannten Vorteilen hat pytest ein Umfassendes Plugin System für erweiterte Testmöglichkeiten, alle Plugins können unter https://docs.pytest.org/en/7.1.x/reference/plugin_list.html eingesehen werden.

Zusammenfassung

Als Fazit kann man sagen, dass Test dabei helfen, die Codequalität zu verbessern. Allerdings sollte man berücksichtigen, dass durch einen einfachen Code Coverage Test weiterhin nicht die Qualität der Tests gewährleistet ist, um zu prüfen, dass die Software immer weiter funktioniert nach einer Anpassung. Denn der Test ist genauso wie die Software selbst, nur so gut wie die Programmierer:innen. Bei mir persönlich ist es so, dass ich durch Tests noch einmal einen anderen Blick auf meine Software habe und über Grenzfälle noch einmal nachdenke, die mir im ersten Moment vielleicht nicht bewusst waren.

Ob man Unittest oder Pytest nutzen sollte, ist am Ende des Tages auch etwas Geschmacksfrage, persönlich sehe ich es immer so auf Zusatzmodule verzichten zu wollen und lieber mit den Board-Mitteln auskommen. Da externe Pakete immer weiteren Code mitbringen, der Lücken enthalten könnte oder die Wartung von Paketen kann auch eingestellt werden.

Kommentare zum Artikel

Es sind noch keine Kommentare vorhanden? Sei der/die Erste und verfasse einen Kommentar zum Artikel "Wie Unit Testing in Python funktioniert?"!

Kommentar schreiben

Vom Autor Empfohlen
close