Zum Inhalt springen

Suchen...

Was ist ein Unit Test? Definition, Beispiel und Praxis

Unit-Tests schreiben die meisten Teams. Uneinig sind sich viele darüber, wie ein guter aussieht, wie weit die Isolation gehen soll und was eine grüne Testsuite beweist.

Was ist ein Unit Test?

Ein Unit Test prüft das kleinste testbare Stück Code, eine Funktion, eine Methode oder eine Klasse, isoliert vom Rest des Systems. Entwicklerinnen und Entwickler schreiben ihn parallel zum Code, er läuft meist in Millisekunden und meldet nach jeder Änderung, ob dieses Stück noch funktioniert. Ein einfaches Beispiel: berechneRabatt(100) muss 10 liefern. Ob die Anforderung hinter dem Code richtig verstanden ist, beantwortet allerdings auch die grünste Testsuite nicht.

Das ISTQB-Glossar führt den Begriff als eigenen Eintrag und definiert den Unit-Test als “Teststufe mit dem Schwerpunkt auf der Bewertung des kleinsten isoliert testbaren Codeteils”. Im Lehrplan heißt dieselbe Stufe Komponententest, mit dem Zusatz “auch Unittest genannt”, ältere Literatur spricht vom Modultest. In meinen 25 Jahren Projektarbeit hat sich trotzdem der englische Begriff gehalten; an ein Team, das diese Stufe anders nannte, kann ich mich nicht erinnern.

Unit Testing, also das Schreiben und Ausführen solcher Tests, ist der früheste Zeitpunkt, an dem ein Test neuen Code ausführen kann. Der ISTQB-Lehrplan zählt “Frühes Testen spart Zeit und Geld” zu seinen sieben Grundsätzen. Einen Bug, den ein Unit-Test aufdeckt, findet die Entwicklerin meist schnell, weil er nah am Code sitzt. Im Systemtest muss ihn erst jemand durch mehrere Schichten zurückverfolgen.

Dazu kommt ein zweiter Nutzen: Unit-Tests halten den Code änderbar, weil sie nach jeder Änderung in Sekunden zeigen, ob das bestehende Verhalten noch stimmt. Und sie verraten einiges über das Design. Fällt der Satz “Das kann man nicht unit-testen”, hat der Code meist zu viele Abhängigkeiten, zu große Einheiten oder zu enge Kopplung. Brauchen die Tests lange, lohnt ebenfalls ein zweiter Blick auf den Schnitt der Einheiten.

Wie sieht das im Code aus? Ein Beispiel

Angenommen, ein Onlineshop gewährt auf jede Bestellung 10 Prozent Rabatt. Die Funktion berechneRabatt(bestellwert) soll bei 100 Euro Bestellwert 10 liefern, bei einer leeren Bestellung 0, und negative Werte soll sie zurückweisen. Ein Unit-Test ruft sie mit genau diesen Eingaben auf und vergleicht das Ergebnis mit dem erwarteten Wert. In TypeScript mit dem Framework Vitest sieht das so aus:

import { describe, it, expect } from "vitest";
import { berechneRabatt } from "./preise";

describe("berechneRabatt", () => {
  it("liefert 10 Prozent des Bestellwerts", () => {
    expect(berechneRabatt(100)).toBe(10);
  });

  it("liefert 0 bei leerer Bestellung", () => {
    expect(berechneRabatt(0)).toBe(0);
  });

  it("weist einen negativen Bestellwert zurück", () => {
    expect(() => berechneRabatt(-1)).toThrow();
  });
});

Der erste Test deckt den Standardfall ab, der zweite den Grenzwert bei null, der dritte eine Eingabe außerhalb der Spezifikation. Alle drei folgen in knapper Form dem Muster Arrange, Act, Assert: Die Eingabe steht direkt im Aufruf, der Aufruf ist das Act, expect() das Assert. Im dritten Test fallen Act und Assert in eine Zeile, weil schon der Aufruf eine Ausnahme werfen muss. Aus einer Funktion mit echter Logik entstehen so meist drei bis fünf Testfälle. Mit JUnit in Java oder pytest in Python bleibt der Aufbau derselbe.

