Was ist ein Unit Test?
Ein Unit-Test prüft die kleinste sinnvoll testbare Einheit einer Software isoliert vom Rest des Systems: eine Funktion, eine Methode, eine Klasse oder ein Modul. Im Deutschen heißt dieselbe Teststufe Modultest oder Komponententest, alle drei Begriffe bezeichnen dasselbe. Das ISTQB definiert den Komponententest als “eine Teststufe mit dem Schwerpunkt auf einer einzelnen Hardware- oder Softwarekomponente”. In der Projektpraxis hat sich der englische Begriff durchgesetzt. Ich kann mich an kein Projekt der letzten 25 Jahre erinnern, in dem diese Teststufe nicht Unit-Test hieß.
Geschrieben werden Unit-Tests von den Entwicklerinnen und Entwicklern selbst, meist mit einem Testframework wie JUnit, pytest oder NUnit. Sie laufen automatisiert bei jeder Änderung am Code, und genau daraus bezieht der Unit-Test seinen Wert: Er ist der schnellste Feedbackmechanismus der Software-Entwicklung. Wer den Code ändert oder erweitert, sieht sofort, ob er damit etwas kaputtgespielt hat.
Unit-Tests sind außerdem ein ausgezeichneter Indikator für die Qualität von Architektur und Design. Ein “Unit-Test ist hier nicht möglich” deutet stark darauf hin, dass an dieser Stelle potentielles Ungemach schlummert: zu viele Abhängigkeiten, zu große Einheiten, zu enge Kopplung. Spätestens beim Refactoring dieses Bereichs fehlt dann die Absicherung durch Tests. Dasselbe gilt für die Laufzeit: Unit-Tests müssen rasant ausführbar sein. Geht das nicht, lohnt ein zweiter Blick auf den Schnitt der Einheiten.
Einordnung in die Teststufen
Der Unit-Test ist die unterste der klassischen Teststufen, vor Integrationstest, Systemtest und Abnahmetest. Jede Stufe fängt die Fehler ein, die auf ihrer Ebene entstehen. Beim Unit-Test sind das Fehler in der inneren Logik einer Einheit: falsche Berechnungen, unsaubere Grenzwertbehandlung, unbehandelte Ausnahmefälle.
Diese Position macht ihn zur günstigsten aller Teststufen. Ein Bug, der im Unit-Test auffällt, ist nah am Code und meist schnell lokalisiert. Derselbe Bug kostet im Systemtest ein Vielfaches, weil er erst durch mehrere Schichten zurückverfolgt werden muss. Und ohne ausreichende Unit-Tests sind stabile Integrations-, System- und Abnahmetests schwer möglich: Instabiles Verhalten auf den oberen Stufen deutet fast immer auf fehlende Robustheit ganz unten hin.
Unit Test vs. Integrationstest
Der Unit-Test prüft eine Einheit für sich, der Integrationstest prüft das Zusammenspiel mehrerer Einheiten über ihre Schnittstellen. Damit eine Einheit wirklich isoliert getestet werden kann, ersetzen Stubs und Mocks ihre Abhängigkeiten: Sie simulieren die Aufrufe und Antworten der Nachbarkomponenten. An dieser Isolation hängt die ganze Aussagekraft. Läuft ein Test nicht isoliert, prüft er die Abhängigkeiten mit, und dann lässt sich nicht mehr sagen, wo ein Fehler wirklich sitzt.
| Unit-Test | Integrationstest | |
|---|---|---|
| Testobjekt | einzelne Funktion, Klasse oder Modul | Zusammenspiel mehrerer Komponenten |
| Fokus | innere Logik der Einheit | Schnittstellen und Datenaustausch |
| Abhängigkeiten | durch Stubs und Mocks ersetzt | real oder teilweise simuliert |
| Testbasis | Komponentenspezifikation, Entwurf, Code | Architektur, Schnittstellenbeschreibungen |
| Typische Fehler | Berechnungen, Grenzwerte, Ausnahmefälle | Datenformate, Aufrufsequenzen, Fehlerweitergabe |
| Laufzeit | Millisekunden bis Sekunden | spürbar langsamer |
| Verantwortung | Entwicklung | Entwicklung, auf höheren Ebenen auch Testteams |
Nach oben schließen zwei weitere Stufen an: Der Systemtest bewertet das vollständige System gegen seine spezifizierten Anforderungen, der Abnahmetest klärt aus Sicht der Auftraggeber, ob das System für den Einsatz taugt. Keine dieser Stufen ersetzt die andere. Sie beantworten unterschiedliche Fragen.
Ziele des Unit-Tests
Ziel des Unit-Tests ist es, funktionale und auch nicht-funktionale Aspekte auf der untersten Ebene zu prüfen. Auf gut getesteten Einheiten bauen die anderen Teststufen und die gesamte Architektur auf. Fehler zeigen sich nah am Code und lassen sich dort schnell und gezielt beheben. Und jede Änderung an der Software bekommt sofort Rückmeldung, ob bestehendes Verhalten noch stimmt.
Der letzte Punkt wird mit der Zeit immer wertvoller. Eine Software ohne Unit-Tests lässt sich anfangs schnell weiterentwickeln. Mit wachsender Komplexität dreht sich das um: Jede Änderung wird zum Risiko, weil niemand mehr sicher sagen kann, was sie an anderer Stelle auslöst. Unit-Tests halten den Code über Jahre änderbar.
Testbasis
Als Testbasis für den Unit-Test dienen alle Informationen, die den kleinen Funktionsblock beschreiben: aus Design oder Architektur abgeleitete Details, Teile einer User Story oder Anforderung, manchmal auch Komponentenspezifikationen oder Modelle. Und der Sourcecode selbst, wenn es um strukturorientierte Testfälle und die Abdeckung von Zweigen und Bedingungen geht.
Testfallableitung
Für die Testfallerstellung eignen sich besonders die strukturierten Testentwurfsverfahren: Äquivalenzklassenanalyse, Grenzwertanalyse, Entscheidungstabellen, dazu Kombinatoriken wie Pairwise oder der Klassifikationsbaum. Gegenüber Systemtest oder Abnahmetest hat man auf dieser Ebene einen handfesten Vorteil: Wertebereiche, Datentypen und Bedingungen sind deutlich konkreter, was die Ableitung von Testfällen erleichtert.
Wichtig sind dabei die Negativtests. Ein Unit-Test, der nur den Standardfall prüft, sagt wenig darüber aus, wie sich die Einheit bei korrupten Daten, Grenzwerten oder Parametern außerhalb der Spezifikation verhält. Genau dort sitzen aber die Fehler, die später teuer werden.
Was macht einen guten Unit-Test aus?
Ein guter Unit-Test ist schnell, unabhängig von anderen Tests, jederzeit wiederholbar mit demselben Ergebnis und wertet sich selbst aus: Er ist grün oder rot, kein Mensch muss Logdateien interpretieren. In der Clean-Code-Community sind diese Eigenschaften als FIRST-Prinzipien bekannt (Fast, Independent, Repeatable, Self-validating, Timely).
Dazu kommt die Lesbarkeit. Ein Unit-Test prüft einen Aspekt, trägt einen sprechenden Namen und folgt einem klaren Aufbau: Vorbereiten, Ausführen, Prüfen (Arrange, Act, Assert). So werden die Tests nebenbei zur Dokumentation. Wer wissen will, wie eine Klasse gedacht ist, liest ihre Tests.
Test Driven Development (TDD)
Durch die agile Softwareentwicklung ist Test Driven Development als Arbeitsweise beim Unit-Testen populär geworden. Dabei dreht sich die Reihenfolge um: Zuerst entsteht der Test und schlägt fehl. Dann schreibt man den funktionalen Code, bis der Test grün ist. Danach wird aufgeräumt. In diesem Rhythmus geht die Entwicklung weiter.
TDD hat viele Vorteile. Der Fokus auf Tests ist eingebaut statt nachgelagert, und der Code wird von Anfang an testbar geschnitten, was sich direkt in der Architektur niederschlägt. TDD hat aber auch Grenzen: Es verlangt Disziplin, und nicht jede Aufgabe eignet sich gleich gut dafür, explorative Arbeit an einem unklaren Problem etwa weniger als die Umsetzung klar spezifizierter Logik. TDD ist ein Werkzeug, keine Pflicht. Wer es beherrscht, hat eine Option mehr.
Codeüberdeckung bei Unit-Tests
Bei Unit-Tests kommt fast immer das Thema Codeüberdeckung zur Sprache. Codeüberdeckung zeigt, wie viel des Sourcecodes beim Ausführen der Testfälle durchlaufen wird. Je nachdem, welche Bestandteile man misst, unterscheidet man etwa Anweisungsüberdeckung und Zweigüberdeckung.
Gerne wird die Überdeckung als Testendekriterium oder Definition of Done genutzt: “Unittests müssen 80 % Codeabdeckung aufweisen.” Solche Zahlen festzulegen und zu messen ist ein interessanter Aspekt. Daraus ergibt sich aber ein gravierendes Problem: Die Qualität der Testfälle kann sinken, weil versucht wird, die Abdeckung mit möglichst einfachen Testfällen zu erreichen. Sonderfälle und Negativtests fallen weg, da sie zur Erhöhung der Abdeckung kaum beitragen. Hundert Prozent Abdeckung mit schwachen Tests sind weniger wert als siebzig Prozent mit guten.
Solche Messlatten sind darum mit Vorsicht zu genießen. Sie können Bewusstsein für Unit-Tests schaffen, aber ebenso zu mehr Fahrlässigkeit führen. Die Überdeckung ist ein Diagnosewerkzeug, das Lücken sichtbar macht. Als Zielvorgabe taugt sie nur bedingt.
Testumgebung
Für Unit-Tests gibt es in der Regel zwei Umgebungen: die Entwicklungsumgebung, in der die Testfälle schnell und unkompliziert laufen, und den Build-Server oder die CI-Pipeline, wo sie bei jedem Commit automatisch mitlaufen. Für beide Varianten existiert ein breites Angebot an Werkzeugen und eingespielten Abläufen.
Da sich Unit-Tests immer nur auf eine Einheit konzentrieren, bleibt die Frage: Was passiert mit dem Rest, den aufrufenden und den aufgerufenen Klassen? Diese werden typischerweise durch Testtreiber, Stubs und Mocks ersetzt, die die notwendigen Aufrufe und Antworten simulieren.
Testdaten
Durch die Kleinteiligkeit der Unit-Tests ist der Umgang mit Testdaten meist gut beherrschbar. Die Daten müssen nur einen bestimmten Aspekt abdecken und lassen sich leicht aufbauen und wieder löschen. Sie liegen entweder direkt bei den Testfällen oder in einem gemeinsamen Repository. In größeren Projekten kann zusätzlich ein Testdatengenerator sinnvoll sein, vor allem für Grenzfälle und Sonderkonstellationen, die von Hand mühsam zu pflegen wären.
Testautomatisierung im Unit-Test
Testautomatisierung ist beim Unit-Test ein No-Brainer. Für alle gängigen Programmiersprachen existieren etablierte Frameworks: JUnit für Java, pytest für Python, NUnit für .NET, Jest oder Vitest für JavaScript und TypeScript. Sie unterstützen Erstellung und Ausführung gleichermaßen, in der Entwicklungsumgebung wie in der Build-Pipeline.
Während auf den höheren Teststufen bei jeder Automatisierung abgewogen wird, ob sich der Aufwand lohnt, stellt sich diese Frage hier nicht. Ein Unit-Test, der nicht automatisiert läuft, ist praktisch keiner: Sein Wert liegt gerade darin, bei jeder Änderung ohne Zutun mitzulaufen.
Unit-Tests in agilen Projekten
Notwendig sind Unit-Tests in allen Projektarten. In agilen Projekten genießen sie von Anfang an einen hohen Stellenwert, denn durch das laufende Refactoring und die kurzen Zyklen ist schnelles Feedback hier unverzichtbar. Wer alle zwei Wochen liefern will, kann nicht vor jedem Release manuell nachprüfen, ob noch alles funktioniert.
Die Testpyramide nach Mike Cohn bringt das auf den Punkt: An der Basis viele schnelle Unit-Tests, darüber weniger Integrationstests, an der Spitze wenige Ende-zu-Ende-Tests. Die Verteilung ist kein Dogma, aber die Grundidee trägt: Je weiter unten ein Fehler gefunden wird, desto billiger ist er.
Typische Probleme
In Projekten treffe ich immer wieder auf drei Probleme, die es zu lösen gilt:
- Negativtests und Sonderfälle fehlen. Die Unit-Tests zeigen dann nur, dass der Standardfall funktioniert, aber nicht, was bei korrupten Daten oder Parametern außerhalb der Spezifikation passiert.
- Der Fokus liegt auf der Funktionalität, Unit-Tests entstehen gar nicht erst. Das Tückische daran: Zu Beginn funktioniert das gut. Die Software ist überschaubar, alles läuft, der Entwicklungsfortschritt ist genial. Im Hintergrund baut sich jedoch ein technischer Schuldenberg auf, der sich im Nachhinein nur schwer abtragen lässt.
- Die Notwendigkeit wird nicht erkannt. Aussagen wie “Ich bin fertig, ich muss nur noch Tests schreiben” zeigen, dass Unit-Tests als losgelöste Aufgabe gesehen werden. In High-Performance-Teams ist “fertig” erst dann, wenn auch alle Tests entwickelt sind. Das ist ein anderes Mindset, und es führt meiner Erfahrung nach zu deutlich besseren Ergebnissen.
Grenzen des Ansatzes
Unit-Tests prüfen, ob eine Einheit das tut, was ihr Entwickler beabsichtigt hat. Ob diese Absicht fachlich richtig war, prüfen sie nicht: Ein Missverständnis in den Anforderungen wandert ungehindert in Code und Test gleichermaßen. Dafür braucht es die höheren Teststufen und den Abgleich mit den Anforderungen.
Auch das Zusammenspiel der Einheiten bleibt außen vor, das ist Sache des Integrationstests. Und eine Warnung aus der Praxis: Wer zu viel mockt, testet am Ende vor allem seine Mocks. Wenn ein Test nur noch prüft, ob simulierte Antworten korrekt weitergereicht werden, ist seine Aussagekraft nahe null.
Aus der Praxis
Der Unit-Test ist für mich die wichtigste aller Teststufen. Auf diese Stufe schaue ich zuerst, wenn ich ein neues Beratungsprojekt starte, denn hier zeigt sich, wie es um die Robustheit der Applikation steht. Ein Mindestmaß an Unit-Tests ist in der Praxis angekommen, mehr als bei den anderen Teststufen. Dennoch hakt es oft bei der Umsetzung: Zu häufig 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 holen.