Unittest 

 15. März 2021

Der Unittest ist für mich die essentiellste aller Teststufen. Auf sie schaue ich auch zuerst, wenn ich ein neues Beratungsprojekt starte. Warum? Hier entsteht die Robustheit der Applikation. Ohne ausreichende Unittests sind stabile Integrations-, System- und Abnahmetests schwer möglich. Gute Unittests helfen massiv dabei, dass der Code konsistent bleibt. Probleme, dass das System durch Änderungen instabil wird oder sich unerwartete Seiteneffekte zeigen, lassen sich mit Unittests im Vorfeld minimieren. Und sie sind ein ausgezeichneter Indikator zur Qualität der Architektur und des Designs. Ein “Unittest ist hier nicht möglich” deutet stark darauf hin, dass hier potentielles Ungemach schlummert. Spätestens beim Refactoring dieses Bereichs fehlt dann die Qualitätssicherung in Form von Tests. Unittests sind ein idealer Feedbackmechanismus bei der Software-Entwicklung.

Ändert oder erweitert man den Code, liefern Unittests sofort Feedback, ob man damit etwas kaputtgespielt hat. Das ist wiederum der Grund, warum Unittests auch rasant ausführbar sein müssen. Geht das nicht, ist das wieder ein Indiz dafür, nochmal einen Blick auf die Architektur und das Design zu werfen.

Definition Unittest

In der Literatur wird die Teststufe als Komponententest definiert und taucht unter diesem Begriff häufig auf. Auch Modultest ist ein gängiges Synonym. Das ist natürlich auch etwas abhängig von der Programmiersprache und dem Kontext. In der Projektpraxis hat sich jedoch Unittest durchgesetzt. Ich kann mich an kein Projekt der letzten 20 Jahren erinnern, wo diese Teststufe nicht Unittest hieß.

Das ISTQB definiert Komponententest als: “Eine Teststufe mit dem Schwerpunkt auf einer einzelnen Hardware- oder Softwarekomponente.”

Generell kann man sagen: Unittest ist der Test der kleinsten Einheit. Ob diese jetzt Modul, Klasse, Anweisung oder sonst sie heißt.

Testbasis

Als Testbasis für den Unittest dienen alle Informationen, die diesen kleinen Funktionsblock beschreiben. Das können aus dem Design oder der Architektur abgeleitete Infos sein, oder Teile einer User Story oder Anforderung. Manchmal gibt es auch Komponentenspezifikationen oder Modelle, die die Funktionalität der Unit beschreiben.

Testfallerstellung für Unittests

Zur Testfallerstellung beim Unit- oder Modultest eignen sich ganz besonders die strukturierten Testentwurfsmethoden wie Äquivalenzklassenanalyse, Grenzwertanalyse oder Entscheidungstabellen. Ebenso Kombinatoriken wie Pairwise oder Klassifikationsbaum. Gegenüber anderen Teststufen wie Systemtest oder Abnahmetest, hat man hier den Vorteil, dass Wertebereiche, etc. deutlich konkreter sind, was die Ableitung von Testfällen erleichtert.

Durch agile Softwareentwicklung ist Test Driven Development (TDD) sehr populär geworden. Dabei wird nicht zuerst der Programmcode geschrieben und dann der Testcode, sondern umgekehrt. Es wird zuerst der Test geschrieben, dieser schlägt fehl, dann wird der funktionale Code entwickelt, damit der Testfall “grün” wird. In dieser Struktur wird weiterentwickelt. TDD hat viele Vorteile, gerade was den Fokus auf Tests betrifft, aber auch ein paar Nachteile.

Testobjekt

Das Testobjekt für den Unittest ist die kleinste Einheit der jeweiligen Programmiersprache, die sinnvoll getestet werden kann. Es geht darum, diese Einheit für sich selbst zu testen, ohne Interaktion mit anderen dieser Einheiten (Klassen, Module, etc.).

Testziele des Unittests

Ziel des Unittests ist es, die funktionalen und auch nicht-funktionalen Aspekte auf unterster Ebene zu prüfen. Das hat mehrere Vorteile:

  • Durch die Tests entsteht ein robustes Fundament, auf welchem die anderen Teststufen und die gesamte Architektur aufbauen können
  • Auftretende Fehler im Unittest können durch die Nähe zum Code schnell und gezielt behoben werden
  • Schneller Feedbackmechanismus, wenn Änderungen an der Software durchgeführt werden.

Sehr oft kommt hier das Thema Codeabdeckung zur Sprache. Was versteht man unter Codeabdeckung? Codeabdeckung zeigt, wieviel des Software-Sourcecodes beim Ausführen der Testfälle benutzt werden. Die Codeabdeckung kann auf verschiedene Bestandteile des Codes abzielen. Darum unterscheidet man z.B. zwischen Anweisungsüberdeckung oder Zweigüberdeckung.