Wer schreibt die Tests?

In der Regel die Entwicklerinnen und Entwickler, und zwar für ihren eigenen Code. So steht es auch im ISTQB-Lehrplan Foundation Level: Komponententests führen normalerweise Entwickler in ihrer Entwicklungsumgebung durch. Niemand kennt die innere Logik einer Einheit besser als die Person, die sie geschrieben hat, und die Tests sollen stehen, bevor die Änderung in die gemeinsame Codebasis wandert.

Testerinnen und Tester schreiben Unit-Tests selten selbst. Schärfer machen können sie sie trotzdem, denn Grenzwerte, Äquivalenzklassen und Negativfälle sind Testentwurfswissen. Wenn jemand mit diesem Blick die Unit-Tests durchsieht, fallen fehlende Randfälle deutlich früher auf.

Was ist der Unterschied zwischen Unit- und Integrationstests?

Der Unit-Test prüft eine Einheit für sich, der Integrationstest das Zusammenspiel mehrerer Einheiten über ihre Schnittstellen. Damit die Isolation gelingt, ersetzen Stubs und Mocks die Abhängigkeiten und simulieren die Aufrufe und Antworten der Nachbarkomponenten. Ein Test ohne diese Isolation führt die Abhängigkeiten mit aus, ein Fehlschlag kann dann aus jeder von ihnen stammen.

Unit-TestIntegrationstest
Testobjekteinzelne Funktion, Klasse oder ModulZusammenspiel mehrerer Komponenten
Fokusinnere Logik der EinheitSchnittstellen und Datenaustausch
Abhängigkeitendurch Stubs und Mocks ersetztreal oder teilweise simuliert
TestbasisKomponentenspezifikation, Entwurf, CodeArchitektur, Schnittstellenbeschreibungen
Typische FehlerBerechnungen, Grenzwerte, AusnahmefälleDatenformate, Aufrufsequenzen, Fehlerweitergabe
LaufzeitMillisekunden bis Sekundenspürbar langsamer
VerantwortungEntwicklungEntwicklung, auf höheren Ebenen auch Testteams

Im ISTQB-Modell schließt der Komponentenintegrationstest direkt an und nimmt sich die Schnittstellen und Interaktionen zwischen den Einheiten vor. Inkompatible Datenformate oder falsche Aufrufreihenfolgen fallen erst dort auf.

Von der Testbasis zum Testfall

Bevor der erste Test entsteht, stehen drei Entscheidungen an: worauf er sich stützt, welche Testfälle er braucht und wo er mit welchen Daten läuft. In klassischen Projekten landen diese Entscheidungen in einem Stufentestkonzept für den Unit-Test. Agile Teams halten sie meist in ihrer Definition of Done fest.

Testbasis

Zur Testbasis gehört alles, was die Einheit beschreibt: Details aus Design oder Architektur, Teile einer User Story oder Anforderung, manchmal Komponentenspezifikationen oder Modelle. Für strukturorientierte Testfälle und die Zweigüberdeckung kommt der Sourcecode selbst dazu.

Testfälle ableiten

Auf dieser Stufe greifen die strukturierten Testentwurfsverfahren besonders gut: Äquivalenzklassenbildung, Grenzwertanalyse und Entscheidungstabellen, dazu kombinatorische Verfahren wie Pairwise oder der Klassifikationsbaum. Wertebereiche, Datentypen und Bedingungen sind hier viel konkreter als im System- oder Abnahmetest, das erleichtert die Ableitung. Ein Testfall ist nach ISTQB eine Menge aus Vorbedingungen, Eingaben, Aktionen (falls anwendbar), erwarteten Ergebnissen und Nachbedingungen, entwickelt auf Basis von Testbedingungen. Im Unit-Test passt er meist in eine einzige Testmethode.

Die meiste Aufmerksamkeit verdienen die Negativtests, denn genau dort sitzen die Fehler, die später teuer werden. Ein Unit-Test, der nur den Standardfall prüft, sagt wenig darüber, wie die Einheit mit korrupten Daten, Grenzwerten oder Parametern außerhalb der Spezifikation umgeht.

