Für das Erstellen eines Testdesigns werden Informationen von vielen verschiedenen Arbeitsergebnissen verwendet,
darunter auch von Designarbeitsergebnissen, z. B. von der Realisierung von Anwendungsfällen, Designmodellen oder
Klassifizierungsschnittstellen. Die Tests werden ausgeführt, nachdem die Komponenten erstellt wurden. Es ist üblich,
das Testdesign erst unmittelbar vor Ausführung der Tests zu erstellen, sobald die Arbeitsergebnisse aus dem
Softwaredesign vorliegen. Ein Beispiel hierfür sehen Sie in Abbildung 1. Hier wird mit dem Testdesign, das sich auf die
Ergebnisse des Komponentendesigns stützt, erst gegen Ende der Implementierung begonnen. Der Pfeil von der
Implementierung zur Testausführung zeigt an, dass die Tests erst nach Abschluss der Implementierung ausgeführt werden
können.
Abbildung 1: Herkömmliches Testdesign zu einem späten Zeitpunkt im Lebenszyklus
Es geht aber auch anders. Der Test kann zwar erst ausgeführt werden, wenn die Komponente implementiert ist, aber das
Testdesign kann schon früher erstellt werden, z. B. sobald das Designarbeitsergebnis vorliegt. Die folgende Abbildung
zeigt, dass das Testdesign sogar parallel zum Komponentendesign entwickelt werden kann:
Abbildung 2: Test-First-Design in Chronologie zum Softwaredesign
Diese zeitliche Vorverlegung des Testaufwands wird als Test-First-Design bezeichnet. Welche Vorteile bringt diese
Herangehensweise?
-
Fehler sind auch bei sorgfältigster Arbeit am Softwaredesign nicht auszuschließen. Sie könnten einen relevanten
Fakt übersehen. Möglicherweise hindern Sie Ihre eingefahrenen Denkgewohnheiten daran, bestimmte Alternativen zu
sehen. Vielleicht entgeht Ihnen auch etwas, weil Sie gerade müde sind. Die Überprüfung Ihrer
Designarbeitsergebnisse durch andere kann da Abhilfe schaffen. Vielleicht kennen diese Prüfer die Fakten, die Ihnen
fehlen. Vielleicht können sie erkennen, was Sie übersehen haben. Am besten ist es, wenn diese Personen eine andere
Perspektive als Sie haben und das Design mit anderen Augen betrachten können. Dann wird ihnen auffallen, was Ihnen
entgangen ist.
Die Erfahrung zeigt, dass die Testperspektive sehr effektiv ist, da sie ausgesprochen konkret ist. Beim
Softwaredesign ist es einfach, sich vorzustellen, dass in einem bestimmten Feld beispielsweise der Titel des
aktuellen Kunden angezeigt werden soll, und dann zum nächsten Schritt überzugehen, ohne sich weitere Gedanken über
dieses Feld zu machen. Beim Testdesign müssen Sie dagegen entscheiden, was genau in diesem Feld angezeigt
wird. Nehmen wir an, ein Kunde hat früher in der Marine gedient und den Dienstgrad eines Leutnants erworben. Soll
in diesem Fall 'Leutnant aD Michael Ehlert' oder 'Herr Michael Ehlert' angezeigt werden?
Wenn das Testdesign wie in Abbildung 1 aufgeschoben wird, bis die Testausführung ansteht, wird das wahrscheinlich
unnötig Geld kosten. Ein Fehler in Ihrem Softwaredesign könnte bis zum Testdesign unerkannt bleiben. Vielleicht
erwähnt einer der Tester, dass er diesen Kunden aus seiner Zeit bei der Marine kennt, macht daraufhin einen
'Ehlert'-Test und stellt dann fest, dass es ein Problem gibt. In diesem Fall müsste eine teilweise oder ganz fertig
gestellte Implementierung umgeschrieben und das Designarbeitsergebnis aktualisiert werden. Es wäre kostengünstiger,
das Problem vor Beginn der Implementierung zu erkennen.
-
Einige Fehler könnten vor Erstellung des Testdesigns vom Implementierer festgestellt werden, was allerdings auch
nicht optimal ist. Die Implementierung des Designs kommt ins Stocken, weil sich die Frage, wie das Design
eigentlich aussehen sollte, in den Vordergrund drängt. Selbst wenn eine Person die Rolle des Implementierers und
des Designers inne hat, ergibt sich eine Unterbrechung des Prozesses, die bei verschiedenen Personen natürlich
deutlich größer ist. Das Test-First-Design kann derartige Unterbrechungen vermeiden helfen und so zur Steigerung
der Effizienz führen.
-
Das Testdesign unterstützt Implementierer aber auch dadurch, dass es ein Design transparenter gestaltet. Falls der
Implementierer sich fragt, warum ein bestimmtes Design genau so aussehen muss, kann das Testdesign ein konkretes
Beispiel für das gewünschte Verhalten liefern. So können Fehler durch Missverständnisse beim Implementierer
vermieden werden.
-
Die Fehlerzahl wird selbst dann reduziert, wenn der Implementierer die Frage gar nicht stellt, sondern sie
nur hätte stellen sollen. Stellen Sie sich vor, es gibt eine Mehrdeutigkeit, die der Designer unbewusst auf die
eine Art interpretiert hat, die vom Implementierer aber auf eine andere Art ausgelegt wird. Der Implementierer
richtet sich nach dem Design, aber auch nach bestimmten Anweisungen bezüglich des erwarteten Verhaltens der
Komponente. Wenn konkrete Testfälle vorliegen, erhöht sich die Wahrscheinlichkeit, dass das Verhalten der
Komponente tatsächlich den Anforderungen entspricht.
Die folgenden Beispiele sollen Ihnen helfen, sich ein Bild vom Test-First-Design zu machen.
Nehmen wir an, Sie sollen ein System für die Zuteilung von Besprechungsräumen erstellen, damit nicht jedesmal die
Sekretärin gefragt werden muss. Eine der Methoden der Klasse MeetingDatabase ist getMeeting und hat
folgende Signatur:
Meeting getMeeting(Person, Zeit);
Wenn eine Person und eine Zeit eingegeben wird, gibt getMeeting die Besprechung zurück, an der die Person zur
angegebenen Zeit laut Plan teilnehmen soll. Falls der Plan keine Besprechungstermine für die Person enthält, gibt sie
das spezielle Meeting-Objekt unscheduled zurück. Es gibt ein paar Testfälle, die sofort auf der Hand
liegen:
-
Wird unscheduled zurückgegeben, wenn die Person zur angegebenen Zeit keinen Besprechungstermin hat?
-
Wird die richtige Besprechung zurückgegeben, wenn die Person zur angegebenen Zeit an einer Besprechung teilnehmen
soll?
Diese Testfälle sind nichts Besonderes, müssen aber dennoch irgendwann ausprobiert werden. Es spricht nichts dagegen,
sie gleich zu erstellen, indem wir den Testcode schreiben, der dann später ausgeführt wird. Der Java-Code für den
ersten Test könnte wie folgt aussehen:
// Falls keine Besprechung zur gegebenen Zeit ansteht,
// wird 'unscheduled' erwartet.
public void testWhenAvailable() { Person Freddy = new Person("Freddy"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(Freddy, now) == Meeting.unscheduled); }
Es gibt aber auch interessantere Testideen. Diese Methode sucht unter anderem nach einer Übereinstimmung, und bei
Suchmethoden sollte generell die Frage gestellt werden, was passieren soll, wenn mehrere Übereinstimmungen gefunden
werden. In diesem Fall müssten wir also fragen, ob eine Person gleichzeitig an zwei Besprechungen teilnehmen kann.
Zunächst scheint das unmöglich zu sein, aber die Sekretärin könnte auf diese Frage eine überraschende Antwort parat
haben. Es stellt sich heraus, dass einige Führungskräfte recht häufig zwei Besprechungstermine zur selben Zeit haben.
Sie schauen dann bei einer Besprechung vorbei, geben eine gewisse Zeit "Rückendeckung" oder Anweisungen und gehen dann
zur nächsten Besprechung. Ein System, das dieses Verhalten nicht abbilden kann, würde zumindest teilweise nicht genutzt
werden.
Dieses Beispiel für Test-First-Design zeigt, wie ein Analysefehler auf der Implementierungsebene erkannt wird. Dazu
gibt es noch einiges zu bemerken:
-
Sie könnten drauf hoffen, dass diese Anforderung bei einer guten Definition und Analyse der Anwendungsfälle
entdeckt, das Problem rechtzeitig vermieden und getMeeting anders entworfen worden wäre (so dass die Methode
auch keine Besprechung oder eine Reihe von Besprechungen zurückgeben kann). Bei der Analyse werden aber immer
einige Probleme übersehen, die besser schon während der Implementierung bemerkt werden und nicht erst nach dem
Deployment.
-
In vielen Fällen haben weder Designer noch Implementierer das Fachwissen, um solche Probleme zu erkennen. In diesem
speziellen Fall werden sie weder die Gelegenheit noch die Zeit haben, bei der Sekretärin nachzufragen. In diesem
Fall würde sich die Person, die Tests für getMeeting entwickelt, fragen, ob es vorkommen kann, dass zwei
Besprechungen zurückgegeben werden müssen. Sie würde eine Weile darüber nachdenken und dann zu dem Schluss kommen,
dass es einen solchen Fall nicht gibt. Mit Test-First-Design lässt sich demnach nicht jedes Problem feststellen,
aber durch das Stellen der richtigen Fragen erhöht sich die Chance, ein Problem zu erkennen.
-
Einige Testverfahren für die Implementierung werden auch für die Analyse verwendet. Das Test-First-Design kann auch
von Analytikern erstellt werden, aber das ist nicht Gegenstand dieses Artikels.
Das zweite Beispiel ist das Modell eines Zustandsdiagramms für ein Heizungssystem.
Abbildung 3: HVAC-Zustandsdiagramm
Alle Pfeile im Zustandsdiagramm könnten mit ein paar Tests überprüft werden. Ein Test könnte mit einem System im
Leerlauf beginnen, ein Ereignis 'Zu heiß' auslösen, im Zustand 'Kühlung/Aktiv' eine Störung vorsehen, ein weiteres
Ereignis 'Zu heiß' auslösen und das System dann wieder in den Leerlauf versetzen. Damit sind jedoch nicht alle Pfeile
abgearbeitet, so dass weitere Tests erforderlich sind. Bei dieser Art von Tests wird auf verschiedene
Implementierungsprobleme geachtet. Durch das Nachvollziehen jedes Pfeils wird beispielsweise überprüft, ob bei der
Implementierung einer vergessen wurde. Durch die Verwendung von Ereignisabläufen mit Fehlerpfaden, auf die Pfade mit
korrektem Verhalten folgen, wird geprüft, ob der Fehlerbehandlungscode Teilergebnisse löschen kann, damit diese keinen
Einfluss auf spätere Berechnungen haben. (Weitere Informationen zum Testen von Zustandsdiagrammen finden Sie im
Abschnitt Richtlinie für Arbeitsergebnisse: Testideen für Zustandsdiagramme und
Ablaufdiagramme.)
Das letzte Beispiel beschäftigt sich mit einem Ausschnitt aus einem Designmodell. Es gibt eine Assoziation zwischen
Gläubiger und Rechnung, die vorsieht, dass jeder Gläubiger mehrere offene Rechnungen haben kann.
Abbildung 4: Assoziation zwischen den Klassen 'Gläubiger' und 'Rechnung'
Auf diesem Modell basierende Tests würden das System jeweils mit einem Gläubiger prüfen, der keine offenen Rechnungen,
eine offene Rechnung und eine große Anzahl offener Rechnungen hat. Ein Tester würde auch fragen, ob es Situationen
gibt, in denen eine Rechnung mehreren Gläubigern oder keinem Gläubiger zugeordnet werden kann. (Das derzeitige, mit
Papier arbeitende System, das durch das Computersystem ersetzt werden soll, führt die Buchhaltung vielleicht ohne
Kreditoren, um einen besseren Überblick über die anstehenden Arbeiten zu haben.) Falls dem so ist, sollte auch dieses
Problem durch die Analyse erkannt werden.
Das Test-First-Design wird üblicherweise vom Designer des Arbeitsergebnisses erstellt, kann aber auch von einer anderen
Person erstellt werden. Übernimmt der Designer selbst diese Aufgabe, entsteht weniger Kommunikationsaufwand, denn die
notwendigen Erklärungen gegenüber einem Testdesigner entfallen. Ein gesonderter Testdesigner müsste zudem einige Zeit
investieren, um sich gründlich mit dem Design des Arbeitsergebnisses vertraut zu machen. Viele Fragen ergeben sich ganz
natürlich sowohl während der Gestaltung des Softwareprodukts als auch beim Entwerfen der Tests, so dass es ausreicht,
wenn eine Person diese einmal stellt und die Antworten in Form von Tests aufschreibt. Eine solche Frage wäre z. B., was
geschehen würde, wenn der Kompressor im Zustand X ausfällt.
Wenn der Designer des Arbeitsergebnisses gleichzeitig der Testdesigner ist, hat das aber auch Nachteile, denn er ist
bis zu einem gewissen Grad "betriebsblind" und kann eigene Fehler leichter übersehen. Bei der Erstellung des
Testdesigns werden einige dieser Fehler offenbar werden, aber wahrscheinlich nicht in dem Maße, wie es bei einem
Außenstehenden der Fall gewesen wäre. Inwieweit dies ein Problem darstellt, hängt vom jeweiligen Designer und dessen
Erfahrungen ab.
Wenn eine Person für das Software- und das Testdesign zuständig ist, können beide Designs nicht parallel erstellt
werden. Das ist ein weiterer Nachteil. Werden die Rollen zwei verschiedenen Personen zugeteilt, steigt zwar der
Gesamtaufwand, aber das Design wird wahrscheinlich in kürzerer Zeit fertig gestellt. Jemand, der es kaum erwarten kann,
das Design endlich abzuschließen und mit der Implementierung zu beginnen, wird den zusätzlichen Zeitaufwand für das
Testdesign sicher frustrierend finden. Er könnte versucht sein, sich auf das Nötigste zu beschränken, um schneller
voranzukommen.
Nein. Nein, denn während der Erstellung des Komponentendesigns werden nicht alle Entscheidungen getroffen.
Entscheidungen der Implementierungsphase können schlecht mit Tests überprüft werden, die auf dem Design basieren. Das
klassische Beispiel hierfür ist eine Routine zum Sortieren von Arrays. Es gibt viele verschiedene
Sortierungsalgorithmen mit unterschiedlichen Vor- und Nachteilen. Quicksort ist für große Arrays in der Regel schneller
als Insertion-Sort, für kleine Arrays jedoch oft langsamer. Es könnte somit ein Sortierungsalgorithmus implementiert
werden, der für Arrays mit mehr als 15 Elementen Quicksort und ansonsten Insertion-Sort verwendet. Diese Arbeitsteilung
muss aus den Designarbeitsergebnissen nicht ersichtlich sein. Sie könnten sie zwar in einem
Designarbeitsergebnis darstellen, aber der Designer wird es vielleicht nicht lohnend finden, so explizite
Entscheidungen zu treffen. Da die Größe des Arrays beim Design keine Rolle spielt, könnten im Testdesign unabsichtlich
nur kleine Arrays verwendet werden. Dies hätte zur Folge, dass der Quicksort-Code überhaupt nicht getestet werden
würde.
Schauen wir uns als weiteres Beispiel den folgenden Ausschnitt aus einem Ablaufdiagramm an. Es zeigt einen
SecurityManager, der die Methode log() von StableStore aufruft. Die Methode log() gibt in
diesem Fall einen Fehler zurück, der den SecurityManager veranlasst, Connection.close() aufzurufen.
Abbildung 5: Instanz des SecurityManager-Ablaufdiagramms
Dieses Beispiel kann dem Implementierer als Mahnung dienen. Sobald log() scheitert, muss die Verbindung
geschlossen werden. Im Test muss die Frage beantwortet werden, ob der Implementierer dafür gesorgt hat, dass dies auch
tatsächlich und in jedem Fall geschieht. Zur Beantwortung der Frage muss der Testdesigner alle an
StableStore.log() gerichteten Aufrufe finden und sicherstellen, dass es an jedem dieser Aufrufpunkte einen zu
behandelnden Fehler gibt.
Wenn Sie sich den ganzen Code angesehen haben, der StableStore.log() aufruft, werden Sie die Ausführung eines
solchen Tests vielleicht sonderbar finden und sich fragen, ob eine Überprüfung des Codes auf korrekte Fehlerbehandlung
nicht ausreicht.
Vielleicht ist eine solche Codeüberprüfung genug. Fehlerbehandlungscode ist allerdings immer fehleranfällig, weil er
oft implizit von Voraussetzungen abhängt, die durch das Auftreten des Fehlers nicht erfüllt sind. Das klassische
Beispiel hierfür ist der folgende Code zur Behandlung von Zuordnungsfehlern:
while (true) { // Ausgangsebene der Ereignisschleife
try { XEvent xe = getEvent(); ... // Hauptteil des Programms
} catch (OutOfMemoryError e) { emergencyRestart();
} }
Dieser Code versucht nach Fehlern wegen Speichermangels eine Wiederherstellung. Dazu führt er eine Bereinigung durch,
um Speicher verfügbar zu machen, und setzt dann die Ereignisverarbeitung fort. Gehen wir davon aus, dass dies ein
annehmbares Design ist. Die Methode emergencyRestart ist sehr darauf bedacht, keinen Speicher zu reservieren.
Das Problem besteht allerdings darin, dass emergencyRestart eine Dienstprogrammroutine aufruft, die eine andere
Dienstprogrammroutine aufruft, von der wiederum eine Dienstprogrammroutine aufgerufen wird, die letztlich ein neues
Objekt zuordnet. Dies ist aufgrund des Speichermangels aber gar nicht möglich, so dass das ganze Programm scheitert.
Diese Art von Fehlern ist durch eine reine Codeprüfung kaum feststellbar.
Bisher sind wir implizit davon ausgegangen, dass der größte Teil des Testdesigns so früh wie möglich fertig gestellt
werden sollte. Das bedeutet, dass alle nur denkbaren Tests aus dem Designarbeitsergebnis abgeleitet werden und später
nur noch Tests zu ergänzen sind, die auf Interna der Implementierung basieren. In der Ausarbeitungsphase könnte dies
jedoch unangemessen sein, denn so umfassende Tests lassen sich vielleicht nicht mit der Zielsetzung einer Iteration
vereinbaren.
Nehmen wir an, es wird ein Architekturprototyp erstellt, um den Investoren die Machbarkeit des Produkts zu
demonstrieren. Dieser Prototyp könnte auf einigen wichtigen Anwendungsfallinstanzen basieren. Es sollte getestet
werden, ob der Code diese Instanzen unterstützt. Welche Nachteile könnte es haben, darüber hinausgehende Tests zu
erstellen? Es könnte beispielsweise auf der Hand liegen, dass beim Prototyp wichtige Fehlerfälle außer Acht gelassen
wurden. Warum sollte diese Fehlerbehandlung nicht in Form entsprechender Testfälle dokumentiert werden?
Und was ist, wenn der Prototyp seinen Zweck erfüllt und offenlegt, dass der Architekturansatz der falsche ist? Dann ist
die Architektur mitsamt den ganzen Tests für die Fehlerbehandlung hinfällig. In einem solchen Fall hätte der Aufwand
für das Testdesign keinen Nutzen gebracht. Es wäre wohl besser gewesen, noch zu warten und nur die Tests zu entwerfen,
mit denen überprüft werden kann, ob dieser Prototyp für den Konzeptnachweis die Erfolgschancen des Konzepts bestätigt.
Diese Frage scheint von untergeordnetem Interesse zu sein, ist jedoch in ihrer psychologischen Wirkung nicht zu
unterschätzen. In der Ausarbeitungsphase werden wichtige Risiken beurteilt, auf die sich das gesamte Projektteam
konzentrieren sollte. Wenn die Teammitglieder sich mit nebensächlichen Fragen beschäftigen müssen, zieht dies
Aufmerksamkeit von den wichtigen Teamzielen ab.
Wie also kann das Test-First-Design erfolgreich in der Ausarbeitungsphase genutzt werden? Es kann eine wichtige Rolle
bei der angemessenen Untersuchung architektonischer Risiken spielen. Wenn überlegt wird, wie das Team genau wissen
kann, ob ein Risiko erkannt oder vermieden wurde, besteht mehr Klarheit für den Entwicklungsprozess und die
Möglichkeit, von vornherein eine bessere Architektur zu entwerfen.
In der Konstruktionsphase werden Designarbeitsergebnisse in ihre endgültige Form gebracht. Alle erforderlichen
Anwendungsfälle werden umgesetzt und die Schnittstellen für alle Klassen werden implementiert. Die Zielsetzung dieser
Phase ist Vollständigkeit, so dass das Test-First-Design ebenfalls fertig gestellt sein sollte. Spätere Ereignisse
könnten zeigen, dass Tests ungeeignet sind. Dies sollte jedoch, wenn überhaupt, nur einige wenige Tests betreffen.
In der Konzeptions- und der Übergangsphase geht es in der Regel weniger um Designaktivitäten, die getestet werden
müssten. Falls es solche Aktivitäten gibt, ist das Test-First-Design anzuwenden. Es könnte in der Konzeptionsphase
beispielsweise zur Überprüfung der Erfolgschancen der Kandidatenkonzepte genutzt werden. Wie in der Konstruktions- und
Ausarbeitungsphase sollten die Tests mit den Zielsetzungen der jeweiligen Iteration in Einklang gebracht werden.
|