Was ist Codeüberdeckung?

Gerne wird Codeabdeckung auch als Testendekriterium oder Definition of Done genutzt. Dann finden sich Formulierungen wie “Unittests müssen 80% Codeabdeckung aufweisen”. Solche Zahlen festzulegen und zu messen ist durchaus ein interessanter Aspekt. Daraus ergibt sich aber ein gravierendes Problem: Die Qualität der Testfälle kann sinken, da versucht wird die Abdeckung mit möglichst einfachen Testfällen zu erreichen. Alle Sonderfälle oder Negativtests (z.B. mit anderen Testdaten) werden weggelassen, da sie nicht oder kaum zur Erhöhung der Testabdeckung beitragen.

Daher sind diese Messlatten wie “80% Codeabdeckung” immer mit Vorsicht zu genießen. Sie können helfen, Bewusstsein für Unittests zu schaffen, aber ebenso zu mehr Fahrlässigkeit führen.

Testumgebung für Unittests

Für Unittests finden sich in der Regel zwei Testumgebungen. Zum Einen die Entwicklungsumgebung des Entwicklers, wo die Testfälle schnell und unkompliziert ausgeführt werden können. Zum Anderen der Build-Server oder die Pipeline, in der die Software für weitere Zwecke gebaut wird. Für beide Varianten stehen heutzutage Unmengen an Werkzeugen, Scripts und Best Practices zur Verfügung.

Da sich Unittests immer nur auf eine Einheit fokussieren, ist die Frage: Was passiert mit dem Rest, z.B. den aufrufenden oder den aufgerufenen Klassen. Diese werden typischerweise durch Testtreiber und Stubs ersetzt, die die notwendigen Aufrufe und Antworten simulieren.

Testdaten

Durch die Modularität und Kleinteiligkeit der Unittests ist der Umgang mit Testdaten meist gut umsetzbar. Diese müssen nur einen gewissen Aspekt abdecken und können daher leicht aufgebaut bzw. gelöscht werden. Sie werden entweder direkt bei den Testfällen verortet oder in einem gemeinsamen Repository. Auch ein Testdatengenerator macht in größeren Projekten Sinn.

Testautomatisierung des Unittests

Testautomatisierung ist in Unittests ein No-Brainer. Es gibt für alle gängigen Programmiersprachen mittlerweile Frameworks, die sowohl bei der Erstellung als auch bei der Durchführung der Unittests unterstützen. Und das sowohl in den Entwicklungsumgebungen als auch in den Build-Pipelines.

Unittests in agilen Projekten

Unittests sind in allen Projektarten notwendig und mittlerweile auch gut vertreten. In agilen Projekten haben sie auch von Anfang an einen hohen Stellenwert genossen. Durch das laufende Refactoring und die kurzen Zyklen, in denen die Software gebaut wird, sind sie hier als Feedback essentiell.

Typische Probleme

In Projekten treffe ich immer wieder auf drei Probleme, die es zu lösen gilt:

  1. Es werden keine Negativ-Tests oder Sonderfälle geprüft. D.h. Unittests testen dann nur, ob der Standardfall funktioniert, aber nicht was passiert, wenn korrupte Daten oder Parameter außerhalb der Spezifikation genutzt werden.
  2. Der Fokus liegt auf der Funktionalität und es werden keine Unittests geschrieben. Das Problem dabei ist, dass das auch zu Beginn gut funktioniert. Die Software ist noch überschaubar, die Komplexität gering, alles läuft und der Entwicklungsfortschritt ist genial. Im Hintergrund baut sich jedoch ein technischer Schuldenberg auf, der im Nachhinein nur mehr schwer abzuarbeiten ist.
  3. Die Notwendigkeit wird nicht erkannt. Aussagen wie “Ich bin fertig, ich muss nur mehr Tests schreiben” deuten schon darauf hin, dass Unittests als losgelöste Aufgabe gesehen werden. Im Gegensatz dazu ist bei High-Performance-Teams “fertig” erst dann, wenn auch alle Tests entwickelt sind. Hier herrscht ein anderes Mindset, was meiner Erfahrung nach zu deutlich besseren Ergebnissen führt.

Unittest in der Praxis

Ein Mindestmaß an Unittests ist bereits- mehr als die anderen Teststufen- in der Praxis angekommen. Dennoch gibt es häufig Probleme bei der Umsetzung. Zu oft liegt der Fokus rein auf der Codeabdeckung oder die Testfallentwicklung wird als lästiges Übel wahrgenommen. Dabei sind gerade auf dieser Ebene so viele Quick-Wins für den Projekterfolg zu machen.