Umgebung und Daten

Unit-Tests laufen meist an zwei Orten: in der Entwicklungsumgebung, schnell und ohne Umstände, und auf dem Build-Server oder in der CI-Pipeline bei jedem Commit. Testtreiber, Stubs und Mocks vertreten die aufrufenden und aufgerufenen Klassen. Die Daten bleiben überschaubar, weil jeder Test nur einen Aspekt abdeckt; sie liegen direkt bei den Testfällen oder in einem gemeinsamen Repository. In größeren Projekten hilft ein Testdatengenerator bei Grenzfällen, die von Hand mühsam zu pflegen wären.

Wann wird Testen auf Unit-Ebene schwierig?

Einen einzelnen Unit-Test zu schreiben ist nicht schwer, mit einem Framework sind es wenige Zeilen. Schwierig wird es, wenn der Code nicht dafür gebaut ist. Eine Klasse, die selbst eine Datenbankverbindung öffnet, die Systemzeit liest und einen Webservice aufruft, lässt sich erst isolieren, wenn sich ihr Design ändert.

Die zweite Hürde ist Legacy-Code. Ohne bestehende Tests ist jedes Refactoring, das den Code testbar machen soll, selbst ein Risiko; viele Teams beginnen deshalb mit Tests rund um die Stellen, die sie als Nächstes ändern müssen. Die dritte Hürde ist Urteilsvermögen: Welche Fälle zählen, wie viel wird gemockt, und prüft ein Test Verhalten oder spiegelt er nur die Implementierung? Bei der ersten dieser Fragen helfen Testentwurfsverfahren, denn Äquivalenzklassen und Grenzwertanalyse beantworten systematisch, welche Fälle eine Einheit braucht.

Was macht einen guten Unit-Test aus?

Ein guter Unit-Test ist schnell, unabhängig von anderen Tests, jederzeit mit demselben Ergebnis wiederholbar und wertet sich selbst aus: Er wird grün oder rot, ohne dass jemand Logdateien deuten muss. In der Clean-Code-Community heißen diese Eigenschaften FIRST-Prinzipien (Fast, Independent, Repeatable, Self-validating, Timely).

Dazu kommt Lesbarkeit. Ein guter Unit-Test prüft einen Aspekt, trägt einen Namen, der sagt, was er erwartet, und folgt dem Aufbau Arrange, Act, Assert. So geschriebene Tests dokumentieren nebenbei, wie eine Klasse gedacht ist.

Mocking mit Augenmaß

Mocking ersetzt die Abhängigkeiten einer Einheit durch simulierte Objekte, damit der Test isoliert und schnell läuft. Das ISTQB fasst solche Ersatzkomponenten unter dem Begriff Testdouble zusammen und unterscheidet mehrere Arten: Ein Platzhalter (Stub) gibt vordefinierte Antworten, ein Mock simuliert das erwartete Verhalten der Komponente während des Tests. Statt der echten Datenbank liefert ein Stub zum Beispiel einen festen Kundendatensatz; der Mock eines Webservice kann zusätzlich prüfen, ob die Einheit ihn wie erwartet aufgerufen hat. Sinnvoll sind Testdoubles, wenn eine Abhängigkeit langsam ist, nicht verfügbar oder schwer in einen definierten Zustand zu bringen.

Meine Faustregel: mocken, was langsam oder extern ist, und echte Objekte überall dort verwenden, wo sie schnell und deterministisch sind. Ein Test, der jede Abhängigkeit ersetzt, prüft vor allem, ob simuliertes Verhalten korrekt durchgereicht wird, und sagt wenig über die eigene Logik der Einheit.

Test zuerst: Test Driven Development

Unit-Tests entstehen am besten parallel zum Code oder davor, spätestens aber, bevor die Änderung in die gemeinsame Codebasis wandert. Auf “später” verschobene Tests schreibt kaum jemand mehr, denn der Aufwand wächst, sobald die Details der Einheit nicht mehr präsent sind.

