Können Asserts außerhalb von Unit-Tests verwendet werden?
Assertions sind Code-Stücke, die dafür sorgen, dass eine bestimmte Annahme des Entwicklers an der betreffenden Stelle im Programmablauf wahr ist. Schlägt die Annahme fehl, wird das Programm angehalten und eine (hoffentlich) hilfreiche Fehlermeldung ausgegeben. Wenn Sie in einer guten IDE arbeiten, kann diese die Assertion sogar „abfangen“ und eine Debug-Session starten, um die Situation und die Variablen zum Fehlerzeitpunkt zu untersuchen.
Hintergrund
Asserts begegnen Ihnen vermutlich häufiger im Unit-Test-Code. Test-Frameworks haben das Konzept von Asserts übernommen und um viele nützliche Varianten/Hilfsfunktionen erweitert, darunter:
- Assert.AreEqual
- clojure.test/is
- unittest.TestCase.assertNotIn
- Assert.That
- EXPECT_NEAR
- rackunit/check-match
- pytest.raises
Pytest macht sich sogar die Assert-Anweisung von Pythonzunutze. Abgesehen von diesem Hijacking hat Python seine eigene Assert-Anweisung, und um diese Assertions, die nicht aus Unit Testing Suiten stammen, sondern Teil einer Sprache sind, geht es hier.
Das Konzept ist recht ähnlich, wenn man sich die allgemeine Verwendung des Wortes „Assert“ in Unit-Tests ansieht. Wie bei einer Assertion wird auch bei einem Unit-Test die Annahme des Entwicklers zum Programmablauf geprüft. Unit-Tests sind jedoch in der Regel komplexer, können (etwas) länger dauern und einen Setup- oder Teardown-Code erfordern. Eine zusätzliche Einschränkung für die oben aufgeführten Assertions ist, dass sie eine eigene Umgebung haben und ausschließlich in Ihrem Unit-Test-Code vorkommen sollten.
Asserts wurden jedoch nicht mit dem Unit-Testing eingeführt. Sie fanden ihren Weg in den Quellcode bereits Mitte der 60er Jahre, wie die Programmiersprache Algol W Language Description belegt. Die wohl bekannteste Form ist das Makro Assert aus der C-Standardbibliothek von 1978. Über ein Jahrzehnt später führte Kent Beck das Unit-Testing in der Softwareentwicklung ein. Das ist also ein klares „Ja“ auf die Eingangsfrage, andernfalls wären Asserts 22 Jahre lang mehr als nutzlos gewesen.
Widerstand gegen Assertions
Bei Code-Reviews habe ich – selbst von erfahrenen Entwicklern – manchmal Kommentare wie diesen hier erhalten:
Keine Assertions im Produktionscode.
Beim ersten Mal war ich verdutzt. Ich verteidigte meinen Standpunkt und fügte mich weitgehend, nachdem wir die Meinungsverschiedenheit beigelegt hatten. Es sollte aber kein Einzelfall bleiben – immer wieder musste ich einzelne Asserts rechtfertigen. In den Firmen, in denen ich zuvor gearbeitet habe, hatten wir sehr viele Assertions in unserem Produktionscode und da gab es nie Probleme.
Missverständnisse ausräumen
Eine häufige „Warnung“ war, dass Assertions bei Release-Builds deaktiviert werden. Ja, das ist der Standard, aber wir müssen hier unbedingt unterscheiden: Assertions sind kein Exception Handling! Das sind zwei unterschiedliche Tools für verschiedene Aufgaben. Assertions sind ausschließlich für Entwickler gedacht. Schlagen Assertions fehl, deutet das auf einen Fehler im Code hin! Da kann ein Softwareanwender nichts machen.
Assertions sind also dazu gedacht, beim Entwickler fehlzuschlagen. Dementsprechend schreiben, testen und debuggen wir unsere Software die meiste Zeit im Debug-Modus. Gerade dann können wir nämlich auch noch entgegensteuern – denn, wir erinnern uns: Eine fehlgeschlagene Assertion weist auf einen Fehler hin).
Außerdem verbietet uns niemand, Assertions im Release-Modus aktiviert zu lassen. Da die Hardware immer schneller wird, gehen einige Unternehmen dazu über, sie aktiviert zu lassen. John Reger [1] nennt valgrind als ein bekanntes Tool, das von Always-on-Assertions profitiert.
Letztlich ist es vielleicht besser, in einen Fehler zu laufen, als mit einem unbekannten/beschädigten Zustand weiterzumachen – Stichwort Erlang. Allerdings unter einem Vorbehalt: Assertions müssen immer Nebeneffekt frei sein, da sich Ihr Code unterschiedlich verhält, je nachdem, ob die Assertions aktiviert sind oder nicht, selbst wenn keine von ihnen jemals ausgelöst wird.
Verwendung von Asserts
Für Eingabevalidierung, Netzwerkkommunikation und andere Aspekte, die außerhalb Ihrer Kontrolle liegen, sollten Sie lieber Exceptions als Asserts verwenden.
Wenn Sie jedoch irgendwann sicherstellen wollen, dass Sie die Übergänge Ihrer Zustandsmaschine nicht durcheinander gebracht haben, ist dies ein Fall für ein Assert. Grundsätzlich ist das Überprüfen von Invarianten das Paradebeispiel für die Verwendung von Assertions. Sie machen nicht nur darauf aufmerksam, wenn sich bei der Überarbeitung des Codes ein Fehler eingeschlichen hat, sondern dienen auch als Dokumentation, die immer aktuell sein wird.
Anstelle eines passiven und nicht aussagekräftigen
# Das hier sollte nie passieren
.
versuchen Sie es mit
Assert Falsch, „Wurde ein neuer Zustand eingeführt und dessen Handling vergessen?“
und streichen Sie diesen Punkt von Ihrer Sorgenliste.
Nehmen Sie den Begriff „Zustandsmaschine“ hier nicht zu wörtlich. Sie kann auch drei Funktionsaufrufe tief verwendet werden, um eine Annahme zu überprüfen, die bereits von der äußersten Funktion aufgestellt worden sein sollte (z. B. assert len(self.queue) > 1).
Überblick
Lassen Sie uns zusammenfassend einige Punkte der beiden Ansätze vergleichen.
|
ASSERTION |
EXCEPTION HANDLING |
Kontext |
Kontrollierbar |
erwartete, aber auch unvorhersehbare Fehler |
Kontext |
darf nicht passieren |
Kontingenz, mit der wir möglicherweise umgehen müssen |
Denkweise |
„Vorsicht ist besser als Nachsicht.“ |
„Was kann schiefgehen und kann ich etwas dagegen tun?“ |
Integration |
fast immer inaktiv |
Teil Ihres Code-Ablaufs/ihrer Logik |
Geeignet für |
Entwickler |
Entwickler/Anwender |
Komplexität |
Kosten gering halten |
kann bei Bedarf komplex sein |
Aktiviert/deaktiviert |
kann im Release-Modus deaktiviert werden |
immer aktiviert |
Effekt |
„Offensichtlich“ ⇒ Fehler durch Code-Anpassung beheben |
abfangbar/unterdrückbar |
Assertions in der Praxis
Um herauszufinden, wer sonst noch meiner Meinung ist, habe ich nach einigen Open-Source-Projekten gesucht und deren Repositories heruntergeladen. Anschließend habe ich den Quellcode manuell in Implementierungs- und Testordner unterteilt und ein Skript ausgeführt, das Kommentare überspringt und zusammenzählt, was als Assert zählt (angepasst für die jeweiligen Sprachen LISP, Lua, JavaScript/TypeScript, Python, Java und natürlich die C-Familie).
Im Gegensatz zu dem, was ich oben geschrieben habe, habe ich für die Erstellung dieses Diagramms auch nach den verschiedenen Assertions aus Unit-Testing-Frameworks gesucht. Erstens, weil ich der Meinung bin, dass Asserts und Unit-Testing eine gemeinsame Basis haben. Zweitens, weil es sonst kein sehr interessanter Vergleich gewesen wäre.
Ergebnisse
Nach aufsteigendem Prozentsatz der Assertions in der Implementierung geordnet, beginnen wir mit CPython (wie der Name schon sagt, in C implementiert) und Cassandra (Java). 95 % aller Assertions finden sich in den Tests. Auf die restlichen ca. 4.000 Assertions im Produktionscode möchte ich an dieser Stelle nicht weiter eingehen.
PyTorch (C++) zeigt einen starken Anstieg bis zu einer fast ausgeglichenen Verteilung (41 %). Der 3D-Modellierer Blender ist ebenfalls in C++ implementiert und enthält 92 % aller Assertions in der Implementierung. Fairerweise sollte erwähnt werden, dass die Tests anscheinend ausschließlich in Python durchgeführt werden (d. h. weniger Code-Zeilen) und dass es im Allgemeinen nicht so viele Tests gibt.MySQL (ein weiteres C++-Projekt) hat dagegen eine ganze Reihe von Tests, die aber im Vergleich zu der Menge an Code und Asserts in der Implementierung unbedeutend sind.
Bei SQLite ist die Situation etwas verzerrt. Wir finden etwa ein Dutzend Asserts in den Tests, diese werden aber nicht zum Testen verwendet. Stattdessen gibt es etwa 10.000 Dateien mit der Endung .test. In diesen Dateien finden wir eine Art DSL, die es ermöglicht, einfache SQL-Anweisungen zu schreiben und dann ihre Auswirkungen/Ergebnisse zu testen. Es scheint daher gut getestet zu sein, allerdings sind fast alle Assertions in der Implementierung enthalten.
Fazit
Es gibt viele Varianten: Von vielen Asserts in der Implementierung über eine ausgewogene Mischung bis hin zu den meisten Asserts im Test. Wenn ich mir diese Beispiele ansehe, scheint mir selbst letzteres nicht gegen die Regel „Keine Asserts im Produktionscode“ zu verstoßen. Wie oben dargelegt, bin ich der Ansicht, dass die Aussage eher „keine Asserts anstatt Exceptions“ lauten sollte.
Als zusätzliche Möglichkeit gibt es Code Contracts, eine anspruchsvolle Version von Assert (von Anfang an in Eiffel und seit kurzem z. B. auch in .NET). Sie ermöglichen die Spezifikation von
- Vor- und Nachbedingungen für Methoden, und
- Invarianten für Klassen, die vor/nach jedem Methodenaufruf gelten.
Und genau wie Assertions können sie für Release-Builds kompiliert werden – und werden es wegen der wesentlich höheren Kosten wahrscheinlich auch. Im Gegensatz zu Assertions werden sie nur in den Produktionscode eingefügt, da sie genau diesen beschreiben.
Wenn sie also ein mächtigerer Assert sind und in den Produktionscode gehören, warum sollte man Asserts dann nicht auch dort einfügen? Mit anderen Worten: Worauf warten Sie noch? Oder, um es in Python zu sagen:
assert „assert“ in production code.