Beim Entwerfen von Tests werden viele Informationsquellen genutzt, z. B. Designmodelle, Klassifizierungsmerkmale,
Schnittstellen, Zustandsdiagramme und der eigentliche Code. Diese Informationen aus den Quellendokumenten müssen zu
gegebener Zeit in ausführbare Tests umgesetzt werden:
-
spezifische Eingaben für die zu testende Software
-
eine bestimmte Hardware- und Softwarekonfiguration
-
Initialisierung in einem bekannten Status
-
spezifische erwartete Ergebnisse
Es ist möglich, die Informationen aus einem Quellendokument direkt in ausführbare Tests umzusetzen. Oft ist es jedoch
sinnvoll, einen Zwischenschritt einzuschieben, der das Schreiben von Testideen in eine Liste mit Testideen vorsieht. Anhand dieser Liste
werden dann die ausführbaren Tests erstellt.
Eine Testidee wird manchmal auch als zu testende Anforderung bezeichnet und ist eine kurze Angabe zu einem Test, der
ausgeführt werden könnte. Nehmen wird als einfaches Beispiel eine Funktion, die eine Quadratwurzel berechnet, und für
die wir einige Testideen entwickeln wollen:
-
Als Eingabe soll eine Zahl verwendet werden, die geringfügig kleiner als null ist.
-
Als Eingabe soll null verwendet werden.
-
Es soll eine perfekte Quadratzahl wie 4 oder 16 getestet werden. (Ist das Ergebnis genau 2 oder 4?)
Jede dieser Ideen ließe sich sofort in einen ausführbaren Test mit genauen Beschreibungen der Eingaben und der
erwarteten Ergebnisse umsetzen.
Diese weniger spezifische Zwischenform hat zwei Vorteile:
-
Testideen lassen sich besser prüfen und sind leichter verständlich als komplette Tests. Die Gedankengänge, die zu
einem bestimmten Test geführt haben, lassen sich besser nachvollziehen.
-
Testideen unterstützen kombinierte und komplexere Tests. Dies ist weiter unten unter der Überschrift Testdesign mit Liste genauer beschrieben.
Alle unsere Beispiele zur Quadratwurzel beschreiben Eingaben. Testideen können jedoch alle Elemente eines ausführbaren
Tests umfassen. Die Angaben "Auf einem Laserjet IIIP drucken" oder "Mit gefüllter Datenbank testen" beschreiben
beispielsweise einen Aspekt der Testumgebung für einen Test. Sie sind jedoch ziemlich unvollständig, denn es wird
nicht gesagt, was auf dem Drucker gedruckt oder was mit der gefüllten Datenbank getestet werden soll.
Dennoch stellen diese Angaben sicher, dass wichtige Ideen, die später detaillierter im Testdesign beschrieben werden
können, nicht vergessen werden.
Testideen basieren oft auf Fehlermodellen, dem Verständnis, welche Fehler in einer Software plausibel
sind, und dem Wissen, wie solche Fehler mit Tests aufgedeckt werden können. Nehmen wir Grenzwerte als Beispiel. Es darf
sicher angenommen werden, dass die Quadratwurzelfunktion wie folgt implementiert werden könnte:
double sqrt(double x) {
if (x < 0)
// Fehler signalisieren
...
Dass für < fälschlicherweise <= angegeben werden kann, erscheint ebenfalls plausibel. Diese Art
Fehler kommt häufig vor und sollte daher auf jeden Fall überprüft werden. Wenn X den Wert 2 hat, kann der
Fehler nicht gefunden werden, denn sowohl der falsche Ausdruck (x<=0) als auch der richtige (x<0)
führen zu derselben Verzweigung der Anweisung if. Wenn wir für X den Wert -5 verwenden, kann der
Fehler ebenfalls nicht gefunden werden. Die einzige Möglichkeit, den Fehler aufzudecken, ist die Verwendung des Wertes
0 für X, womit die zweite Testidee bestätigt wird.
Das Fehlermodell ist hier explizit, es kann in anderen Fällen aber auch implizit sein. Wenn ein Programm beispielsweise
einen verlinkte Struktur manipuliert, sollte getestet werden, ob die neue Struktur kreisförmig ist. Es gibt viele
Fehler, die unbeabsichtigt zu einer kreisförmigen Struktur führen und nicht einzeln aufgezählt werden müssen. Es reicht
aus, dass ein Fehler wahrscheinlich genug ist, um einen Test zu rechtfertigen.
Wenn Sie die folgenden Links anklicken, erfahren Sie, wie Testideen aus verschiedenen Arten von Fehlermodellen
entwickelt werden können. Die beiden ersten Abschnitte beschäftigen sich mit expliziten Fehlermodellen und der letzte
Abschnitt mit impliziten.
Diese Fehlermodelle lassen sich auf viele verschiedene Arbeitsergebnisse anwenden. Der erste Abschnitt beschreibt
beispielsweise die Möglichkeiten, die Sie bei Booleschen Ausdrücken haben. Solche Ausdrücke sind in Code,
Wächterbedingungen, Zustands- und Folgediagrammen enthalten, aber auch in normalen Beschreibungen des Verhaltens von
Methoden (wie Sie sie für eine publizierte API finden können).
Gelegentlich ist es hilfreich, Richtlinien für bestimmte Arbeitsergebnisse zu haben. Lesen Sie hierzu den Abschnitt Richtlinie: Testideen für Zustandsdiagramme und Ablaufdiagramme.
Eine Liste mit Testideen kann Ideen aus vielen Fehlermodellen enthalten, die wiederum aus mehreren Arbeitsergebnissen
abgeleitet sein können.
Nehmen wir an, Sie entwerfen Tests für eine Methode, die in einer sequenziellen Datensammlung nach einer Zeichenfolge
sucht. Die Methode kann bei der Suche die Groß-/Kleinschreibung beachten oder ignorieren und gibt den Index der ersten
gefundenen Übereinstimmung zurück. Wird keine Übereinstimmung gefunden, gibt die Methode -1 zurück.
int Collection.find(String string,
Boolean ignoreCase);
Hier sehen Sie einige Testideen für diese Methode:
-
Die Übereinstimmung wird an der ersten Position gefunden.
-
Die Übereinstimmung wird an der letzten Position gefunden.
-
Es wird keine Übereinstimmung gefunden.
-
Es wurden zwei oder mehr Übereinstimmungen in der Datensammlung gefunden.
-
Die Groß-/Kleinschreibung wird ignoriert. Es wird eine Übereinstimmung gefunden, die jedoch bei Berücksichtigung
der Groß-/Kleinschreibung keine solche wäre.
-
Die Groß-/Kleinschreibung wird beachtet. Es wird eine exakte Übereinstimmung gefunden.
-
Die Groß-/Kleinschreibung wird beachtet. Eine Zeichenfolge, die bei ignorierter Groß-/Kleinschreibung eine
Übereinstimmung gewesen wäre, wird übersprungen.
Es wäre einfach, für jede dieser Ideen einen Test zu implementieren, so dass Sie insgesamt sieben Tests ausführen
müssten. Sie können jedoch verschiedene Testideen in einem Test zusammenfassen. Mit dem folgenden Test würden
beispielsweise die Testideen 2, 6 und 7 umgesetzt werden:
Konfiguration: Datensammlung initialisiert mit ["dawn", "Dawn"]
Aufruf: collection.find("Dawn", false)
Erwartetes Ergebnis: Rückgabewert 1 (Der Wert läge bei 0, wenn "dawn" nicht übersprungen werden würde.)
Je weniger spezifisch Testideen sind, desto leichter lassen sie sich kombinieren.
Alle hier genannten Testideen ließen sich in drei Tests unterbringen. Und warum sind drei Tests für sieben Testideen
besser als sieben Einzeltests?
-
Wenn Sie eine Vielzahl einfacher Tests entwerfen, wird für die Erstellung von Test N+1 in der Regel der Test N
kopiert und gerade so weit modifiziert, dass mit ihm die neue Testidee realisiert werden kann. Insbesondere bei
etwas komplexerer Software wird der Test N+1 das Programm wahrscheinlich fast genauso ausführen wie der Test N und
fast genau demselben Pfad durch den Code folgen.
Eine kleinere Anzahl von Tests, die jeweils mehrere Testideen enthalten, lässt ein solches Kopieren und
Modifizieren nicht zu. Jeder Test läuft somit etwas anders als der vorherige ab, so dass der Code auf verschiedene
Weise und entlang verschiedener Pfade ausgeführt wird.
Und welchen Vorteil hat das? Wenn die Liste mit Testideen vollständig wäre und für jeden Programmfehler eine
Testidee enthalten würde, machte es keinen Unterschied, wie Sie die Tests schreiben. In der Liste fehlen jedoch
immer einige Testideen, die Programmfehler aufdecken könnten. Wenn scheinbar nicht notwendige Varianten hinzugefügt
werden, damit sich die Schritte eines Tests deutlich von denen des vorherigen Tests unterscheiden, erhöht sich die
Chance, dass Sie mit einem der Tests durch Zufall auf einen Programmfehler stoßen. Bei wenigen komplexen Tests
besteht tatsächlich eine größere Wahrscheinlichkeit, dass Sie einer Testidee nachgehen, die Sie sonst gar nicht in
Erwägung gezogen hätten.
-
Manchmal werden Ihnen beim Erstellen komplexerer Tests auch neue Ideen einfallen. Bei einfachen Tests geschieht
dies weit seltener, weil so viele Schritte genau mit denen des vorherigen Tests übereinstimmen und damit
routinemäßiges Denken und Handeln begünstigen.
Es gibt aber auch Gründe, die gegen komplexe Tests sprechen.
-
Wenn mit jedem Test genau eine Testidee umgesetzt wird und der Test für die Idee 2 scheitert, wissen Sie sofort,
woran dies höchstwahrscheinlich liegen wird. Das Programm ist nicht in der Lage, eine Übereinstimmung an der
letzten Position zu finden. Bei einem Test, der die Ideen 2, 6 und 7 umsetzt, lässt sich der Fehler nicht so leicht
eingrenzen.
-
Komplexe Tests sind schwerer zu verstehen und zu verwalten, weil die Absicht des Tests nicht so offensichtlich
ist.
-
Es ist auch schwieriger, komplexe Tests zu entwerfen. Einen Test zu konstruieren, mit dem fünf Testideen umgesetzt
werden sollen, kostet oft mehr Zeit als das Konstruieren von fünf Tests für jeweils eine Idee. Darüber hinaus
schleichen sich auch schneller Fehler ein. Sie könnten denken, dass Sie allen fünf Testideen Rechnung tragen,
obwohl Sie eigentlich nur vier umsetzen.
In der Praxis müssen Sie Komplexität und Einfachheit vernünftig gegeneinander abwägen. Die ersten Tests, denen Sie die
Software unterziehen (in der Regel die Smoke Tests), sollten einfach, leicht verständlich und leicht zu verwalten
sein und das Ziel haben, die offensichtlichsten Probleme festzustellen. Spätere Tests sollten komplexer sein, jedoch
nicht so komplex, dass sie nicht mehr verwaltet werden können.
Sobald Sie eine Gruppe von Tests fertig gestellt haben, sollten Sie sie auf die im Abschnitt Konzept: Entwicklertests erläuterten typischen Testdesignfehler überprüfen.
Eine Liste mit Testideen kann bei der Überprüfung von Designarbeitsergebnissen hilfreich sein. Schauen Sie sich
beispielsweise diesen Teil eines Designmodells mit der Beziehung zwischen den Klassen 'Abteilung' und 'Mitarbeiter'
an.
Abbildung 1: Beziehung zwischen den Klassen 'Abteilung' und 'Mitarbeiter'
Nach den Regeln für die Erstellung von Testideen aus einem solchen Modell müssten Sie den Fall berücksichtigen, in dem
eine Abteilung viele Mitarbeiter hat. Wenn Sie ein Design durchgehen und sich fragen, wie es an dieser Stelle wohl
aussehen würde, wenn die Abteilung viele Mitarbeiter hat, könnten Sie Design- oder Analysefehler entdecken. Sie könnten
beispielsweise erkennen, dass im Design immer nur jeweils ein Mitarbeiter von einer Abteilung in eine andere versetzt
werden kann. Dies könnte ein Problem sein, wenn das Unternehmen häufiger umfassende Restrukturierungsmaßnahmen
durchführt, bei denen viele Mitarbeiter die Abteilung wechseln.
Fehler, bei denen eine Möglichkeit übersehen wird, werden als Versäumnisfehler bezeichnet. Solche Fehler kommen
aber nicht nur im Design vor. Sie können auch Tests übersehen haben, mit denen sich diese Fehler feststellen ließen.
Schauen Sie sich beispielsweise [GLA81], [OST84], [BAS87], [MAR00] und andere Studien an, die belegen, wie häufig Versäumnisfehler erst beim
Deployment festgestellt werden.
Die Rolle von Tests im Bereich der Designaktivitäten ist im Abschnitt Konzept:
Test-First-Design näher erläutert.
Bei der Rückverfolgbarkeit muss immer ein geeigneter Kompromiss gefunden
werden. Rechtfertigt der Nutzen der Rückverfolgbarkeit die damit verbundenen Kosten? Diese Frage muss beim Definieren
der Anforderungen an die Auswertung und Rückverfolgbarkeit berücksichtigt werden (siehe Aufgabe: Anforderungen für Bewertung und Rückverfolgbarkeit
definieren).
Wenn die Rückverfolgbarkeit lohnt, werden in der Regel Tests auf die Arbeitsergebnisse zurückgeführt, aus denen sie
hergeleitet wurden. Es könnte beispielsweise eine Rückverfolgbarkeit zwischen einer API und den Tests für diese API
geben. Wenn sich die API ändert, wissen Sie, welche Tests geändert werden müssen. Ändert sich der Code (der die API
implementiert), wissen Sie, welche Tests ausgeführt werden müssen. Falls Ihnen ein Test Kopfzerbrechen bereitet, können
Sie herausfinden, welche API mit diesem Test überprüft werden soll.
Die Liste mit Testideen eröffnet zusätzliche Möglichkeiten für die Rückverfolgbarkeit. Sie können einen Test auf die
darin umgesetzten Testideen zurückführen und diese Testideen auf das ursprüngliche Arbeitsergebnis.
|