Test Driven Development (TDD) dreht die Reihenfolge ganz um: Test schreiben, scheitern sehen, gerade so viel Code schreiben, dass er besteht, dann aufräumen. Dieser Rhythmus macht den Code von Anfang an testbar, und das sieht man der Architektur an. TDD verlangt allerdings Disziplin und funktioniert am besten bei Logik mit klarer Spezifikation.

Wenn die Prozentzahl zum Ziel wird

Codeüberdeckung zeigt, wie viel des Sourcecodes die Tests durchlaufen, gemessen etwa als Anweisungs- oder Zweigüberdeckung. Viele Teams machen daraus ein Endekriterium oder einen Punkt der Definition of Done: “Unit-Tests müssen 80 Prozent Codeüberdeckung erreichen.”

Die Zahl lässt sich dann mit möglichst einfachen Tests treiben, Sonderfälle und Negativtests fallen weg, weil sie die Prozentzahl kaum bewegen. Hundert Prozent Überdeckung mit schwachen Tests sind weniger wert als siebzig Prozent mit guten. Als Diagnosewerkzeug taugt die Überdeckung, weil sie zeigt, welche Bereiche kein Test berührt, über die Qualität der Tests sagt sie dagegen wenig.

Automatisch bei jedem Commit: Frameworks und CI-Pipeline

Auf keiner anderen Teststufe ist Automatisierung so selbstverständlich wie hier, denn ein Unit-Test ist dafür gemacht, bei jeder Codeänderung in Sekunden mitzulaufen. Eine Einheit im Debugger durchzugehen findet vielleicht einen Fehler, hinterlässt aber keinen Test, der beim nächsten Commit wieder läuft.

Ein Unittest-Framework liefert dafür das Gerüst: Es organisiert die Testfälle, führt sie aus und meldet die Ergebnisse. Für alle gängigen Sprachen gibt es etablierte Frameworks: JUnit für Java, pytest für Python, NUnit für .NET, Jest oder Vitest für JavaScript und TypeScript. In der CI-Pipeline läuft das Framework als Build-Schritt bei jedem Commit oder Pull Request, etwa in GitHub Actions, GitLab CI oder Jenkins, und ein fehlgeschlagener Test bricht den Build ab. Damit das trägt, braucht es kurze Laufzeiten (Minuten, nicht Stunden), einen lesbaren Testreport und die Regel, dass ein roter Build sofort repariert wird.

Agile Teams sind darauf am stärksten angewiesen. Wer alle zwei Wochen liefert, kann nicht vor jedem Release das ganze Produkt von Hand nachprüfen. Die Testpyramide bildet das ab. Mike Cohns ursprüngliches Modell kennt drei Schichten, Unit-Tests an der Basis, Service-Tests in der Mitte und UI-Tests an der Spitze; eine verbreitete Variante nennt sie Unit-, Integrations- und End-to-End-Tests. In beiden liegen unten viele kleine, schnelle, isolierte Tests und oben wenige langsame, die große Teile der Funktionalität prüfen.

Wann sich der Aufwand nicht lohnt

Jede Einheit mit eigener Logik braucht Unit-Tests. Ausnahmen sind trivialer Code ohne solche Logik, etwa reine Getter und Setter, Wegwerf-Prototypen und Bereiche, in denen das Risiko im Zusammenspiel liegt, etwa reine Konfiguration oder UI-Layout.

Ein Sonderfall ist explorative Arbeit an einem unklaren Problem. Dort kann es sinnvoll sein, mit den Tests zu warten, bis die Struktur steht.

Drei Probleme, die ich immer wieder sehe

In Projekten treffe ich immer wieder auf dieselben drei Probleme:

  1. 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.
  2. Unit-Tests entstehen gar nicht erst. Zu Beginn geht das gut: Die Software ist überschaubar, alles läuft, der Fortschritt sieht großartig aus. Im Hintergrund wächst aber ein technischer Schuldenberg, der sich später nur mühsam abtragen lässt.
  3. Tests gelten als eigene Aufgabe. “Ich bin fertig, ich muss nur noch Tests schreiben” zeigt, dass das Team Unit-Tests als Anhängsel sieht. In den Teams, die ich am besten arbeiten sehe, ist eine Aufgabe erst fertig, wenn auch ihre Tests stehen, und diese Haltung führt zu deutlich besseren Ergebnissen.

