Der erste Test gehört an den letzten Fehler
In einer gewachsenen Anwendung entscheidet nicht die Testtechnik über den Nutzen, sondern die Auswahl: Wenige Tests an den richtigen Stellen tragen mehr als hundert an den leichten.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Ohne Auswahl endet der Anlauf nach zwei Wochen
Der übliche Start: Es wird mit dem Verzeichnis für Hilfsfunktionen begonnen, weil das ohne Umbau testbar ist. Nach zwei Wochen gibt es zweihundert Tests, die prüfen, dass eine Zeichenkette gekürzt und ein Datum formatiert wird. Die Abdeckung steigt sichtbar, die Fehler im Betrieb bleiben, weil sie in der Preisberechnung und im Zustandswechsel des Bestellvorgangs entstehen, und dort ist noch nichts getestet.
Der zweite verbreitete Fehlstart ist die Zielvorgabe. Wird eine Abdeckungszahl vereinbart, bevor die ersten sinnvollen Tests existieren, entstehen Tests, die Code ausführen, ohne etwas zu prüfen. Solche Tests erfüllen die Vorgabe und sagen nichts aus. Wenn sie später brechen, versteht niemand ihre Absicht, also werden sie übersprungen, und aus dem übersprungenen Test wird ein dauerhafter Zustand.
Was diese Anläufe wirklich kostet, ist nicht die investierte Zeit, sondern die Schlussfolgerung daraus. Nach dem zweiten gescheiterten Versuch gilt im Team als ausgemacht, dass sich diese Anwendung nun einmal nicht testen lässt. Danach wird jede Änderung wieder von Hand durchgeklickt, und größere Umbauten unterbleiben, weil sich niemand traut.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Der Einstieg in sieben Schritten
Die Reihenfolge ist so gewählt, dass nach jedem Schritt etwas Brauchbares vorliegt. Wenn der Anlauf nach Schritt drei unterbrochen wird, hast du trotzdem einen Test, der einen echten Fehler festhält.
- 1
Den letzten echten Fehler heraussuchen
Nimm einen Fehler aus dem Betrieb, für den du die Eingabedaten von damals noch bekommst. Schreib in einem Satz auf, was hätte passieren sollen und was passiert ist. Das ist bereits die Beschreibung deines ersten Tests.
Geschafft, wenn: ein Fall mit konkreten Eingaben und einem erwarteten Ergebnis, formuliert ohne Code
- 2
Die Testumgebung minimal aufsetzen
Ein Testwerkzeug, eine Konfigurationsdatei, ein Eintrag in den Skripten. Für Code ohne DOM genügt die Node-Umgebung, für Komponenten kommt jsdom oder happy-dom dazu. Widersteh der Versuchung, gleich Abdeckungsberichte, Momentaufnahmen und einen Pipeline-Schritt mit einzurichten.
Geschafft, wenn: ein einziger Befehl, der ohne Zusatzargumente läuft und einen bewusst grünen Test meldet
- 3
Den Fehler als roten Test nachstellen
Schreib den Test so, dass er mit dem aktuellen Code fehlschlägt. Wenn er nicht fehlschlägt, hast du den Fehler noch nicht verstanden oder triffst eine andere Stelle als gedacht. Erst wenn er rot ist, folgt die Korrektur.
Geschafft, wenn: ein Test, der ohne die Korrektur fehlschlägt und mit ihr durchläuft
- 4
Eine Naht in den Code schneiden
Zieh die Regel aus ihrer Umgebung heraus: die Berechnung aus dem Ereignisrückruf, die Umwandlung aus der Anzeige. Übergib der neuen Funktion alles, was sie braucht, statt es sich holen zu lassen. Ein Standardwert am Parameter lässt bestehende Aufrufe unverändert.
Geschafft, wenn: eine Funktion, die aus ihren Eingaben ein Ergebnis macht, ohne auf Globales zuzugreifen
- 5
Zeit, Zufall und Netzwerk stillstellen
Ersetz neue Date-Objekte durch eine übergebene Uhr oder eine Zeitsteuerung des Testwerkzeugs, Zufall durch einen festen Wert und HTTP-Aufrufe durch eine Attrappe auf Protokollebene. Damit hängt das Ergebnis nicht mehr am Tag, an dem der Test läuft.
Geschafft, wenn: zwei Läufe mit identischem Ergebnis, auch am Monatsletzten und um 23:59 Uhr
- 6
Randfälle ergänzen
Zu jeder Regel gehören die Grenze selbst, ein Wert knapp darunter und darüber, der leere Fall und der Fehlerfall. Ein Test pro Regel reicht, wenn er die Grenze trifft, und zehn Tests reichen nicht, wenn sie alle die Mitte prüfen.
Geschafft, wenn: je Regel mindestens ein Test, der die Grenze prüft und nicht den bequemen Fall
- 7
Bei jedem Push laufen lassen
Die Tests müssen automatisch laufen, sonst werden sie beim ersten Zeitdruck übergangen. Ein roter Lauf blockiert das Zusammenführen. Übersprungene Tests bekommen eine Frist, sonst wachsen sie zu einem stillen Bestand heran, den niemand mehr anfasst.
Geschafft, wenn: ein roter Lauf verhindert die Zusammenführung, statt in einer Liste zu landen
Fünf Kriterien, die die Reihenfolge festlegen
- 01 Ist hier schon einmal ein Fehler in den Betrieb gelangt? Dann zuerst hierhin.
- 02 Wird die Datei oft geändert? Häufige Änderungen brauchen ein Netz.
- 03 Steckt eine Regel darin, also eine Berechnung oder ein Zustandswechsel?
- 04 Hängt Geld daran, etwa Preis, Menge, Rabatt oder Versand?
- 05 Leicht zu testen, aber harmlos? Diese Stellen kommen zuletzt.
Was du danach ohne langes Abwägen entscheidest
Testbarkeit ist keine Eigenschaft eines Werkzeugs, sondern eine Eigenschaft des Codes. Der Einstieg besteht deshalb aus zwei Bewegungen: die richtigen Stellen auswählen und an genau diesen Stellen so viel Struktur schaffen, dass ein Test überhaupt möglich wird.
Am Fehler anfangen, nicht am Einfachen
Der letzte Fehler aus dem Betrieb liefert dir Eingabedaten, erwartetes Verhalten und eine Begründung in einem. Er ist außerdem der einzige Test, dessen Nutzen niemand infrage stellt.
Reine Funktionen herausschneiden
Eine Funktion, die aus Argumenten ein Ergebnis berechnet und sonst nichts anfasst, ist ohne Aufbau testbar. Meist genügt es, die Berechnung aus dem Ereignisrückruf herauszuziehen und ihr die Werte zu übergeben.
Abhängigkeiten als Parameter übergeben
Uhrzeit, Zufall, Netzwerkzugriff und Speicher gehören nicht in die Logik, sondern werden ihr von außen übergeben. Ein Standardwert am Parameter hält alle bestehenden Aufrufstellen unverändert und macht den Test trotzdem möglich.
An den Systemgrenzen ersetzen, nicht im Inneren
Ersetz HTTP, Uhr und Dateisystem, also das, was du nicht besitzt. Wer eigene Module durch Attrappen ersetzt, prüft am Ende nur noch die eigene Annahme über sie.
Randfälle statt Sonnenscheinpfad
Der Mittelwert findet selten einen Fehler. Getestet gehören die Grenze, der leere Fall, der doppelte Eintrag und der Fehlerpfad, denn dort stecken die Regeln, über die niemand nachgedacht hat.
Abdeckung als Landkarte lesen
Die Zahl sagt nur, welche Zeilen ausgeführt wurden, nicht, ob etwas geprüft wurde. Nützlich ist die Ansicht der nicht berührten Stellen, nicht der Prozentwert darüber.
KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Welche Stellen zuerst
Die drei brauchbarsten Auswahlkriterien lassen sich ohne Diskussion prüfen. Erstens die Fehlerhistorie: Wo schon einmal etwas schiefging, geht wieder etwas schief. Zweitens die Änderungshäufigkeit, denn was oft angefasst wird, bricht oft. Die Liste dazu liefert die Versionsverwaltung selbst, etwa mit git log über das letzte Jahr, nur die Dateinamen ausgegeben, gezählt und absteigend sortiert. Drittens die fachliche Dichte: Preise, Rabatte, Fristen, Berechtigungen und Zustandswechsel.
Was auf dieser Liste selten weit oben steht, sind die Hilfsfunktionen: leicht zu testen, kaum in Bewegung und arm an Regeln. Ein paar Tests dort schaden nicht, nur sollten sie nicht der Inhalt der ersten zwei Wochen sein, weil sie den Eindruck erwecken, es gehe voran, während die riskanten Stellen unberührt bleiben.
Die zweite Achse ist die Teststufe. Die meisten Regeln lassen sich als Unit-Test prüfen, wenn sie erst einmal freigelegt sind. Für das Zusammenspiel mehrerer Teile ist ein Integrationstest ohne Browser oft das bessere Verhältnis von Aufwand zu Aussage. Und für die zwei oder drei Wege, an denen das Geschäft hängt, lohnt ein Test durch die echte Oberfläche, mehr davon werden schnell langsam und wackelig.
Warum sich alter Code nicht testen lässt
Die Gründe wiederholen sich. Module führen beim Import bereits etwas aus, bauen eine Verbindung auf oder lesen Konfiguration. Objekte werden mitten in der Logik mit new erzeugt, statt hineingegeben zu werden. Die Zeit kommt aus einem neuen Date-Objekt in der Berechnung selbst. Zugriffe gehen direkt auf document oder auf ein globales fetch. Jeder dieser Punkte macht aus einer Funktion etwas, das nur in einer vollständigen Anwendung läuft.
Die Gegenbewegung heißt Naht: eine Stelle, an der sich Verhalten austauschen lässt, ohne den Code zu ändern. Die billigste Naht ist ein zusätzlicher Parameter mit Standardwert, dicht gefolgt vom Herausziehen einer reinen Funktion. Beides lässt sich in Minuten machen und ändert an den bestehenden Aufrufstellen nichts. Deutlich teurer sind Nähte, die eine Klassenstruktur umbauen, und die brauchst du am Anfang nicht.
Wenn du nicht sicher weißt, was der Code tun soll, dreh die Reihenfolge um: Schreib einen Test, der festhält, was er heute tut, auch wenn das seltsam aussieht. Michael Feathers nennt das in Working Effectively with Legacy Code einen Characterization Test, und dort steht auch die nüchterne Definition, die diesen Einstieg begründet: Alter Code ist Code ohne Tests. Für eine anstehende Ablösung ist eine solche Sammlung das genaueste vorhandene Lastenheft.
Was Abdeckung sagt und was nicht
Abdeckung misst, welche Zeilen und Zweige während der Testläufe ausgeführt wurden. Ob dabei etwas geprüft wurde, misst sie nicht. Ein Test ohne eine einzige Erwartung hebt die Zahl genauso wie ein sorgfältig geschriebener. Deshalb ist ein Zielwert als Vorgabe eine der zuverlässigsten Methoden, wertlose Tests zu erzeugen: Sobald die Zahl das Ziel ist, hört sie auf, ein Messwert zu sein.
Als Werkzeug ist der Bericht trotzdem nützlich, aber in der anderen Richtung. Interessant sind nicht die 68 Prozent oben, sondern die rot markierten Zweige in einer Datei, die Regeln enthält. Ein nie ausgeführter Fehlerpfad in der Zahlungsabwicklung ist eine belastbare Aussage, der Prozentwert über die ganze Anwendung ist keine.
Wer wissen will, ob die vorhandenen Tests tatsächlich etwas fangen, kommt mit Mutationstests weiter. Dabei werden gezielt kleine Änderungen in den Code eingebaut, ein Vergleich umgedreht, eine Bedingung entfernt, und geprüft, ob ein Test daraufhin rot wird. Bleibt alles grün, ist die Zeile zwar abgedeckt, aber ungeprüft. Für JavaScript gibt es dafür Stryker.
Attrappen, die den Test wertlos machen
Die Grundregel lautet: Ersetz das, was du nicht besitzt. HTTP-Aufrufe, Uhrzeit, Zufall, Dateisystem und fremde Dienste sind gute Kandidaten. Eigene Module durch Attrappen zu ersetzen führt dagegen dazu, dass der Test nur noch prüft, ob deine Annahme über das eigene Modul zu deiner Annahme passt. Bricht das Modul später, bleibt der Test grün.
Bei HTTP ist die Ersetzung auf Protokollebene die stabilere Variante. Werkzeuge wie MSW fangen die Anfrage ab und antworten, ohne dass dein Code weiß, dass er im Test läuft. Der Vorteil zeigt sich beim Umbau: Ob intern fetch oder eine Bibliothek verwendet wird, ändert am Test nichts mehr, weil er auf der Ebene sitzt, die tatsächlich verabredet ist.
Der zweite häufige Fehler ist zu genaues Prüfen. Ein Test, der die Reihenfolge interner Aufrufe festschreibt, bricht bei jedem Umbau, ohne dass ein Fehler vorliegt. Prüf lieber das beobachtbare Ergebnis: den Rückgabewert, den geschriebenen Zustand, die abgeschickte Anfrage. Bei Oberflächen heißt das, über Rolle und Beschriftung zu suchen statt über Klassennamen, denn Rolle und Beschriftung sind das, was auch die Nutzer sehen.
Woran du einen brauchbaren Test erkennst
Er hat genau einen Grund, rot zu werden. Sein Name nennt die Regel und nicht die Funktion: gibt bei leerem Warenkorb null zurück sagt beim Fehlschlag bereits, was kaputt ist, während berechneSumme funktioniert nichts sagt. Er läuft unabhängig von seinen Nachbarn, teilt also keinen veränderlichen Zustand, und er läuft in beliebiger Reihenfolge, was sich mit gemischter Reihenfolge im Testlauf leicht überprüfen lässt.
Der Aufbau folgt drei Abschnitten: Ausgangslage herstellen, Aktion ausführen, Ergebnis prüfen. Wenn der erste Abschnitt dreißig Zeilen braucht, ist das eine Aussage über den Code und nicht über den Test. Genau an dieser Stelle lohnt sich der Umbau, denn ein aufwendiger Aufbau bedeutet, dass die Funktion zu viel gleichzeitig weiß.
Und schließlich muss ein Test ohne Netz und ohne besondere Umgebung laufen. Ein Test, der auf dem eigenen Rechner grün ist und in der Pipeline gelegentlich rot, wird nach dem dritten Mal übersprungen. Die häufigsten Ursachen dafür sind echte Zeit, echte Zufallszahlen, echte Netzwerkzugriffe und ein Aufräumen, das zwischen den Tests fehlt.
Dazu passende Kurse
Wie sich das ohne großen Umbau angehen lässt, üben die JavaScript-Kurse bei cmt am Beispiel, dort geht es unter anderem um die ersten Tests in einem gewachsenen Projekt .
Wer Teststufen, Testarten und Abnahmekriterien für ein ganzes Team ordnen muss, findet dafür Testverfahren und ihre Grenzen im Überblick .
Wie sicher bist du beim Thema wirklich?
Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.
Wo du genau das übst
Ein sehr gutes, praxisorientiertes und nachhaltiges Seminar. So sollte es immer sein. Vielen Dank.
Die Schulung ist gerade für Einsteiger oder Entwickler*innen mit eingestaubtem Basiswissen sehr hilfreich.
Ich bin sehr glücklich Franz als Trainer gehabt zu haben. Er hat offensichtlich unglaublich viel Wissen zu den Themen, und schafft es, dieses auch interaktiv und verständlich weiterzugeben.
Häufige Fragen
Unit-Tests oder Tests durch die Oberfläche, was zuerst?
Wie viel Abdeckung ist genug?
Lohnen sich Tests für ein Projekt, das ohnehin abgelöst wird?
Was mache ich mit Code, den ich nicht ändern darf?
Deine Ansprechpartner
Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.
Yves Hoppe
Weiterbildung & Beratung
Hilft dir, aus dem Webentwicklung-Programm den passenden Kurs für deinen Stand zu finden.
Norbert Jansen
Beratung & Inhouse
Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.
Tests, die eine Änderung tatsächlich absichern
In den Test-Kursen bei cmt arbeitest du an einer gewachsenen Anwendung, schneidest Nähte in bestehenden Code und schreibst die Tests, die eine Änderung wirklich absichern.