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 Entwickler:innen, die kaum Tests schreiben oder auch welche, die zuerst die Tests schreiben, aber auch komplexere Konstrukte wie die testgetriebene Entwicklung erfreuen sich immer mehr Beleibtheit. Die Wage liegt für die meisten irgendwo zwischen den Extremen. Falls du mehr zu TDD (Testgetriebener Entwicklung) erfahren willst, kann ich dir den Artikel empfehlen: Wie funktioniert Test Driven Development eigentlich?
In diesem Artikel wollen wir klären, wie Tests in Python funktionieren und die wichtigsten Begriffe wie zum Beispiel:
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.
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.
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.
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 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'])
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.
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%
unittest.main()
ausgeführt wurde, diese war nicht notwendig, da wir coverage
verwenden.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.
test_<name_des_moduls>.py
beginnen und so über das Präfix erkennbar sind.Test<Klassennamen>
benannt sein.test_<funktions/methodennamen
benannt und sollten entsprechend mindestens eine Behauptung (assert) beinhalten.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.
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.
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.
Hinterlasse mir gerne einen Kommentar zum Artikel und wie er dir weitergeholfen hat beziehungsweise, was dir helfen würde das Thema besser zu verstehen. Oder hast du einen Fehler entdeckt, den ich korrigieren sollte? Schreibe mir auch dazu gerne ein Feedback!
Es sind noch keine Kommentare vorhanden? Sei der/die Erste und verfasse einen Kommentar zum Artikel "Wie Unit-Testing in Python funktioniert?"!