Wie Gegenstände können Tests kaputtgehen. Es ist nicht so, dass sie sich abnutzen, sondern vielmehr, dass sich
irgendetwas in ihrer Umgebung geändert hat. Möglicherweise wurden die Tests auf ein neues Betriebssystem portiert oder,
was wahrscheinlicher ist, der Code, den die Tests ausführen, hat sich so geändert, dass die Tests
berechtigterweise scheitern. Angenommen, Sie arbeiten an Version 2.0 einer Anwendung für Online-Banking. In
Version 1.0 wurde die folgende Methode für die Anmeldung verwendet:
public boolean login (String Benutzername);
In Version 2.0 hat der Vertrieb festgestellt, dass Kennwortschutz eine gute Idee sein könnte. Deshalb wurde die Methode
wie folgt geändert:
public boolean login (String Benutzername, String Kennwort);
Alle Tests, die "login" verwenden, scheitern und lassen sich nicht einmal kompilieren. Da zu diesem Zeitpunkt ohne
Anmeldung nicht viel getestet werden kann, können nicht viele sinnvolle Tests ohne die Methode "login" geschrieben
werden. Sie stehen unter Umständen Hunderte oder Tausenden von Tests gegenüber, die scheitern.
Diese Tests können korrigiert werden, indem ein Tool für globales Suchen und Ersetzen verwendet wird, das alle
Vorkommen von login(xxx) findet und durch login(xxx, "Euer Kennwort") ersetzt. Anschließend müssen Sie
dafür sorgen, dass alle Testkonten dieses Kennwort verwenden, und schon funktioniert die Sache.
Wenn der Vertrieb jetzt entscheidet, dass Kennwörter keine Leerzeichen enthalten dürfen, können Sie wieder von vorn
anfangen.
Solche Änderungen sind verschwendete Zeit, insbesondere, wenn die Änderungen nicht so leicht durchzuführen sind, wie es
häufig der Fall ist. Es gibt eine bessere Lösung.
Angenommen, die Tests rufen ursprünglich gar nicht die Methode login des Produkts auf.
Stattdessen rufen sie eine Bibliotheksmethode auf, die alles Erforderliche tut, damit sich der Test anmelden und seine
Arbeit tun kann. Diese Methode könnte zunächst wie folgt aussehen:
public boolean testLogin (String Benutzername) {
return product.login(Benutzername);
}
Wenn die Änderung in Version 2.0 vorgenommen wird, wird die Dienstprogrammbibliothek wie folgt geändert:
public Boolean testLogin (String Benutzername) {
return product.login(Benutzername
, "Euer Kennwort");
}
Anstatt Tausende von Tests zu ändern, ändern Sie genau eine Methode.
Im Idealfall sind alle erforderlichen Bibliotheksmethoden bereits am Anfang der Tests vorhanden. In der Praxis ist es
jedoch so, dass nicht alle Methoden vorhersehbar sind. Dass Sie eine Bibliotheksmethode testLogin benötigen, erkennen Sie wahrscheinlich erst, wenn sich die Methode login für das Produkt ändert. Dienstprogrammmethoden für Tests ergeben sich häufig aus vorhandenen
Tests. Es ist sehr wichtig, dass Sie diese Testkorrekturen fortlaufend durchführen, selbst wenn der Zeitplan
sehr eng ist. Wenn Sie es nicht tun, verschwenden Sie viel Zeit mit einer hässlichen und nicht verwaltbaren Testsuite.
Unter Umständen müssen Sie sie sogar völlig verwerfen, oder Sie sind nicht in der Lage, die erforderlichen neuen Tests
zu schreiben, weil Sie Ihre gesamte verfügbare Zeit damit verbringen, alte Tests zu verwalten.
Anmerkung: Die Tests der Methode login des Produkts rufen die Methode weiterhin direkt
auf. Wenn sich ihr Verhalten ändert, müssen einige oder alle dieser Tests aktualisiert werden. (Wenn keiner der Tests
für die Methode login scheitert, sind sie wahrscheinlich nicht besonders gut darin, Mängel
aufzuspüren.)
Das vorherige Beispiel zeigt, wie Tests von der konkreten Anwendung abstrahieren können. Wahrscheinlich können Sie noch
erheblich mehr abstrahieren. Sie stellen möglicherweise fest, dass eine Reihe von Tests mit einer gemeinsamen Abfolge
von Methodenaufrufen beginnen. Sie melden sich an, richten einen Zustand ein und navigieren zu dem Teil der Anwendung,
den Sie testen. Erst dann führt jeder Test etwas Spezifisches aus. Die gesamte Konfiguration kann und sollte in einer
einzigen Methode mit einem sinnträchtigen Namen wie readyAccountForWireTransfer enthalten sein.
Damit sparen Sie viel Zeit ein, wenn neue Tests eines bestimmten Typs geschrieben werden, und machen gleichzeitig den
Zweck jedes einzelnen Tests wesentlich verständlicher.
Verständliche Tests sind wichtig. Alte Testsuites haben häufig das Problem, dass niemand weiß, was die Tests eigentlich
tun und warum. Wenn sie scheitern, werden sie in der Regel auf die einfachste Weise korrigiert. Das Ergebnis sind
häufig Tests, die weniger Mängel finden. Sie testen nicht mehr das, was sie ursprünglich testen sollten.
Angenommen, Sie testen einen Compiler. Einige der ersten geschriebenen Klassen definieren den internen
Syntaxanalysebaum des Compilers und die entsprechenden Transformationen. Sie haben eine Reihe von Tests, die
Syntaxanalysebäume erstellen und die Transformationen testen. Ein solcher Test können wie folgt aussehen:
/*
* Vorgabe:
* while (i<0) { f(a+i); i++;}
* "a+i" kann nicht aus der Schleife verschoben werden, weil
* der Ausdruck eine Variable enthält, die in der Schleife geändert wird.
*/
loopTest = new LessOp(new Token("i"), new Token("0"));
aPlusI = new PlusOp(new Token("a"), new Token("i"));
statement1 = new Statement(new Funcall(new Token("f"), aPlusI));
statement2 = new Statement(new PostIncr(new Token("i"));
loop = new While(loopTest, new Block(statement1, statement2));
expect(false, loop.canHoist(aPlusI))
Dieser Test ist schwierig zu lesen. Die Zeit vergeht. Es werden Änderungen vorgenommen, die eine Aktualisierung der
Tests erfordern. Jetzt haben Sie mehr Produktinfrastruktur, auf die Sie sich stützen können. Möglicherweise haben Sie
sogar eine Parsing-Routine, die Zeichenfolgen in die Syntaxanalysebäume umwandelt. In diesem Fall wäre es besser, die
Tests komplett umzuschreiben. Beispiel:
loop=Parser.parse("while (i<0) { f(a+i); i++; }");
// Zeiger auf den Teil "a+i" der Schleife abrufen.
aPlusI = loop.body.statements[0].args[0];
expect(false, loop.canHoist(aPlusI));
Solche Tests sind wesentlich einfacher zu verstehen und führen zu einer direkten und künftigen Zeitersparnis. Die
Verwaltungskosten für diese Tests sind so viel niedriger, dass es Sinn machen kann, die meisten so lange aufzuschieben,
bis der Parser verfügbar ist.
Dieser Ansatz hat jedoch einen kleinen Nachteil: Solche Tests können einen Mangel im Transformationscode (was
beabsichtigt ist) oder im Parser (was nicht beabsichtigt ist) finden. Somit können Problemeingrenzung und Debugging ein
wenig schwieriger werden. Auf der anderen Seite ist das Aufspüren eines Problems, das die Parsertests nicht erkennen,
gar keine so schlechte Sache.
Es besteht außerdem die Gefahr, dass ein Mangel im Parser einen Mangel im Transformationscode verdeckt. Die Gefahr ist
nur sehr klein, und die daraus entstehenden Kosten wahrscheinlich geringer als die Kosten für die Verwaltung der
komplizierteren Tests.
Eine umfangreiche Testsuite enthält einige Testblöcke, die sich nicht ändern. Dies betrifft die stabilen Bereiche in
der Anwendung. Andere Testblöcke ändern sich häufig. Diese beziehen sich auf die Bereiche in der Anwendung, in denen
sich das Verhalten oft ändert. Diese letzteren Testblöcke machen in der Regel einen intensiveren Gebrauch von
Dienstprogrammbibliotheken. Jeder Test testet spezifische Verhalten in dem veränderlichen Bereich. Die
Dienstprogrammbibliotheken sind so konzipiert, dass sie einen solchen Test zulassen, um die gewünschten Verhalten zu
prüfen, sind aber relativ immun gegen Änderungen in nicht getestetem Verhalten.
Der zuvor gezeigte Schleifentest ist jetzt immun gegenüber den Details für die Erstellung von Syntaxanalysebäumen. Er
ist weiterhin sensibel für die Struktur eines Syntaxanalysebaums einer while-Schleife (wegen der
Abfolge der Zugriffe, die zum Abrufen der untergeordneten Baumstruktur für a+i erforderlich sind). Wenn sich diese
Struktur als veränderlich herausstellt, kann der Test abstrakter gemacht werden, indem eine Dienstprogrammmethode fetchSubtree erstellt wird:
loop=Parser.parse("while (i<0) { f(a+i); i++; }");
aPlusI = fetchSubtree(loop, "a+i");
expect(false, loop.canHoist(aPlusI));
Der Test ist jetzt nur für zwei Dinge sensibel: die Definition der Sprache (z. B., dass Integer mit ++ inkrementiert werden können) und die Regeln, die Codeverschiebung (Hoisting) für Schleifen regeln
(das Verhalten, das auf Richtigkeit getestet wird).
Selbst mit Dienstprogrammbibliotheken kann ein Test durch Verhaltensänderungen, die nichts mit dem zu tun haben, was
getestet werden soll, regelmäßig scheitern. Eine Korrektur des Tests verspricht nicht viel Chancen, einen Mangel zu
finden, der auf die Änderung zurückzuführen ist, sondern ist lediglich eine Maßnahme, um mit dem Test möglicherweise
irgendwann einmal irgendeinen anderen Mangel zu finden. Die Kosten für eine solche Serie von Korrekturen übersteigen
jedoch möglicherweise den hypothetischen Nutzen (findet er einen Mangel oder nicht) des Tests. Es ist unter Umständen
besser, den Test einfach zu verwerfen und sich der Erstellung neuer Tests mit einem größeren Nutzen zu widmen.
Die meisten Leute zögern, einen Test zu verwerfen, zumindest so lange, bis sie von der Verwaltungslast so überwältigt
werden, dass sie alle Tests verwerfen. Es ist besser, die Entscheidung sorgfältig und Test für Test zu treffen,
indem Sie sich die folgenden Fragen stellen:
-
Wie viel Arbeit ist es, diesen Test wirklich zu korrigieren und möglicherweise zur Dienstprogrammbibliothek
hinzuzufügen?
-
Kann die Zeit anderweitig genutzt werden?
-
Wie wahrscheinlich ist es, dass der Test künftig schwerwiegende Mängel findet? Wie war die bisherige Erfolgsquote
dieses und der zugehörigen Tests.
-
Wie lange dauert es, bis der Test erneut scheitert?
Die Antworten auf diese Fragen sind grobe Schätzungen, möglicherweise sogar nur Vermutungen. Aber diese Fragen zu
stellen, liefert bessere Ergebnisse, als einfach die Richtlinie zu verfolgen, alle Tests zu korrigieren.
Ein weiterer Grund, Tests zu verwerfen, ist Redundanz. In frühen Stadien der Entwicklung kann es eine Vielzahl
einfacher Tests geben, die grundlegende Methoden für den Aufbau eines Syntaxanalysebaums (Konstruktor LessOp usw.) testen. Später, beim Schreiben des Parsers gibt es eine Reihe von Parsertests. Da der
Parser die Konstruktionsmethoden verwendet, werden diese von den Parsertests auch indirekt getestet. Wenn
Codeänderungen die Konstruktionstests zum Scheitern bringen, ist es angebracht, einige dieser Tests zu verwerfen, da
sie redundant sind. Natürlich erfordert jede neue oder geänderte Konstruktion neue Tests. Sie können direkt
implementiert werden (falls der Parser sie nur unzureichend testen kann) oder indirekt (wenn Tests durch den Parser
ausreichend und besser verwaltbar sind).
|