Was eine grüne Testsuite nicht zeigt

Unit-Tests prüfen, ob eine Einheit 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. Das fangen erst die höheren Teststufen und der Abgleich mit den Anforderungen auf, und auch das Zusammenspiel der Einheiten überlassen Unit-Tests dem Integrationstest.

Aus der Praxis

Der Unit-Test ist für mich die wichtigste aller Teststufen. Auf ihn schaue ich zuerst, wenn ich ein neues Beratungsprojekt beginne, denn hier sehe ich, wie stabil eine Anwendung ist. Ein Mindestmaß an Unit-Tests haben die meisten Teams inzwischen, mehr als auf jeder anderen Stufe, und auf keiner anderen Stufe gewinnt ein Projekt mit so wenig Aufwand so viel. Bei der Umsetzung hakt es trotzdem oft: Der Fokus liegt auf der Abdeckungszahl, oder Tests zu schreiben gilt als lästige Pflicht.

Häufig gestellte Fragen

Modultest ist ein älterer Name für den Unit Test: Geprüft wird die kleinste testbare Einheit einer Software, getrennt vom Rest des Systems. Der ISTQB-Lehrplan nennt die Stufe Komponententest, in Projekten sagt man meist Unit-Test. Unterschiede in Ziel, Vorgehen oder Werkzeugen gibt es zwischen den Bezeichnungen nicht.

Unit Testing meint die Tätigkeit, also das Entwerfen, Schreiben, Ausführen und Auswerten der Tests. Dazu gehört, Testfälle aus der Spezifikation einer Einheit abzuleiten, Abhängigkeiten durch Testdoubles zu ersetzen und die Tests in den Build einzubinden. Unit Testing ist also der ganze Arbeitsschritt rund um die Tests.

Das ISTQB-Glossar beschreibt ein Unit-Testframework als Werkzeug, das eine Umgebung für das Testen einer Unit in Isolation bereitstellt. Praktisch heißt das: Testfälle als Code schreiben, gruppieren und mit einem Bestanden-oder-Fehlgeschlagen-Bericht ausführen, lokal wie auf dem Build-Server. Bekannte Beispiele sind JUnit, pytest, NUnit, Jest und Vitest.

Als Nächstes kommt der Komponentenintegrationstest, der die Schnittstellen zwischen den einzeln getesteten Einheiten prüft. Danach folgen Systemtest, Systemintegrationstest und Abnahmetest. Der ISTQB-Lehrplan Foundation Level 4.0 beschreibt damit fünf Teststufen; der Unit-Test, dort Komponententest genannt, ist die unterste davon. Von Stufe zu Stufe wächst das Testobjekt, vom einzelnen Codeteil bis zum Zusammenspiel mit anderen Systemen.

Unit-Tests fangen falsche Berechnungen, unsaubere Grenzwerte und unbehandelte Ausnahmen dort ab, wo sie am billigsten zu beheben sind: in der Einheit, die sie enthält. Mit der Zeit werden sie zum Sicherheitsnetz für Refactorings, und die höheren Teststufen bauen auf Einheiten auf, die bereits wie spezifiziert funktionieren.

Technisch ja, sinnvoll ist es nicht. Ein manuell geprüfter Fall ist nach dem nächsten Commit wieder ungeprüft, ein automatisierter läuft bei jeder Änderung von selbst mit. Debugger oder REPL bleiben nützlich, um einen Fehler zu finden; den Testfall, der ihn künftig abfängt, ersetzen sie nicht.

Podcastfolgen und Artikel zu Testgrundlagen & Testentwurf

Testgrundlagen sicher anwenden

Im ISTQB Foundation Level lernst du Teststufen, Testverfahren und Testmanagement systematisch und praxisnah kennen.