In Kapitel 13 haben wir die Aufgaben und Schwerpunkte der Software-Qualitätssicherung beschrieben und im Detail vorgestellt, wie Reviews organisiert und durchgeführt werden. In diesem Kapitel widmen wir uns dem Programmtest (oder einfach Test), der gemäß der in Abschnitt 13.1.2 eingeführten Terminologie eine dynamische, mechanisch durchgeführte Maßnahme der analytischen Qualitätssicherung ist.
Seit Beginn der Programmierung werden Programme getestet. Das Gebiet entstand also in der Praxis; seit den Siebzigerjahren wurde es aber auch systematisch erforscht.
Wir konzentrieren uns hier auf allgemein wichtige Aspekte des Tests. Eine ausführliche Betrachtung des Programmtests ist beispielsweise in Spillner und Linz (2005) oder in Liggesmeyer (2002) zu finden. Das vergriffene Buch von Riedemann (1997) ist elektronisch verfügbar.
Die folgenden Zitate zeigen, dass mit dem Test ganz unterschiedliche Absichten verbunden sind:
»Testen ist die Ausführung eines Programms
mit dem Ziel, Fehler zu entdecken.«
Myers (1979)
»Testen ist die Vorführung eines Programms oder Systems
mit dem Ziel zu zeigen, dass es tut, was es tun sollte.«
Hetzel (1984)
Das Wort »Test« wurde aus dem Englischen importiert. Ursprünglich ist seine Bedeutung sehr allgemein, synonym mit »Prüfung«, und so wird es im Englischen auch noch oft verwendet. Wir legen diesem Kapitel eine sehr eingeschränkte Definition zu Grunde, etwa in der Bedeutung »praktisch ausprobieren«:
Testen ist die – auch mehrfache – Ausführung eines Programms auf einem Rechner mit dem Ziel, Fehler zu finden.
Wir grenzen den Begriff damit in mehrere Richtungen ab; kein Test in diesem Sinne ist
irgendeine Inspektion eines Programms,
die Vorführung eines Programms,
die Analyse eines Programms durch Software-Werkzeuge, z. B. die Erhebung von Metriken,
die Untersuchung eines Programms mit Hilfe eines Debuggers.
Das zu testende Programm – der Prüfling – entsteht durch Übersetzung aus dem Quellprogramm und enthält direkt oder indirekt Programme wie Editor, Compiler, Linker, Laufzeitsystem und Betriebssystem, aber auch Testtreiber und kooperierende Programme (z. B. ein Datenbanksystem). Wir können nur das Verhalten des gesamten Systems beobachten, nicht den Einfluss der einzelnen Komponenten.
Der Prüfling wird in der Regel auf einem Rechner ausgeführt, der für die Ausführung des Prüflings die gleiche virtuelle Maschine zur Verfügung stellt wie der Rechner, auf dem er später auch laufen soll. Es kann aber auch sinnvoll oder unvermeidlich sein, dass die Zielumgebung nur nachgebildet wird. Das ist vor allem dann notwendig, wenn das Programm später auf einem primitiven Prozessor ohne die zum Testen notwendige Peripherie läuft, wie es in technischen Anwendungen meist der Fall ist.
Wenn jemand ein Programm startet und spontan ein paar Werte eintippt, um zu sehen, ob das Programm damit umgehen kann, testet er. Aber diese Art des Tests ist ineffizient, meist auch ineffektiv. Wir schränken den Begriff darum weiter ein:
Ein systematischer Test ist ein Test, bei dem
• die Randbedingungen definiert oder präzise erfasst sind,
• die Eingaben systematisch ausgewählt wurden,
• die Ergebnisse dokumentiert und nach Kriterien beurteilt werden, die vor dem Test festgelegt wurden.
Betrachten wir die genannten Punkte genauer:
Die Randbedingungen eines Tests sind sämtliche Gegebenheiten, die auf die Resultate Einfluss haben oder haben können. Das ist zunächst der Prüfling selbst: Welches Programm, übersetzt von welchem Übersetzer, wird auf welchem Betriebssystem getestet? Welche andere Software ist beteiligt? Wer hat wann getestet? Wie viel Speicher steht zur Verfügung? Welche Geräte sind angeschlossen, und in welchem Zustand sind sie?
Aus der Spezifikation geht hervor, welche Eingaben das Programm akzeptieren muss und welche Reaktionen des Programms gefordert sind. Neben den Eingabedaten im engeren Sinn (interaktive Eingaben über Tastatur oder Maus, Dateien und Datenbanken, auf die das Programm zugreift) gehören im Allgemeinen auch die Anfangsbelegung des Speichers und die variablen Zustände der beteiligten Geräte zu den Eingaben. Natürlich können manche Einflüsse im konkreten Fall ausgeschlossen werden; wenn beispielsweise das Laufzeitsystem den Speicher automatisch initialisiert, hat die Vorbelegung keine Bedeutung.
Die Ausführung eines Programms wird im Allgemeinen durch den zu Beginn gegebenen Speicherzustand, die Eingabedaten und alle Randbedingungen beeinflusst, die aus der Umgebung einwirken. Natürlich wird in vielen Fällen angestrebt, dass sich nur die Eingabedaten auswirken, aber gerade beim Test dürfen wir nicht unterstellen, dass das auch wirklich so ist. Schon eine nicht initialisierte Variable im Programm bedeutet eine Abhängigkeit von der zufälligen Vorbelegung des Speichers. Eventuell haben auch die Signale aus einem Netzwerk, an das der Rechner angeschlossen ist, Folgen für die Programmausführung. All das ist im weitesten Sinne Eingabe.
Die Resultate des Tests werden dokumentiert, also aufgezeichnet und abgelegt. Bei interaktiven Tests muss diese Aufzeichnung von Hand erstellt werden, wenn keine speziellen Werkzeuge dafür zur Verfügung stehen.
Vor dem Test wurde bereits ermittelt, welche Resultate laut der Spezifikation zu erwarten sind. Im einfachsten Fall erhält man auf diese Weise SollResultate, in der Praxis können meist nur Kriterien definiert werden, die notwendig, aber nicht hinreichend sind, um das Resultat als richtig zu beurteilen. Dieses Problem wird in Abschnitt 19.1.2 näher betrachtet. Die Ist-Resultate werden mit den Soll-Resultaten verglichen oder gegen die Kriterien geprüft.
Wenn beim Soll-Ist-Vergleich eine Abweichung festgestellt wird, liegt ein Fehler vor. Das bedeutet nicht sicher, dass der Prüfling fehlerhaft ist. Es kann auch sein, dass das Soll-Resultat oder der Vergleich fehlerhaft war; möglicherweise wurde auch das falsche Programm getestet. Das Resultat des Tests kann also falsch positiv sein (siehe Abschnitt 13.4.2). Das muss bei der Analyse des Fehlers geklärt werden.
Der Zweck des Tests ist, Fehler zu entdecken. Ein Testfall ist also gut, wenn er hohe Chancen hat, einen vorher noch nicht bekannten Fehler anzuzeigen. Tritt dieser Fall ein, so war der Test erfolgreich. Ist kein Fehler entdeckt worden, dann war der Test erfolglos. Der Test sollte die Einsatzsituation so simulieren, dass aus einem erfolglosen Test auf einen erfolgreichen Einsatz geschlossen werden kann.
Abbildung 13–3 auf Seite 276 zeigt den prinzipiellen Informationsfluss bei einer Prüfung. Abbildung 19–1 zeigt den Informationsfluss, der bei der Vorbereitung, Durchführung und Auswertung eines Tests entsteht: Auf Basis der Anforderungen, bei einem Glass-Box-Test (siehe Abschnitt 19.6) auch auf Basis der bereits gemessenen Überdeckungen und des Codes, werden die Testfälle entworfen. Jeder Testfall besteht aus den Voraussetzungen, die erfüllt sein müssen, der Testeingabe und dem Soll-Resultat. Wenige Soll-Resultate stehen explizit in der Spezifikation (beispielsweise, wenn eine bestimmte Anfangsmeldung spezifiziert ist), die übrigen müssen aus den Anforderungen durch eine simulierte Ausführung abgeleitet werden. Ein Testfall ist also mehr als nur eine Testeingabe!
Abb. 19–1 Informationsfluss beim Test (schematisch)
Zur »Bilderbuch-Vorstellung« von einem mustergültigen Test gehört das SollResultat. Wenn beispielsweise eine Prozedur zu einem Datum zwischen 1900 und 2100 die Anzahl der Tage im betreffenden Monat liefern soll, dann ist genau definiert, was richtig ist. Im Test kann für die Eingabe 10.9.2003 das Resultat 30, für den 13.2.1900 das Resultat 28 verlangt werden. In vielen Fällen ist das aber nicht so einfach möglich, weil
es mehrere oder viele richtige Ergebnisse gibt (Beispiele: der von einem Compiler erzeugte Objektcode, die von einem Reservierungssystem gewählte Zuordnung zwischen Buchungen und Plätzen im Flugzeug),
das Ergebnis nicht präzise spezifiziert ist, weil Anforderungen weich (Abschnitt 16.4.2) oder nur vage definiert sind (Beispiele: Positionierung eines Ausgabefensters auf dem Bildschirm, der exakte Text einer Fehlermeldung),
es kein mathematisch exakt richtiges Ergebnis gibt, das auf dem Rechner darstellbar wäre (Beispiele: alle numerischen Berechnungen, in denen Werte wie 1/3,
oder π vorkommen),
das richtige Ergebnis einfach nicht bekannt ist (Beispiele: Berechnung der Zahl π auf 10000 Stellen genau, wenn π noch nie zuvor so genau bestimmt wurde, Berechnung astronomischer Größen auf der Grundlage neuer Beobachtungen oder neuer Modelle).
In allen diesen Fällen können nur Eigenschaften und Merkmale eines richtigen Resultats angegeben werden, nicht das richtige Resultat. Für die genannten Beispiele kommen folgende Merkmale in Frage:
Wenn der Prüfling (der Compiler) ein Programm übersetzt und der generierte Code ausgeführt wird, muss er sich so verhalten, wie es die Definition der übersetzten Programmiersprache verlangt; das Buchungssystem muss so viele Buchungen akzeptieren und konfliktfrei durchführen, wie Plätze verfügbar sind.
Das Ausgabefenster muss in einem bestimmten Bereich des Bildschirms erscheinen; die Fehlermeldung muss verständlich und informativ sein.
Das Resultat der Rechnung (mit gewissen Eingabedaten) muss in einem bestimmten Intervall liegen, z. B. zwischen 917,1 und 917,3.
Die ersten n Stellen der neu berechneten Zahl π müssen mit den bislang als gesichert geltenden ersten n Stellen von π übereinstimmen; die Gesamtmasse des Universums muss größer sein als die Masse aller bekannten Galaxien.
Die Forderung nach einem einzigen konkreten Soll-Resultat ist also für die Praxis meist zu streng. Wir geben stattdessen Prädikate an, die das Resultat eingrenzen. Diese Prädikate haben für die Teilresultate r1, r2, ... , rn die Form einer Funktion f(r1, r2, ... , rn), die als Resultat wahr (in der Bedeutung »plausibel«) oder falsch (in der Bedeutung »fehlerhaft«) liefert. Wenn nachfolgend von Soll-Resultaten die Rede ist, so sind damit meist die beschriebenen Prädikate gemeint.
Natürlich bietet es sich an, die Prüfung der Resultate gegen die Soll-Resultate zu automatisieren. Beispielsweise könnte das Prädikat »das Resultat x sollte zwischen 917,1 und 917,3 liegen« im JUnit-Testrahmenwerk folgendermaßen realisiert werden:
assertTrue(917.1 <= x & x <= 917.3);
Eine spezielle, in der Software-Wartung besonders wichtige Technik, die SollResultate zu erzeugen, wird beim Regressionstest angewendet.
regression testing — Selective retesting of a system or component to verify that modifications have not caused unintended effects and that the system or component still complies with its specified requirements.
IEEE Std 610.12 (1990)
Wenn ein Programm im Zuge der Wartung verändert wurde, hoffen wir, dass der angestrebte Effekt (eine Korrektur, Anpassung oder Erweiterung) erzielt wurde, dass aber keine unbeabsichtigten Effekte, also Fehler, entstanden sind. Leider sind solche Fehler aber sehr wahrscheinlich (siehe Abschnitt 22.3.1). Darum ist es zweckmäßig, auch einen Test durchzuführen, der speziell darauf abzielt, Fehler durch Veränderungen zu entdecken. Das ist der Regressionstest.
Dem Regressionstest liegen folgende Feststellungen und Annahmen zu Grunde:
Die Änderung des Programms ist geringfügig, das Verhalten des Programms ist nach der Änderung überwiegend wie vorher.
Das Programm wurde vor der Änderung eingesetzt und hat dabei keine oder nur wenige falsche Resultate geliefert; es war also grundsätzlich in Ordnung, selbst wenn die Änderung eine Korrektur war.
Nach einer Korrektur liefert das Programm unter gleichen Bedingungen gleiche Resultate (mit Ausnahme der bislang falschen Resultate).
Nach einer Erweiterung liefert es gleiche Resultate, denn die neue Funktionalität kann bislang noch nicht verwendet worden sein. Die alte Funktionalität darf sich nicht geändert haben.
Nach einer Modifikation dürfen die neuen Resultate dort und nur dort von den alten abweichen, wo dies gewünscht war.
Betrachten wir dazu ein Beispiel: Ein Programm, das die Einwohner einer Gemeinde verwaltet, hat der ältesten Bürgerin der Stadt zum sechsten Geburtstag gratuliert. Anscheinend wurde das Alter (06) nur zweistellig gespeichert. Ansonsten gab es mit diesem Programm keine Probleme. Es soll nun korrigiert und auch erweitert werden, sodass die Verwaltung feststellen kann, ob ein Einwohner noch einen Zweitwohnsitz hat. Nach der Änderung muss das Programm mit den gleichen Eingaben die gleichen Ergebnisse liefern wie vorher, nur die alte Dame sollte keinen Brief mit dem Inhalt »Bald kommst du zur Schule ...« bekommen. Die oben genannten Annahmen treffen in diesem Fall also zu.
Um einen Regressionstest durchführen zu können, muss präzise dokumentiert worden sein, wie sich das Programm früher verhalten hat. Dazu sind alle Eingaben, im Beispiel auch der aktuelle Stand der elektronischen Einwohnerkartei, zusammen mit den Ausgaben zu archivieren. Das geänderte Programm wird mit den alten Daten gefüttert. Die (meist sehr umfangreichen) Resultate, darunter auch die veränderte Einwohnerkartei, können dann mit den archivierten Resultaten verglichen werden. Wenn alle Ergebnisse in elektronischer Form vorliegen, kann das vollautomatisch geschehen. Die im Vergleich erkannten Unterschiede werden gemeldet. Im Beispiel sollte der einzige Unterschied sein, dass der unsinnige Brief nicht erzeugt wird.
Außerdem muss natürlich auch in einem weiteren, separaten Schritt getestet werden, ob die beabsichtigten Änderungen erfolgreich waren. Dazu brauchen wir im Beispiel Testfälle für Leute mit und ohne Zweitwohnsitz.
Abb. 19–2 Vorgehen beim Regressionstest (ohne Korrekturen nach den Tests)
Abbildung 19–2 zeigt schematisch den Ablauf. Nachdem das Programm implementiert ist und eingesetzt wurde, ist die Situation so wie auf der linken Seite dargestellt; wichtig ist, dass alle Dokumente, neben Spezifikation und Code auch die Eingabedaten und Resultate, archiviert sind. Rechts sieht man, wie eine Änderung durchgeführt wird: Die Änderung der Spezifikation führt zu einer Änderung des Codes (gestrichelte Pfeile). Nun wird der neue Code mit den alten Daten ausgeführt, die Resultate (1*) werden mit den alten Resultaten (1) verglichen, die Abweichungen werden wie oben beschrieben interpretiert. Neue Eingabedaten (entsprechend der veränderten Spezifikation) ergänzen die alten, es entstehen die Eingabedaten2. Auch mit diesen wird der Code2 ausgeführt, die Resultate2 werden wieder archiviert. Damit ist der nächste Änderungszyklus (rechts grau angedeutet) vorbereitet, er wird genau wie der erste durchgeführt.
Der Vorteil des Regressionstests liegt darin, dass man nicht (nur) die ursprünglichen Testdaten verwendet, sondern die meist reichlich verfügbaren Daten aus dem realen Einsatz der Software. Man verwendet also die alten Resultate als Soll-Ergebnisse für das geänderte Programm.
Allerdings lässt sich der Regressionstest nicht in allen Fällen anwenden: Wenn beispielsweise ein optimierender Compiler minimal verändert wurde, kann er durchaus völlig anderen Code erzeugen als zuvor. In diesem Falle gilt nicht mehr die Voraussetzung, dass sich die Ausgabe des Programms nicht oder nur sehr wenig ändert.
Wer einen Löwen jagt, muss wissen, wie ein Löwe aussieht. Er sollte auch wissen, wo sich der Löwe gern aufhält, welche Spuren er hinterlässt, welche Geräusche er erzeugt. Bei der Jagd nach Fehlern im Programm ist das nicht anders.
Leider wissen wir aber über die Fehler fast nichts (vgl. Abschnitt 13.3.2), nur dies: Es gibt mindestens einen Testfall, der den Fehler anzeigt, bei dem also Sollund Ist-Resultate nicht übereinstimmen.
Wenn es genau einen solchen Testfall gibt, sprechen wir von einem Punktfehler. Wenn es dagegen Wertebereiche gibt (beispielsweise alle negativen Zahlen oder alle Zeichenreihen, die zwei gleiche Zeichen enthalten), liegt ein Bereichsfehler vor.
Punktfehler entstehen nur durch Sabotage oder (viel wahrscheinlicher) durch Dummheit und Schlamperei, selten auch durch Zufall. Wer beim Testen bestimmte Variablen sehen will und darum eine Abfrage einbaut, die auf die Eingabe »Donald Duck« reagiert, hat, wenn er die Abfrage im Programm vergisst, einen Punktfehler eingebaut. Einen solchen Punktfehler, dessen Ursache uns leider nicht bekannt ist, gab es in Microsoft Excel 4.0 (1992), das speziell auf die Zahl 1,407 374 883 553 28 bizarr reagierte. Anders lag der Fall in einer Triebwerkregelung der US Air Force, in der die Wertebereiche »> K« und »< K« unterschiedlich behandelt wurden, der Wert K aber vergessen worden war; auch das führt zu einem Punktfehler. Wenn der Fehler wie bei Excel für einen von 1015 Werten auftritt, ist seine zufällige Entdeckung praktisch unmöglich; sie ist weniger wahrscheinlich als zwei Sechser im Lotto in zwei aufeinanderfolgenden Ziehungen mit jeweils einem Tipp.
Bereichsfehler betreffen einen bestimmten Anteil q des Eingaberaums. Die Verwendung von n zufällig gewählten Eingaben deckt (unter der realistischen Annahme eines sehr großen Eingaberaums) den Fehler mit der Wahrscheinlichkeit Q = 1-(1-q)n auf. Tabelle 19–1 zeigt für einige Werte von n und q, wie wahrscheinlich es ist, dass ein Bereichsfehler entdeckt wird.
q > |
0,1 |
0,03 |
0,01 |
0,003 |
0,001 |
||||||
10 |
65 |
% |
26 |
% |
10 |
% |
3 |
% |
1 |
% |
|
30 |
96 |
% |
60 |
% |
26 |
% |
9 |
% |
3 |
% |
|
100 |
99,997 |
% |
95 |
% |
63 |
% |
26 |
% |
10 |
% |
|
300 |
100,000 |
% |
99,989 |
% |
95 |
% |
59 |
% |
26 |
% |
|
1000 |
100,000 |
% |
100,000 |
% |
99,996 |
% |
95 |
% |
63 |
% |
Tab. 19–1 Die Wahrscheinlichkeit, dass ein Bereichsfehler entdeckt wird
Wie man an den Zahlen sieht, kommt es auf das Produkt von n und q an. Die folgende Rechnung zeigt, dass ein Fehler mit der Wahrscheinlichkeit e -q ·n unentdeckt bleibt: Die Wahrscheinlichkeit, dass ein Fehler in n Tests nicht erkannt wird, ist
F = (1 - q)n = (1-1/p)n = (1-1/p)p · a = ((1-1/p)p)a = Za
Darin ist p als Kehrwert von q definiert und a als n · q = n / p. Z steht für (1-1/p)p. Für große p kann man statt 1-1/p auch den Kehrwert von 1+1/p setzen wegen (1+x) · (1-x) = 1- x2 ≈ 1 für kleine x. Damit ist Z ≈ (1+1/p)-p ≈ 1/e entsprechend der Definition von e: e = lim (1+1/k)k für k → ∞. Also ist F ≈ e-a ≈ e-q · n. Um einen bestimmten Wert von F zu erreichen, sind n Tests nötig mit n = a/q = - ln(F)/q. Beispiel: Sei q = 10-9, F = 10-3. Daraus folgt n = 3 · ln(10) · 109 ≈ 7 · 109. Wenn wir ein Programm, das in einem von 109 Fällen ein falsches Resultat liefert, mit 7 Milliarden Testfällen untersuchen, ist die Wahrscheinlichkeit, dass wir den Fehler übersehen, etwa eins zu tausend.
Wenn es genau x Möglichkeiten gibt, ein Programm auszuführen (z. B. x verschiedene Werte des einzigen Parameters einer Prozedur), und alle x Möglichkeiten im Test geprüft werden, haben wir einen vollständigen Test durchgeführt. Sind die Resultate in allen Fällen korrekt, beweist der Test die Korrektheit des Prüflings.
Leider ist ein vollständiger Test nur in extrem einfachen Fällen möglich, beispielsweise für eine Prozedur, die prüft, ob mindestens zwei von drei Parametern des Typs boolean TRUE sind; in diesem Fall gibt es nur acht Ausführungsmöglichkeiten. Sobald Zahlen vorkommen, ist die Zahl der möglichen Ausführungen viel zu hoch, um einen vollständigen Test zu gestatten. Schon eine Funktion mit einem einzigen INT-Parameter hat 232, also mehr als vier Milliarden Ausführungsmöglichkeiten, die beliebig unterschiedlich sein können. Ein Programm, das von drei Variablen, Parametern o. Ä. mit je 32 bit abhängt, hat 296 oder etwa 80 · 1027 verschiedene Startzustände, erfordert also im vollständigen Test 80 · 1027 Testfälle. Wer die Möglichkeit hätte, pro Sekunde eine Milliarde Testfälle zu bearbeiten, brauchte dazu etwa das 190-fache Alter des Universums. Darum ist der vollständige, das Programm verifizierende Test völlig ausgeschlossen; ein Programm ist niemals ausgetestet, es ist auch nach vielen tausend Tests kaum angetestet, denn mehr als 99,999 % der möglichen Eingabedaten wurden im Test nicht verwendet!
Wir müssen beim Testen also eine Stichprobe wählen, einige wenige Fälle von den praktisch unendlich vielen möglichen; wenn wir dabei geschickt vorgehen, können wir die Chancen, Fehler zu entdecken, verbessern. Wir können aber niemals sicher sein, alle Fehler entdeckt zu haben. Dijkstra hat das in den berühmten Satz gefasst:
Program testing can be used to show the presence of bugs, but never to show their absence!
E.W. Dijkstra (1970)
Eine bewährte Faustregel besagt, dass in einem (leidlich systematischen) Test die Hälfte aller Fehler auffällt. Eine schlampige Software-Entwicklung kann also nicht durch einen intensiven Test ausgeglichen werden. Wer nach Test und Korrektur weniger als n Fehler in 1000 LOC haben will, muss dafür sorgen, dass 1000 LOC vor dem Test nicht mehr als 2n Fehler enthalten, d. h., es gilt in erster Linie, durch geeignete konstruktive Maßnahmen Fehler zu vermeiden.
Die wichtigsten Vorzüge des Testens gegenüber anderen Verfahren (vor allem Inspektionen) sind:
Testen ist ein »natürliches« Prüfverfahren. Denn wir probieren alles aus, um zu sehen, ob es funktioniert, oft auch, um es (im ursprünglichen Sinne) zu begreifen.
Der (systematische) Test ist reproduzierbar und damit objektiv, wenn die Anfangssituation reproduzierbar ist und die Testumgebung deterministisch arbeitet. Sein Erfolg hängt also nicht von der Tagesform des Testers ab.
Den investierten Aufwand kann man mehrfach nutzen. Denn ein Test lässt sich, einmal vorbereitet und sauber dokumentiert, mit geringem Aufwand wiederholen. Das ist vor allem in der Software-Wartung ein entscheidendes Argument.
Die Testumgebung wird mitgeprüft. Fehler in den beteiligten Komponenten (Übersetzer, Bibliotheken, Betriebssystem usw.) fallen möglicherweise im Test auf.
Das Systemverhalten wird sichtbar gemacht. Auch wenn der Test in der Regel keine Prüfung der Effizienz oder der Bedienbarkeit einschließt, fallen Mängel in diesen und anderen Punkten doch wahrscheinlich auf.
Diesen Vorteilen steht eine Reihe von Nachteilen gegenüber:
Ein Korrektheitsnachweis durch Testen ist praktisch unmöglich (siehe Abschnitt 19.1.5).
Im Test kann man nicht alle Anwendungssituationen nachbilden, sei es, weil sie sich nicht einfach herbeiführen lassen (z. B. eine bestimmte rasche Folge von Interrupts), sei es, weil die Anwendungssituationen noch nicht bekannt sind (z. B. die Lage kurz vor der Kernschmelze eines Atomkraftwerks).
Im Test wird die Funktionalität geprüft. Eventuell werden (eher zufällig) auch Mängel der Bedienschnittstelle oder des Verhaltens unter Last bemerkt. Alle anderen Eigenschaften des Codes, vor allem die Wartbarkeit, sind völlig ausgeblendet.
Nur der Code ist dem Test zugänglich, fast alle anderen Dokumente bleiben beim Testen ungeprüft. Testen fördert also die früher übliche Fixierung der Entwickler auf den Code und die Geringschätzung der übrigen Dokumente.
Der Test zeigt die Fehlerursache nicht. Bis zur Analyse der Testresultate ist nicht einmal klar, ob der Fehler im Prüfling liegt.
Tests kann man nach verschiedenen Kriterien klassifizieren. Dabei gibt es unter den Begriffen starke Abhängigkeiten.
Für die Auswahl der Testfälle kann sich der Tester auf drei Informationsquellen stützen: auf die Spezifikation, auf den Code des Prüflings und auf Informationen, die in früheren Programmläufen gesammelt wurden. Außerdem hilft ihm seine Erfahrung.
Werden die Testfälle auf Basis der in der Spezifikation geforderten Eigenschaften des Prüflings ausgewählt (z. B. Funktionalität, Antwortzeit), dann spricht man von einem Black-Box-Test oder auch von einem Funktionstest (Abschnitt 19.5).
Berücksichtigt man bei der Wahl der Testfälle die innere Struktur des Prüflings und die (durch spezielle Werkzeuge erstellten) Aufzeichnungen früherer Programmläufe, dann handelt es sich um einen Glass-Box-Test oder Strukturtest (Abschnitt 19.6).
Im Allgemeinen weiß der Tester auf Grund seiner Erfahrung, wo mit Fehlern zu rechnen ist, und er wählt Testfälle so, dass sie die typischen Fehler anzeigen. Bekannte Beispiele sind die sogenannten »off-by-one«-Fehler, die bewirken, dass Schleifen einmal zu oft oder zu wenig durchlaufen werden, oder nicht abgefangene Bedienfehler. Dieses Erraten der Fehler (error guessing) hilft vor allem beim Black-Box-Test; der erfahrene Tester entdeckt manche Fehler, die der unerfahrene erst durch einen weit aufwändigeren Glass-Box-Test erkennt.
Ein naiver Test durch Leute, die gar nichts über das Programm wissen, kann Fehler anzeigen, die ein Experte nicht findet. Denn ein Laie erzeugt oft (aus Sicht der Entwickler) völlig unsinnige Eingaben und bizarre Umgebungsbedingungen (z. B. Eingabegerät ausgeschaltet, Datenbank völlig leer), an die bei der Programmierung niemand gedacht hat.
Klassifizieren wir Tests nach dem Aufwand, der für Vorbereitung und Archivierung getrieben wird, dann gibt es die folgenden Formen:
Laufversuch
Der Entwickler übersetzt, bindet und startet sein Programm. Nach einigen Abstürzen und Korrekturen gelingt es, das Programm in ganzer Länge auszuführen und dabei Resultate zu erzielen, die nicht offensichtlich falsch sind.
Wegwerftest
Jemand führt ein Programm aus und gibt dabei spontan oder nach kurzer Überlegung Daten vor. Er betrachtet kurz die Resultate und erkennt in einigen Fällen Fehler. (Das Wort »Wegwerftest« ist hier analog dem Wegwerftaschentuch gemeint: Die Testfälle sind von schlechter Qualität, die Ergebnisse werden nicht archiviert.)
Systematischer Test (vgl. die Definition in Abschnitt 19.1.1)
Jemand – nicht der Autor des Programms – leitet aus der Spezifikation Testfälle ab, führt das Programm mit den Testeingaben aus und vergleicht die Ergebnisse mit den Soll-Resultaten. Prüfling, Randbedingungen, Testfälle und Ist-Resultate werden dokumentiert.
Die Kosten eines systematischen Tests betragen ein Vielfaches der Kosten eines Laufversuchs. Trotzdem ist der systematische Test rentabler, denn er
liefert objektive Aussagen über die durchgeführten Prüfungen,
lässt sich rasch und mit geringen Kosten wiederholen, weil die kreative Leistung dokumentiert ist,
erlaubt, wenn später Fehler auftreten, eine Analyse (»Warum wurde dieser Fehler nicht entdeckt?«), die zur stetigen Verbesserung der Testdatenwahl führt.
Zudem liefert der systematische Test als Nebenprodukt die Daten, die in jeder Prüfung erhoben werden sollten: Mit welchem Aufwand wurde geprüft? Wie viele Fehler wurden gefunden?
Aus diesen Gründen ist der systematische Test letztlich auch aus Kostengründen vorteilhaft. Nachfolgend gehen wir von diesem Ansatz aus.
Wir unterscheiden folgende Tests:
Einzeltest
Bei diesem Test werden einzelne, überschaubare Programmeinheiten getestet, je nach verwendeter Programmiersprache also z. B. Funktionen, Unterprogramme oder Klassen. Er wird häufig auch als Unit-Test bezeichnet.
Modultest
Dieser Test ähnelt dem Einzeltest, aber der Prüfling ist keine einzelne Programmeinheit, sondern eine aus mehreren Einheiten bestehende Komponente (z. B. ein »package« oder »module«).
Integrationstest
Im Integrationstest wird geprüft, ob die zusammengesetzten Programmeinheiten richtig interagieren. Er zielt also darauf ab, Fehler in den Schnittstellen und in der Kommunikation zwischen den Teilen zu finden.
Systemtest
Der Systemtest ist ein Integrationstest auf höchster Ebene, der Kommunikationsprobleme zwischen den Subsystemen anzeigen soll. Gleichzeitig ist dies der einzige Test, der aufdecken kann, dass die geforderte Funktionalität nicht vollständig implementiert wurde.
Im Allgemeinen wird bottom-up, also beginnend mit kleinen Einheiten, getestet.
Neben der Funktionalität gibt es weitere testbare Eigenschaften. Wenn sie seriös geprüft werden sollen, sind dafür spezielle Tests erforderlich.
Funktionstest
Die wichtigste testbare Eigenschaft eines Programms ist seine Funktionalität, die durch die Anforderungen spezifiziert ist.
Installationstest
Es muss möglich sein, die Software mit den gelieferten Anleitungen und Programmen zu installieren und in Betrieb zu nehmen. An die Umgebung (andere Software) und an die Betriebsmittel (z. B. den verfügbaren Speicherplatz) darf die Software nur Ansprüche stellen, die durch die Spezifikation abgedeckt sind.
Wiederinbetriebnahmetest
Ebenso wichtig wie die Installation ist die Wiederinbetriebnahme eines Systems, nachdem der Betrieb unterbrochen war. Anders als bei der Installation kann es bei der Wiederinbetriebnahme Probleme durch alte, defekte oder obsolete Daten desselben Programms geben.
Dieser Test prüft, ob das System über die geforderte Dauer ohne Störungen läuft.
Last- und Stresstest
Dabei wird getestet, ob sich das System auch unter hoher oder höchster Belastung so verhält, wie es gefordert war. Je nach Anforderungen wird auch das Verhalten bei Überlast getestet.
Regressionstest
Nach einer Korrektur oder Veränderung des Programms muss man damit rechnen, dass die Programme neue Fehler enthalten. Um diese zu erkennen, vergleicht man im Regressionstest die Resultate des veränderten Programms mit entsprechenden alten Resultaten (siehe Abschnitt 19.1.3).
Generell muss jede testbare Anforderung – nicht nur jede funktionale – durch mindestens einen Testfall abgedeckt werden. Das gilt beispielsweise für maximale Antwortzeiten oder für die minimalen Hardware-Anforderungen. Auch hier liefert der Test natürlich keinen Beweis der Korrektheit.
Besonders bei Software-Produkten spricht man von Alpha- und Beta-Tests. Dagegen ist der Abnahmetest charakteristisch für Auftragsprojekte.
Alpha- und Beta-Test
Als Alpha-Test wird der Test eines Software-Produkts beim Hersteller bezeichnet. Die Software kann dabei noch erhebliche Mängel haben. Sind diese Mängel behoben, so wird das Produkt im Beta-Test speziellen Kunden zur Verfügung gestellt, damit sie es entsprechend seinem Zweck benutzen. Diese Kunden genießen früher als andere die Vorteile eines neuen Produkts, müssen dafür aber auch mehr oder minder gravierende Mängel in Kauf nehmen. Ihre Erfahrungen werden vom Hersteller ausgewertet, um das Produkt zu verbessern.
Abnahmetest
Der Akzeptanz- oder Abnahmetest ist aus Sicht des Software-Herstellers kein Test, sondern eine Vorführung. Er soll zeigen, dass sich das System so verhält, wie es der Vertrag, die Spezifikation verlangt. Der Kunde wird allerdings versuchen, Schwachstellen zu erkennen. Dazu kann er eigene Testfälle mitbringen. Besteht die Software den Abnahmetest, so werden in der Regel Zahlungen des Kunden fällig, und die Gewährleistungsfrist beginnt.
Wie wir in Abschnitt 19.2.2 beschrieben haben, ist ein systematischer Test aus vielen Gründen einem Laufversuch oder Wegwerftest vorzuziehen. Nachfolgend stellen wir den prinzipiellen Ablauf vor, der jedem systematischen Test zu Grunde liegt.
Jeder systematische Test besteht im Kern aus den Schritten Vorbereitung, Ausführung und Auswertung. Planung und Analyse sind nicht minder wichtig, stehen aber getrennt davon am Anfang und am Ende des Projekts (Abb. 19–3).
Weil der Test eines Systems aus mehreren einzelnen Tests besteht und insgesamt erhebliche Ressourcen beansprucht, müssen die Tests sorgfältig geplant und aufeinander abgestimmt werden. Dabei ist zu entscheiden, welche Eigenschaften durch welche Tests geprüft werden sollen, es sind also die Testziele und die Teststrategie zu bestimmen. Auf dieser Basis wird jeder einzelne Test geplant: Die erforderlichen Schritte werden festgelegt, die benötigten Ressourcen und die Dauer werden ermittelt.
Eine Analyse der Testberichte sollte Teil des Projektabschlusses sein. Hier ist zu prüfen, ob die Teststrategie und der Testaufwand sinnvoll gewählt waren. Die dokumentierten Ergebnisse der Analyse fließen in Maßnahmen zur Verbesserung des Entwicklungsprozesses und ganz speziell der Tests ein.
Abb. 19–3 Prinzipieller Testablauf
In der Testvorbereitung wird die eigentliche intellektuelle Leistung des Testens erbracht. Je gründlicher sie durchgeführt wird, desto einfacher wird die Testausführung. Im Extremfall kann die Ausführung ganz automatisiert werden.
Der wichtigste und umfangreichste Teil der Vorbereitung ist die Auswahl und Spezifikation der Testfälle. Dazu ist zunächst festzulegen, welche Testgüte erreicht werden soll. Anschließend werden die Testfälle möglichst systematisch gewählt; wir gehen darauf in den Abschnitten 19.4 bis 19.6 näher ein. Die Testfälle sind in sogenannten Testszenarien zu gruppieren und sollten nach Prioritäten geordnet werden, damit bei der Testdurchführung die wichtigsten Testfälle auch dann ausgeführt werden, wenn der Test unter Termindruck verkürzt werden sollte, was in der Praxis eher die Regel als die Ausnahme ist.
Daneben muss das benötigte Testgeschirr, das ist die Umgebung, um den Test durchzuführen, geschaffen werden. Zum Testgeschirr gehören die Testtreiber (test drivers) und die Platzhalter (stubs), die die fehlenden Komponenten vertreten.
test driver — A software module used to invoke a module under test and, often, provide test inputs, control and monitor execution, and report test results. Syn: test harness.
stub — (1) A skeletal or special-purpose implementation of a software module, used to develop or test a module that calls or is otherwise dependent on it.
(2) A computer program statement substituting for the body of a software module that is or will be defined elsewhere.
IEEE Std 610.12 (1990)
Ein Testtreiber versorgt den Prüfling mit Testeingaben, ein Platzhalter steht für eine Komponente, die vom Prüfling benötigt wird, aber (noch) nicht integriert ist. Ein Platzhalter liefert entweder vordefinierte Werte oder simuliert (meist nur oberflächlich) das Verhalten der fehlenden Komponente. Da Platzhalter, vor allem solche mit einer gewissen Funktionalität, beträchtlichen Aufwand verursachen, sollte der Test so angelegt werden, dass möglichst wenige davon benötigt werden (siehe Abschnitt 20.2).
Größere Einheiten erfordern oft zusätzliche Hard- und Software, z. B. eine Datenbank in definiertem Zustand oder einen Motorprüfstand, der die vom Prüfling zu verarbeitenden Messdaten in Echtzeit liefert. Aber auch spezielle Testwerkzeuge, die den Test unterstützen, gehören zum Testgeschirr. Bei sicherheitskritischen Anwendungen kann das Testgeschirr bis zu einem Drittel des Gesamtaufwands beanspruchen. Darum wird es nicht weniger sorgfältig erstellt und verwaltet als das eigentliche Produkt. Jeder Entwickler sollte daran denken, dass das Testen mit der Auslieferung der Software nicht endgültig beendet ist, sondern während der Wartung immer wieder stattfindet.
Das Testvorgehen, also die Sequenz der im Test durchzuführenden Schritte, wird so festgelegt, dass die Tests in möglichst rascher Folge und mit möglichst wenigen Eingriffen und Änderungen durchgeführt werden können. Beispielsweise können durch die Testfälle die Belegungen der Datenbank aufgebaut werden, die in weiteren Testfällen vorausgesetzt werden. Auf diese Weise lassen sich die »Rüstzeiten« minimieren. Das Testvorgehen wird in der Testvorschrift dokumentiert.
Ist die Testumgebung fertiggestellt, kann der Prüfling aus der Konfigurationsverwaltung kopiert und in der Testumgebung installiert werden. Nachdem sichergestellt wurde, dass der Prüfling in der Testumgebung tatsächlich ausgeführt werden kann (was nicht immer der Fall ist), kann die Testausführung nach der Testvorschrift beginnen. Dabei wird genau protokolliert, welche Testfälle ausgeführt und welche Ergebnisse dabei erzielt wurden. Das Testprotokoll ist ein wichtiges Dokument der Testausführung, dem nicht nur die Ist-Resultate, sondern auch die exakten Angaben zu allen beteiligten Software-Einheiten (Prüfling, Testdaten usw.) zu entnehmen sind.
Soweit der Test nicht automatisiert ist, also vor allem bei interaktiven Programmen, wird der Tester offensichtliche Fehler sofort erkennen und die betreffenden Resultate im Protokoll kennzeichnen; weitere Konsequenzen sollten zu diesem Zeitpunkt nicht gezogen werden. Insbesondere sollte der Tester weder den Test abbrechen noch versuchen, den Fehler zu beheben, sondern wenn möglich den Test vollständig durchführen.
Natürlich kann es vorkommen, dass ein schwerwiegender Fehler eine Fortsetzung unmöglich macht. Wenn das Programm bei der ersten Eingabe abstürzt, muss der Test möglicherweise abgebrochen werden. In diesem Falle hat die Qualitätssicherung in der Entwicklung versagt. Der Test und die daran geknüpfte Korrektur der Programme soll und kann aus guter Software sehr gute Software machen, nicht aber einer Programmruine Leben einhauchen.
Der Prüfling wird also während des Tests nicht verändert oder korrigiert, sodass der gesamte Test mit einer ganz bestimmten Version durchgeführt wird. Auch darf der Prüfling nicht speziell für den Test modifiziert werden, er kann aber feste, speziell den Test unterstützende Bestandteile haben (wenn z. B. der normalerweise nicht erkennbare Zustand des Systems überprüft werden soll). Für die Testausführung fordern wir also:
Keine spezielle Testvariante des Prüflings!
Kein Abbruch des Tests, wenn Fehler erkannt wurden!
Keine Modifikationen des Prüflings im Test!
Kein Wechseln zwischen Test und Debugging!
Die Einhaltung dieser Regeln hat folgende Vorteile:
Der Aufwand für den Test lässt sich recht genau abschätzen.
Der Test deckt die grundsätzlichen Mängel auf.
Es werden keine kumulativen Korrekturen durchgeführt.
Das Testgeschirr wird effizient genutzt.
Der Prüfling wird nicht durch vergessene Zusätze für die Fehlersuche beschädigt.
Die erzielten Ergebnisse betreffen ein ganz bestimmtes Programm, im letzten Test das später auszuliefernde Produkt.
Wie schon erwähnt, ist das Testprotokoll ein wichtiges Ergebnis. Es belegt, mit welchem Testgeschirr an welchem Prüfling (in welcher Version und Variante) welche der Testfälle ausgeführt und welche Ergebnisse dabei erzielt wurden. Das Protokoll wird darum wie alle anderen Dokumente der Konfigurationsverwaltung unterstellt.
Der letzte Schritt beim Testen ist der Vergleich der Ergebnisse mit den Soll-Resultaten. Wird der Test Schritt für Schritt interaktiv durchgeführt, dann werden Istund Soll-Ergebnis für jeden Testfall sofort verglichen. Wenn der Test automatisiert ist, übernimmt ein spezielles Werkzeug den Vergleich von Soll- und IstResultaten. In beiden Fällen muss sichergestellt sein, dass Abweichungen nicht nur zufällig auffallen, sondern systematisch entdeckt werden. Dabei sollten auch unbeabsichtigte Effekte des Programms erkannt werden. Wenn beispielsweise eine Information aus einer Datenbank abgerufen wird, erkennt man leicht, ob das Resultat den Erwartungen entspricht. Man kann aber im Allgemeinen nicht ausschließen, dass der Inhalt der Datenbank anschließend verändert ist. Eine solche Kontrolle, ob das Programm keine unbeabsichtigten Nebenwirkungen hat, ist meist sehr viel schwieriger als die Kontrolle der geforderten Wirkungen.
Jede erkannte Abweichung, die nicht als falsch positiv klassifiziert werden kann (siehe Abschnitt 13.4.2), löst eine Problemmeldung aus und liefert damit die notwendigen Informationen für die Fehlersuche und -behebung.
Das Ergebnis der Testauswertung ist der Testbericht. Er enthält Verweise auf alle den Test betreffenden Dokumente (insbesondere auf das Testprotokoll) und administrative Angaben (z. B. die Namen der beteiligten Personen, die Anzahl und Schwere der gefundenen Fehler und den benötigten Aufwand). Die zusammenfassende Schlussbewertung des Prüflings gibt auch Auskunft darüber, ob das Testendekriterium (Abschnitt 19.3.5) erreicht wurde.
In der Praxis werden Tests zwar eingeplant, die dafür vorgesehenen Zeiten werden aber oft als Puffer behandelt, die die in der Entwicklung eingetretenen Verzögerungen ausgleichen. Das Testendekriterium ist damit: Auslieferungstermin erreicht. Auf diese Weise kommt der Test im wahren Sinne des Wortes zu kurz. Außerdem wird zwar meist der Test geplant, nicht aber die erforderliche Korrektur und die Wiederholung des Tests nach der Korrektur. Damit bleibt den Testern gar keine andere Wahl als der unsystematische Wechsel zwischen Test und Debugging.
Besser ist es, ein sinnvolles Testendekriterium festzulegen. Man kann den Test beenden, wenn
a) alle spezifizierten Testfälle ohne Befund absolviert wurden,
b) er x Stunden (Tage, Wochen) gedauert hat,
c) er den Aufwand y beansprucht hat,
d) n Fehler gefunden sind,
e) z Stunden (Tage, Wochen) lang kein Fehler mehr entdeckt wurde,
f) die durchschnittlichen Testkosten für die Entdeckung eines Fehlers x Euro übersteigen (siehe Abb. 19–4).
Abb. 19–4 Kosten als Testendekriterium
Die Zahlenwerte für x, n, z werden auf Grund von Erfahrungen oder Schätzungen festgelegt.
Die Kriterien a bis f sind nicht alle gleich sinnvoll und auch nicht mit jedem Testverfahren verträglich. Wenn eine abgeschlossene Menge von Testdaten vorliegt, ist offensichtlich das Kriterium a zweckmäßig. Das Kriterium d setzt voraus, dass man sehr genau abschätzen kann, mit wie vielen Fehlern zu rechnen ist. Eine untere Grenze kann in Kombination mit b oder c als notwendiges, aber nicht hinreichendes Kriterium für das Testende sehr sinnvoll sein. Kriterium e kommt in Frage, wenn mit Zufallsdaten getestet wird. Das ist nicht nur beim eigentlichen statistischen Testen (siehe Abschnitt 19.7) der Fall, sondern auch, wenn es (wie z. B. beim Test einer Software für eine Motorsteuerung mit Signalen von einem Motor auf dem Prüfstand) Echtzeiteffekte gibt, die nicht deterministisch geplant werden können.
Auch die Testgüte, z. B. die erreichte Überdeckung im Glass-Box-Test (siehe Abschnitt 19.6), liefert sinnvolle Testendekriterien.
Wir gehen hier nicht auf die zahlreichen Werkzeuge ein, die auf dem Markt angeboten werden; sie können grob in die folgenden Kategorien geordnet werden:
Werkzeuge für den Glass-Box-Test, die den Code instrumentieren und Überdeckungsmaße ermitteln; oft bieten sie weitere Metrik-Funktionen an.
Werkzeuge für den Einzel- und Systemtest, die Testfälle und Resultate verwalten und die automatische Durchführung der Tests unterstützen.
Capture/Replay-Werkzeuge zur automatischen Aufzeichnung der Interaktionen, damit auch interaktive Programme automatisch getestet werden können, sowie Hilfsmittel zur Bearbeitung der Skripts, die von den Capture/Replay-Werkzeugen generiert werden.
Hilfsmittel für den Soll-Ist-Vergleich und zur Verwaltung von Testfällen für den interaktiven Test (»manual testing«).
Spezialwerkzeuge, beispielsweise Werkzeuge für den Last- und Stresstest oder Werkzeuge für den Test von eingebetteter Software.
Zumindest ein Werkzeug für den Glass-Box-Test gehört zwingend zur Grundausstattung des Testers; der Regressionstest muss durchgängig mit Werkzeugen unterstützt werden.
Wie bei allen Werkzeugen im Software Engineering ist die Literaturlage betrüblich; die meisten der im Web reichlich angebotenen Arbeiten sind alles andere als neutrale Informationen. Eine sehr umfangreiche Liste meist kommerzieller Testwerkzeuge findet man beispielsweise auf den Webseiten der IMBUS AG (Testtools, o.J.); Informationen über Open-Source-Testwerkzeuge werden auf den von M. Aberdour erstellten Webseiten angeboten (Open-Source-Testtools, o.J.).
Für jeden Test muss eine sinnvolle Menge von Stichproben (Testfällen) ausgewählt werden; diese Auswahl ist die zentrale Aufgabe des Testers. Dabei versucht er, mit einer möglichst kleinen Menge von Testfällen möglichst vielen Fehlern auf die Spur zu kommen.
Ein Testfall ist gut, wenn er mit hoher Wahrscheinlichkeit einen noch nicht entdeckten Fehler aufzeigt; er ist dann erfolgreich, wenn er einen noch nicht entdeckten Fehler nachweist. Ein idealer Testfall ist
repräsentativ, d. h., er steht stellvertretend für viele andere Testfälle,
fehlersensitiv, d. h., er hat nach der Fehlertheorie eine hohe Wahrscheinlichkeit, einen Fehler anzuzeigen,
redundanzarm, d. h., er prüft nicht, was auch andere Testfälle schon prüfen.
Die Praxis verlangt auch hier Kompromisse; das Ziel, möglichst viele Fehler zu entdecken, veranlasst uns, bei den genannten Merkmalen Abstriche zu machen.
Damit Testfälle wiederverwendet werden können, müssen sie exakt dokumentiert werden. Zu jedem Testfall sind die folgenden Informationen anzugeben:
der Anfangszustand der Umgebung, eventuell auch des Prüflings,
die Werte aller Eingabedaten,
die notwendigen Bedienungen,
die erwarteten Ausgaben,
wenn nötig die Maßnahmen, um das System nach dem Test in einen definierten Zustand zu bringen.
Zusätzlich sollten für jeden Testfall die durch ihn abgedeckten Anforderungen angegeben werden. Die vollständig definierten Testfälle werden in der Testfallspezifikation zusammengefasst. Es hat sich bewährt, die Testfälle nicht nur mit Prioritäten zu versehen, sondern sie auch inhaltlich zu strukturieren. Eine dreistufige Hierarchie, bestehend aus Testgruppe, Test-Suite und Testfall, ist zweckmäßig.
Die Ansätze, um Fehler in Programmen durch Testen zu finden, lassen sich drei Kategorien zuordnen, die durch unterschiedliche Modelle der (möglicherweise fehlerhaften) Programme gekennzeichnet sind:
a) Ein Programm wird als eine Menge von Elementen betrachtet, wobei verschiedene Elemente auf verschiedenen Abstraktionsebenen in Frage kommen, z. B. Anweisungen oder Funktionen im Sinne der Spezifikation. Nachdem man eine dieser Mengen gewählt, jedes Element darin (also jede Anweisung bzw. jede Funktion) geprüft und korrekte Resultate erhalten hat, erwartet man, dass kein Element defekt ist und zu einem Fehler führt.
b) Wird ein Programm mit bestimmten Daten ausgeführt, so entsteht eine Folge von Schritten, die man als Programmpfad bezeichnet. Nachdem man jeden möglichen Pfad mindestens einmal getestet und korrekte Resultate erhalten hat, erwartet man, dass keiner der Pfade falsch angelegt ist und darum zu einem falschen Ergebnis führt.
c) Wird ein Programm mit Daten getestet, die gleiche statistische Eigenschaften haben wie die Daten, die später beim Gebrauch des Programms verwendet werden, kann aus den Resultaten des Tests eine (statistische) Aussage über die Zuverlässigkeit des Programms abgeleitet werden.
Alle drei Ansätze beruhen auf Erfahrungen außerhalb des Software Engineerings:
a) Wenn im Auto der Lüfter läuft, ist der Radioempfang gestört.
Hier ist anscheinend eine Komponente schuld.
b) Immer, wenn ich nach einer langen Fahrt das Auto kurz abgestellt habe, kann ich den Motor nicht mehr starten.
Hier führt offenbar ein bestimmter Ablauf (Pfad) zu einem Fehler.
c) Im Kurzstreckenverkehr ist das Auto sehr zuverlässig.
Hier schließen wir (unter der Voraussetzung, dass weiterhin die gleichen Randbedingungen gelten) aus einer Erfahrung auf die Zukunft.
Keiner der Ansätze lässt sich für normale Programme so umsetzen, dass die oben formulierten Ansprüche wirklich erfüllt werden; die Ansätze beruhen eben auf vereinfachten Modellen:
a) Die Tatsache, dass ein Element unter bestimmten Bedingungen funktioniert, garantiert leider nicht, dass es immer funktioniert. Das Konzept der Überdeckungen ist blind für die Wechselwirkungen zwischen den Elementen.
b) Wir können die möglichen Pfade im Allgemeinen nicht feststellen, und in den meisten Fällen wären es auch viel mehr, als wir testen können. Zudem ist es möglich, dass auch von zwei Ausführungen auf demselben Pfad die eine korrekte, die andere falsche Resultate liefert.
c) Die statistischen Eigenschaften der Daten lassen sich im Allgemeinen nicht voraussagen, sie können sich auch im Laufe der Zeit verändern. Bei einem Test mit Zufallsdaten ist es schwierig bis unmöglich, die Soll-Resultate festzustellen. Zudem gibt eine statistische Aussage zur Zuverlässigkeit im Einzelfall keine Sicherheit.
Trotzdem geben uns die drei Modelle wichtige Hinweise, wie ein Test möglichst erfolgreich angelegt werden kann. Ansatz c führt zum statistischen Test (Abschnitt 19.7), Ansatz a zu den Überdeckungsmaßen, die im Black-Box-Test, vor allem aber im Glass-Box-Test (Abschnitt 19.6) verwendet werden. Als (Test-)Überdeckung bezeichnet man den Anteil der Elemente, die im Test benutzt wurden, an der Gesamtzahl.
Ansatz b führt zum Konzept der Äquivalenzklassen (siehe Abschnitt 19.4.4) und damit zum Black-Box-Test (Abschnitt 19.5); in Abschnitt 19.6.6 wird erläutert, warum die in der Literatur übliche Zuordnung der Pfadüberdeckung zum Glass-Box-Test nicht sinnvoll ist.
Wir wollen mit wenigen Testfällen eine möglichst große Wirkung erzielen, d. h. viele Fehler entdecken. Mit anderen Worten: Wir versuchen, keine überflüssigen Testfälle zu bearbeiten. Myers hat dazu das Konzept der Äquivalenzklassen entwickelt (Myers, 1979).
Nehmen wir beispielsweise an, dass ein Programm Personennamen verwaltet. Ein eingegebener Name wird gespeichert; wenn er ein zweites Mal eingegeben wird, soll eine Fehlermeldung erzeugt werden. Im Test geben wir einen Namen (»Meier«) ein und erwarten, dass er akzeptiert wird. Geben wir diesen Namen erneut ein, so sollte die Fehlermeldung erscheinen. Natürlich können wir den Test mit dem Namen »Mayer« wiederholen. Sehr wahrscheinlich wird der Erfolg oder Misserfolg des Tests der gleiche sein wie für »Meier«. Denn es gibt keinen Grund zur Vermutung, dass der Ablauf für »Mayer« in irgendeiner Weise anders ist als für »Meier«: Entweder treten bei keinem der beiden Namen Fehler auf, oder bei beiden. Die beiden Testsequenzen sind also – wenn unsere Annahmen stimmen – äquivalent. Von mehreren äquivalenten Testfällen brauchen wir nur einen zu testen, denn das Resultat ist auch für alle anderen Fälle in dieser Äquivalenzklasse gültig.
Wir definieren: Zwei Testfälle f1 und f2 sind im Hinblick auf den Erfolg des Tests stark äquivalent, wenn sie austauschbar sind, weil beide geeignet oder beide ungeeignet sind, einen bestimmten Fehler anzuzeigen1.
Wenn die Elemente einer Menge möglicher Testfälle F = {f1, f2, ..., fn} für ein Programm P stark äquivalent sind, reicht es aus, einen einzigen Repräsentanten von F für den Test auszuwählen. F ist eine (starke) Äquivalenzklasse der Testfälle für P. Ein »wasserdichtes« Verfahren, um alle starken Äquivalenzklassen zu identifizieren, wäre so etwas wie der Stein der Weisen für den Test. Den gibt es leider nicht, wenn man von der irrealen Idee des vollständigen Tests absieht.
Ein vollständiger Test auf der Basis der starken Äquivalenzklassen erfordert, dass aus jeder Klasse ein Testfall gewählt wird. Da die Fälle in derselben Klasse äquivalent sind, decken alle so erzeugten Testdaten dieselben Fehler auf. Da jede Klasse berücksichtigt wird, also (falls das Programm Fehler enthält) auch jede Klasse, die einen Fehler aufdeckt, decken die Testdaten alle Fehler auf.
Da wir keine Möglichkeit haben, die Klassen starker Äquivalenz zuverlässig zu erkennen, behelfen wir uns mit schwacher Äquivalenz: Zwei Testfälle sind schwach äquivalent, wenn wir Gründe haben, starke Äquivalenz zu vermuten. Solche Gründe sind zahlreich: Wenn z. B. zwei Eingaben den gleichen Ablauf des Programms bewirken, die gleiche Funktion des Programms in Anspruch nehmen oder die gleiche Fehlermeldung hervorrufen, können wir Äquivalenz vermuten. Jede schwache Äquivalenz beruht auf einer spekulativen Fehlertheorie: Wir rechnen damit, dass die Klassen starker Äquivalenz bestimmte Muster aufweisen, die wir erraten können. Wenn verschiedene Fälle denselben Programmablauf (Pfad) hervorrufen, sind sie wahrscheinlich äquivalent. Dies ist der Zusammenhang zwischen Pfadüberdeckung und Äquivalenzklassen.
Wenn ein Programm nach Eingabe des Datums »31. Juni 2010« eine Fehlermeldung bringt, wird es auch den 31. September 2010 nicht akzeptieren, und umgekehrt. Hinter dieser Aussage steckt kein Wissen, sondern eine Hypothese: Wir vermuten, dass der 31. September genau denselben Programmablauf, dieselben Entscheidungen auslöst wie der 31. Juni. Diese Spekulation kann falsch sein, dann stimmen schwache und starke Äquivalenz nicht überein, und wir entdecken Fehler nicht, die wir hätten entdecken können. Wenn der Programmierer geglaubt hatte, dass alle geradzahligen Monate 31 Tage haben (wie August, Oktober und Dezember »beweisen«), sind die beiden Datumsangaben nicht äquivalent. Bei den Namen im ersten Beispiel können wir vermuten, dass auch »Müller« in der Äquivalenzklasse liegt, zu der »Meier« und »Mayer« gehören. Es ist aber denkbar, dass wegen des Umlauts oder des Doppelkonsonanten der Name »Müller« anders behandelt wird und darum nicht mit Meier« und »Mayer« in einer Klasse liegt. Natürlich könnte es auch ganz anders sein, etwa wenn das »y« eine spezielle Reaktion hervorruft oder wenn der Programmierer Mayer hieß und die »originelle« Idee hatte, bei Auftreten seines Namens eine besondere Reaktion des Programms auszulösen.
Unsere Erfahrung beim Test von Programmen zeigt, dass die Behandlung von Eingaben, die an den Grenzen von Wertebereichen liegen, häufig fehlerhaft ist. Darum werden die Grenzwerte als eigene Äquivalenzklassen behandelt. Wenn wir mit REAL-Zahlen arbeiten, müssen wir statt des (im Allgemeinen nicht darstellbaren) Nachbarwertes einen geringfügig größeren oder kleineren Wert verwenden.
Die Bildung der Äquivalenzklassen (für gültige und vor allem ungültige Eingaben) erfolgt nach Erfahrung und Intuition, also heuristisch! Um die Äquivalenzklassen für die Eingaben zu bestimmen, geht man wie folgt vor:
1. Der Spezifikation werden die Eingabegrößen und ihre Gültigkeitsbereiche entnommen. Die Grenzen der Eingabebereiche, die geordnet sind, trennen Äquivalenzklassen gültiger und ungültiger Eingaben.
2. Wenn man vermutet, dass die Werte einer Äquivalenzklasse ungleich behandelt werden, teilt man die Klasse in neue Unterklassen auf.
3. Werte an Bereichsgrenzen, die erfahrungsgemäß oft falsch verarbeitet werden, sollten gesondert behandelt, also speziellen Äquivalenzklassen zugeordnet werden.
4. In jeder Äquivalenzklasse wird ein Testfall gewählt (Eingabe und Soll-Resultat).
Tabelle 19–2 zeigt einige einfache Beispiele. Bei der Enumeration wurde unterstellt, dass keine unterschiedliche Behandlung der drei Werte zu vermuten ist. Für die REAL-Zahlen wurde eine fiktive Darstellungsgenauigkeit angenommen.
Klasse gültiger Eingaben |
Klassen ungültiger Daten |
Grenzfälle |
Repräsentant |
|
Die ganzen Zahlen von a = 5 bis n = 555 |
a ≤ E ≤ n |
|
5 |
100 |
|
E<a |
|
1 |
|
|
E>n |
4 |
999 |
|
Natürliche Zahl (inkl. 0) bis max. n = 1000 |
E>n |
|
0 |
227 |
|
E<0 |
|
-171 |
|
|
E>n |
-1 |
1033 |
|
Enumeration (Alpha, Beta, Gamma) |
E |
|
|
Gamma |
|
E ∉ {Alpha, Beta, Gamma} |
|
Omega |
|
Bereich der REAL-Zahlen mit Betrag bis 1 |
-1,0 ≤ E ≤ 1,0 |
|
-1,0 |
0,555 |
|
E < -1,0 |
|
-2,500 |
|
|
E> 1,0 |
-1,0001 |
1,015 |
Tab. 19–2 Bildung von Äquivalenzklassen für den Eingabewert E
Betrachten wir als weiteres Beispiel ein Programm, das in Zeichenreihen, die zwischen 1 und 100 Zeichen lang sein können, die Kleinbuchstaben durch Großbuchstaben ersetzen soll. Eine extrem simple Fehlertheorie wäre, dass die Umwandlung entweder immer versagt oder immer korrekt funktioniert. Dann gehören alle möglichen Eingaben zur selben Klasse. Da wir Fehler nur erkennen können, wenn eine Umwandlung stattfindet, wählen wir einen Testfall mit Kleinbuchstaben, beispielsweise »XYZ++abc«; das Soll-Resultat ist »XYZ++ABC«.
Vermutlich werden Zeichenreihen ohne Kleinbuchstaben anders behandelt, wir schaffen darum eine zweite Klasse, die wir durch den Testfall »$$RWTH$$« repräsentieren (Soll-Resultat = Eingabe).
Natürlich lassen sich die Fehler in der Regel nicht so leicht aufdecken, wir nehmen also unsere Erfahrung hinzu. Die Grenzfälle erfordern in den Programmen oft eine spezielle Bearbeitung. Wir identifizieren also zwei weitere Klassen, nämlich Zeichenreihen minimaler und maximaler Länge. Schließlich ist in vielen Fällen robustes Verhalten des Programms gefordert, es soll also sinnvoll reagieren, wenn die Längenbedingung verletzt wird. Damit entstehen zwei weitere Klassen, die leere Zeichenreihe und eine mit mehr als 100 Zeichen; insbesondere 101 Zeichen sollten getestet werden.
Wenn wir vermuten, dass die Reihenfolge der Zeichen eine Rolle spielt, dass also »aB« möglicherweise anders behandelt wird als »Ba«, dann ergeben sich auch dadurch weitere Klassen. Und schließlich müssen wir überlegen, ob die Kombination der genannten Klassen neue Klassen ergibt oder nicht, ob wir also beispielsweise zwischen maximal langen Zeichenreihen mit und ohne Kleinbuchstaben unterscheiden müssen.
Das Beispiel zeigt: Die Äquivalenzklassen können nicht einfach mechanisch festgelegt werden, Intuition und Erfahrung sind von großer Bedeutung. Und es zeigt auch, dass wir niemals sicher sein können, alle Klassen der starken Äquivalenz erkannt zu haben. Wird die Zeichenreihe beispielsweise vom Programm in Abschnitte zu je 32 Zeichen gegliedert, dann muss man mit Problemen rechnen, wenn gerade 32, 64 oder 96 Zeichen eingegeben werden, auch 33, 65 und 97 wären dann speziell zu betrachten.
Ein letztes (nicht erfundenes) Beispiel zeigt, warum wir nie sicher sein können, alle Klassen der starken Äquivalenz entdeckt zu haben: Ein kleines ADA-Demonstrationsprogramm zur rekursiven Berechnung der Fakultätfunktion wurde mit den Mitteln des Exception Handlings gegen alle Fehleingaben abgesichert, also gegen Eingabe negativer oder gebrochener Zahlen und gegen die Eingabe von Text. Zusätzlich wurde sichergestellt, dass die Berechnung der Fakultät bei der Eingabe einer großen Zahl nicht zu einem Zahlenüberlauf führt. Mit den Eingaben -5, 3.14, Blabla und 1000 funktionierte alles einwandfrei. Trotzdem brachte ein naiver Benutzer das Programm sofort zum Absturz: Er hatte als Argument 1000000 eingetippt. Aber war dieser Fall nicht abgefangen und getestet worden (durch die Äquivalenzklasse »Zahl zu groß« mit dem Repräsentanten 1000)? Nein. Denn die Eingabe einer sehr großen Zahl führt, bevor es zum Zahlenüberlauf kommt, zu einem Kellerüberlauf (stack overflow). Eine Million Rekursionsstufen waren zu viel. Von der Klasse »Zahl zu groß« musste also eine Klasse »Zahl viel zu groß« abgespalten werden.
Bei einem Black-Box-Test betrachtet man das Programm als Monolithen, über dessen innere Beschaffenheit man nichts weiß und nichts wissen muss. Man prüft, ob das Programm tut, was die Spezifikation verlangt. Dies ist die wichtigste Form des Tests. Alle anderen Tests ergänzen den Black-Box-Test, sie können ihn nicht ersetzen.
Testfälle für den Black-Box-Test sollten vorbereitet werden, sobald die Spezifikation vorliegt (siehe Punkt 10 in Abschnitt 16.9). Dadurch ist nicht nur gewährleistet, dass die Testfälle rechtzeitig verfügbar sind, sondern die Spezifikation wird einer zusätzlichen Prüfung unterzogen, die oft Fehler und Lücken aufdeckt. Denn wer einen Testfall, also Eingabe und Soll-Resultat entwirft, muss genau hinschauen und kann nicht übersehen, wenn die Spezifikation seine Fragen nicht oder nicht eindeutig beantwortet.
Ein umfassender Black-Box-Test sollte
alle Funktionen des Programms aktivieren (Funktionsüberdeckung),
alle möglichen Eingaben bearbeiten (Eingabeüberdeckung),
alle möglichen Ausgabeformen erzeugen (Ausgabeüberdeckung),
die Leistungsgrenzen ausloten,
die spezifizierten Mengengrenzen ausschöpfen,
alle definierten Fehlersituationen herbeiführen.
Die Funktionsüberdeckung geht von der Menge der Funktionen aus, die ein Programm anbietet. Die Eingabeüberdeckung ist auf die Menge möglicher Eingaben bezogen, die Ausgabeüberdeckung auf die Menge möglicher Ausgaben. Achtung, es geht hier nicht um alle im Detail unterschiedlichen Ein- und Ausgaben, also um vollständigen Test, sondern um die Klassen der Ein- und Ausgaben. Zum Beispiel fallen zwei Namen in dieselbe Klasse, wenn es nicht aus speziellen Gründen geraten erscheint, sie zu unterscheiden. Oft sind Ein- und Ausgabeüberdeckung durch die Funktionsüberdeckung impliziert, aber nicht immer.
Beispiel: Ein Programm, das Auskunft über Studenten gibt, biete die folgenden Funktionen: (a) Anfangsmeldung, (b) Einlesen einer Matrikelnummer, (c) Ausgabe der über den Studenten gespeicherten Informationen oder (d) einer Fehlermeldung (»Student existiert nicht«), (e) Endemeldung. Statt der Matrikelnummer kann auch der Name eingegeben werden. Wir können also alle Funktionen in Anspruch nehmen und dabei immer nur die Matrikelnummer verwenden. Wir haben dann die Funktionsüberdeckung, aber nicht die Eingabeüberdeckung erreicht.
Alle Funktionen außer d können wir mit einem einzigen Testfall abdecken. Fehlerfälle benötigen typischerweise jeweils einen eigenen Testfall, wenn der Programmablauf im Fehlerfall abgebrochen wird. Wenn ein Programm mit k verschiedenen Fehlerfällen enden kann, brauchen wir mindestens k+1 Testfälle, um die vollständige Funktionsüberdeckung zu erreichen; wenn sich verschiedene Funktionen gegenseitig ausschließen, sind es entsprechend mehr.
Die Grenzfälle (z. B. Namen minimaler und maximaler Länge sowie die angrenzenden Fehlerfälle) werden wie oben beschrieben speziell getestet.
Um die Testfälle für eine vollständige Funktionsüberdeckung zu gewinnen, kann man nach folgendem Schema vorgehen:
1. Suche in der Spezifikation der Anforderungen alle Funktionen und schreibe sie in eine Liste.
2. Suche in den Anforderungen alle Eingabe- und Ausgabegrößen.
3. Suche zu einer noch nicht ausgeführten Funktion aus der Liste die benötigten Eingabegrößen und die zu erzeugenden Ausgabegrößen.
4. Definiere für diese Funktion einen Testfall, d. h. die Eingabe und die erwartete Ausgabe. Markiere die von diesem Testfall ausgeführten Funktionen in der Liste.
5. Wiederhole die Schritte 3 und 4, bis alle Funktionen der Liste markiert sind.
Die Liste der Funktionen und der Markierungen, welche Funktionen in welchem Testfall ausgeführt werden, wird üblicherweise in Form einer Tabelle dargestellt, der sogenannten Funktionstestmatrix.
In den Abschnitten 19.8.2 bis 19.8.6 zeigen wir an einem etwas größeren Beispiel, wie die vorgestellten Black-Box-Techniken – Eingabe- und Ausgabeüberdeckung mit Hilfe von Äquivalenzklassen und die Funktionsüberdeckung – angewendet werden können.
Endliche Automaten werden in der Informatik eingesetzt, um das Verhalten von Systemen zu spezifizieren. Formal ist ein endlicher Automat ein gerichteter Graph, dessen Knoten und Kanten Zustände und Zustandsübergänge darstellen. Zu jedem Zeitpunkt ist der Automat in genau einem Zustand; Eingaben lösen Zustandsübergänge aus.
Der Vorteil eines Zustandsautomaten ist seine Anschaulichkeit. Sowohl die Entwickler als auch die Benutzer haben intuitiv eine Vorstellung davon, welche Zustände das System durchläuft.
Der Zustand ist das Gedächtnis des Systems. Je nach Zustand kann eine Eingabe (ein Ereignis) unterschiedliche Effekte haben; eine Aktion kann ausgelöst werden, der Zustand kann sich ändern (Zustandsübergang). Zu Beginn ist der Automat im Startzustand; meist ist mindestens ein Zustand als Endzustand ausgezeichnet.
Betrachten wir als Beispiel eine sehr einfache Bankkontenverwaltung. Ein Konto kann eröffnet und geschlossen werden, es kann ein Betrag (b) gutgeschrieben und abgebucht werden, und der Kontostand kann abgefragt werden. Für jedes Konto sind der Kontostand (kst) und ein Überziehungslimit (limit) gespeichert. Ein Konto kann gesperrt werden, dann sind Gutschriften und Kontostandabfragen, aber keine Belastungen möglich; es kann, wenn es gesperrt ist, wieder entsperrt werden. Abbildung 19–5 zeigt einen Zustandsautomaten, der ein solches Konto modelliert.
Abb. 19–5 Beispiel für einen endlichen Automaten
Zustandsautomaten werden im Software Engineering meist nur unvollständig beschrieben. Es gibt also eine Reihe von Ereignis-Zustand-Paaren, die nicht im Automaten aufgeführt sind. So wird im gezeigten Automat im Zustand »überzogen« das Ereignis »schließen« nicht betrachtet. Ein nicht spezifiziertes EreignisZustand-Paar bedeutet, dass das Ereignis in diesem Zustand nicht erlaubt oder unmöglich ist.
Bei einem zustandsbasierten Test muss jeder Zustand mindestens einmal erreicht werden (Zustandsüberdeckung). Höhere Testgüte erzielt man, wenn von jedem Zustand aus in jeden möglichen Folgezustand gewechselt wird (im Beispiel in Abb. 19–5 etwa von Ü nach Ü, O und G-; Zustandspaarüberdeckung). Die Testgüte wird weiter gesteigert, wenn für jeden Zustandsübergang auch jedes Ereignis, das zum Zustandsübergang führt (im Beispiel die Ereignisse h, i und j für den Übergang von Ü nach Ü), im Test wirksam wird (Transitionsüberdeckung).
Um die Testfälle für den zustandsbasierten Test systematisch auszuwählen, werden in Binder (2000) sogenannte Roundtrip-Folgen durch den Zustandsautomaten bestimmt. Eine Roundtrip-Folge beginnt immer im Startzustand und endet in einem Endzustand oder in einem Zustand, der bereits in dieser oder einer anderen Roundtrip-Folge enthalten und dort nicht der letzte Zustand der Folge ist. Wenn der Test aller Roundtrip-Folgen keine Fehler zeigt, dann stimmt das Verhalten des Prüflings mit dem im Zustandsautomaten spezifizierten Verhalten überein. Der Test der Roundtrip-Folgen findet alle Zustandsübergangsfehler und deckt ggf. auf, dass Zustände fehlen.
Um die Roundtrip-Folgen zu ermitteln, wird aus dem Zustandsautomaten ein Zustandsübergangsbaum abgeleitet. Dies geschieht nach folgendem Algorithmus:
1. Erzeuge den Wurzelknoten (Stufe 0 des Baums) und bezeichne ihn mit dem Startzustand. Setze die aktuelle Stufe k auf 0.
2. Bearbeite jeden Knoten der Stufe k:
• Falls der Zustand, mit dem dieser Knoten bezeichnet ist, ein Endzustand ist oder bereits im Baum vorkommt, markiere den aktuellen Knoten als »terminal«.
• Andernfalls erzeuge für jeden ausgehenden Zustandsübergang und jede Bedingung eine neue Kante, die den aktuellen Knoten mit einem neuen Knoten auf Stufe k+1 des Baums verbindet. Bezeichne die neue Kante mit Ereignis/Bedingung/Aktion und den neuen Knoten mit dem Folgezustand.
3. Falls es Knoten auf Stufe k+1 gibt, inkrementiere k und wiederhole Schritt 2.
In unserem Beispiel wird aus dem Automaten in Abbildung 19–5 der Zustandsübergangsbaum in Abbildung 19–6 abgeleitet. (Abhängig von der Reihenfolge, in der die Knoten expandiert werden, entstehen unterschiedliche Übergangsbäume, die aber logisch äquivalent sind.)
Abb. 19–6 Zustandsübergangsbaum
Um das durch den Zustandsautomaten spezifizierte Normalverhalten zu prüfen, müssen in unserem Beispiel Testfälle für 15 Roundtrip-Folgen entwickelt werden, je einer für jeden Weg von der Wurzel zu einem Blatt. Die Testfälle der folgenden Tabelle prüfen die drei Roundtrip-Folgen, die an den Blättern 7, 8 und 15 enden.
Folge |
Startzustand, Ereignisse und Zwischenzustände |
Endzustand, Soll-Wert kst |
7 |
S, eröffnen → O, abbuchen (100) → Ü, abbuchen (200) |
Ü, -300 |
8 |
S, eröffnen → O, abbuchen (100) → Ü, gutschreiben (500) |
O, 400 |
15 |
S, eröffnen → O, abbuchen (100) → Ü, sperren → G-, gutschreiben (300) |
G+, 200 |
Auch das Verhalten im Fehlerfall muss geprüft werden. Was soll beispielsweise passieren, wenn ein offenes Konto entsperrt wird oder der Kontostand durch eine Abbuchung unter das Überziehungslimit sinkt? Dazu benötigen wir für jede mögliche Kombination aus Zustand und Ereignis eine Angabe, wie der Automat reagieren soll. Dies kann recht übersichtlich in Tabellenform dargestellt werden. Die (evtl. durch Vorbedingungen erweiterten) Ereignisse bilden die Zeilen der Tabelle, die Zustände die Spalten. Die Tabelle wird gefüllt, indem für jedes Ereignis angegeben wird, ob es im betrachteten Zustand
erlaubt (gültig) ist (√),
ein Fehler ist, der behandelt werden muss (⊗),
ausgeschlossen ist, da die Vorbedingung nicht erfüllt sein kann (-).
Für unser Beispiel entsteht damit die folgende Tabelle.
Ereignis / Vorbedingung |
Reaktion im Zustand |
|||
O |
Ü |
G+ |
G- |
|
kontostand |
√ |
√ |
√ |
√ |
sperren |
√ |
√ |
⊗ |
⊗ |
entsperren |
⊗ |
⊗ |
√ |
√ |
schließen [kst = 0] |
√ |
- |
⊗ |
- |
schließen [kst <> 0] |
⊗ |
⊗ |
⊗ |
⊗ |
gutschreiben [kst + b ≥ 0] |
√ |
√ |
√ |
√ |
gutschreiben [kst + b < 0] |
- |
√ |
- |
√ |
abbuchen [kst - b ≥ 0] |
√ |
- |
⊗ |
- |
abbuchen [limit ≤ kst-b < 0] |
√ |
√ |
⊗ |
⊗ |
abbuchen [kst - b < limit] |
⊗ |
⊗ |
⊗ |
⊗ |
Nun werden für alle ungültigen Ereignis-Zustand-Paare (markiert durch Ä) Testfälle definiert. Beispielsweise prüft der folgende Test zur letzten Zeile der Tabelle die Reaktion auf einen Versuch, ein bereits überzogenes Konto stärker zu belasten, als das Überziehungslimit (hier vorgegeben mit -1000) erlaubt.
Startzustand, Ereignisse und Zwischenzustände |
Erwartete Aktion |
Endzustand, Soll-Wert kst |
S, eröffnen → O, abbuchen(200) → Ü, abbuchen(900) |
Fehlermeldung |
Ü, -200 |
Jeder Prüfling, der sich als endlicher Automat beschreiben lässt, sei es eine Klasse, ein Modul oder das gesamte System, kann durch einen zustandsbasierten Test geprüft werden. Der Aufwand, um den Automaten, den Zustandsübergangsbaum, die Ereignis-Zustand-Tabelle und die daraus abgeleiteten Testfälle zu erstellen, ist schon bei kleineren Automaten erheblich.
Nach jedem Schritt des zustandsbasierten Tests muss der erreichte Zustand kontrolliert werden. Dazu dienen spezielle Operationen, die man als Zustandsreporter bezeichnet. Falls es sie nicht bereits aus anderen Gründen gibt, müssen sie (als dauerhafte Erweiterung) realisiert werden; dazu muss der Quellcode des Prüflings verfügbar sein.
Testdaten werden aus der Spezifikation abgeleitet. Das gilt auch, wenn zur Spezifikation Anwendungsfälle verwendet werden. Dazu müssen neben den Anwendungsfalldiagrammen, die in UML notiert werden können, auch die natürlichsprachlichen Beschreibungen der Anwendungsfälle vorliegen.
Das Anwendungsfalldiagramm stellt alle Anwendungsfälle und ihre formalen Beziehungen dar. Andere wichtige Informationen – wie die Vor- und Nachbedingungen der Anwendungsfälle – werden im Diagramm nicht dargestellt. Diese Informationen sowie die einzelnen Interaktionsschritte sind in der detaillierten Beschreibung der Anwendungsfälle enthalten. Anwendungsfälle behandeln wir ausführlich in Abschnitt 16.7.3.
Das Überdeckungskriterium orientiert sich hier an den Anwendungsfällen; die Implementierung jedes Anwendungsfalls sollte in mindestens einem Test geprüft werden. Ebenso sollten alle Abhängigkeiten zwischen Anwendungsfällen (modelliert durch include- und extend-Beziehungen) im Test geprüft werden. Beim Test der einzelnen Anwendungsfälle stützen wir uns auf die detaillierten Beschreibungen.
In Abbildung 19–7 sind der Normalablauf und (verkürzt) die Sonderfälle aus dem in Abbildung 16–6 auf Seite 388 vorgestellten Anwendungsfall »Authentifizieren« wiedergegeben.
Normalablauf |
1. Der Kunde führt eine Karte ein 2. Der BA42 liest d. Karte und sendet d. Daten z. Prüfung ans Banksystem 3. Das Banksystem prüft, ob die Karte gültig ist 4. Der BA42 zeigt die Aufforderung zur PIN-Eingabe 5. Der Kunde gibt die PIN ein 6. Der BA42 liest die PIN und sendet sie zur Prüfung an das Banksystem 7. Das Banksystem prüft die PIN 8. Der BA42 akzeptiert den Kunden und zeigt das Hauptmenü |
Sonderfälle |
2a Die Karte kann nicht gelesen werden |
Abb. 19–7 Anwendungsfall »Authentifizieren« (Auszug)
Auf Basis der Beschreibung des Anwendungsfalls können wir für jeden Ablauf einen Testfall entwickeln. Bei komplexen Interaktionen mit vielen Sonderfällen ist es jedoch angebracht, die Beschreibung vorher zu formalisieren. Wenn wir dazu eine grafische Notation verwenden, erhalten wir eine übersichtlichere Darstellung, und wir erkennen Fehler und Lücken leichter. Ryser (2003) schlägt für diesen Zweck Zustandsautomaten vor. Wir folgen Sneed und Winter (2001), die Aktivitätsdiagramme benutzen.
Ein Anwendungsfall wird komplett, mit Normalablauf und Sonderfällen, in ein Aktivitätsdiagramm übersetzt. Wir modellieren zuerst den Normalablauf und fügen dann Schritt für Schritt die Sonderfälle hinzu. Aus dem gezeigten Anwendungsfall können wir beispielsweise das in Abbildung 19–8 dargestellte Aktivitätsdiagramm entwickeln (der Normalablauf ist farblich hinterlegt, die verschiedenen Wege sind nummeriert).
Abb. 19–8 Aktivitätsdiagramm des Anwendungsfalls »Authentifizieren«
Bei diesem Umsetzungsschritt stellt man möglicherweise fest, dass das im Anwendungsfall modellierte Verhalten unvollständig ist. Auch können einzelne Schritte zu grob formuliert sein, sie müssen dann detailliert werden. So muss der in unserer Use-Case-Beschreibung enthaltene Schritt 2 »Der BA42 liest die Karte und sendet die Daten zur Prüfung an das Banksystem« in die zwei Schritte »Karte lesen« und »Daten senden« aufgeteilt werden, damit die dazu angegebenen Sonderfälle präzise im Aktivitätsdiagramm modelliert werden können. Fehlende Abläufe und neue oder geänderte Schritte werden sowohl in das Aktivitätsdiagramm als auch in die Use-Case-Beschreibung integriert. Im Beispiel fehlt auch die Angabe, wie eine PIN genau aufgebaut ist. Diese Information wird im Test und natürlich auch für die Entwicklung benötigt, sie muss spezifiziert werden.
Beim Test auf der Basis von Anwendungsfällen wird jeder beschriebene Ablauf, also jeder Weg durch das Aktivitätsdiagramm, wenigstens einmal geprüft (Ablaufüberdeckung). Wir beginnen mit dem Normalablauf und prüfen dann die Sonderfälle. Auf diese Weise werden oft Fehler bei der Programmierung der Anwendungsfälle entdeckt, falsch oder unvollständig implementierte Anwendungsfälle und Abhängigkeiten zwischen den Anwendungsfällen, die nicht beachtet wurden.
Wie die Abbildung 19–8 zeigt, gibt es elf Wege durch das Aktivitätsdiagramm und damit elf Testfälle, um alle Wege zu durchlaufen. Diese Testfälle sind in der folgenden Tabelle 19–3 angegeben. Die Eingaben in eckigen Klammern erfolgen durch Mausklick, Knopfdruck, Menüauswahl o. Ä.; Reaktionen des BA42, die in einer Zelle stehen, geschehen gleichzeitig.
Weg |
Anfangszustand |
Eingabe/Aktion |
Soll-Reaktion Der BA42 ... |
1 |
BA42-Karte mit PIN 1234 |
Karte einführen |
meldet »PIN eingeben« |
1234 [OK] |
zeigt das Hauptmenü an |
||
2 |
Magnetstreifen der Karte ist beschädigt |
Karte einführen |
meldet »Karte nicht lesbar« (4 s) |
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
3 |
Karte einer anderen Bank |
Karte einführen |
meldet »Karte nicht akzeptiert« (4 s) |
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
4 |
BA42-Karte mit PIN 1234; Banksystem offline |
Karte einführen |
meldet »Banksystem nicht erreichbar« (4 s) |
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
5 |
gesperrte Karte |
Karte einführen |
meldet »Karte ungültig oder gesperrt« (4 s); |
|
zieht die Karte ein |
||
zeigt die Willkommen-Botschaft |
|||
6 |
BA42-Karte |
Karte einführen |
meldet »PIN eingeben« |
Abbruch [OK] |
meldet »Vorgang wird abgebrochen« (2 s) |
||
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
BA42-Karte |
Karte einführen |
meldet »PIN eingeben« |
|
> 5s warten |
meldet »Keine Reaktion, Abbruch« (2 s) |
||
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
8 |
BA42-Karte |
Karte einführen |
meldet »PIN eingeben« |
Banksystem offline schalten |
|
||
3456 [OK] |
meldet »Banksystem nicht erreichbar« (4 s) |
||
|
gibt die Karte zurück |
||
zeigt die Willkommen-Botschaft |
|||
9 |
BA42-Karte mit PIN 1234 |
Karte einführen |
meldet »PIN eingeben« |
1212 [OK] |
meldet »Falsche PIN« (4 s), »PIN eingeben« |
||
1234 [OK] |
akzeptiert den Kunden; zeigt das Hauptmenu |
||
10 |
BA42-Karte mit PIN 1234 |
Karte einführen |
meldet »PIN eingeben« |
1212 [OK] |
meldet »Falsche PIN«(4 s), »PIN eingeben« |
||
1111 [OK] |
meldet »Falsche PIN«(4 s), »PIN eingeben« |
||
1234 [OK] |
akzeptiert den Kunden; zeigt das Hauptmenü |
||
11 |
BA42-Karte mit PIN 1234 |
Karte einführen |
meldet »PIN eingeben« |
1212 [OK] |
meldet »Falsche PIN« (4 s), »PIN eingeben« |
||
1111 [OK] |
meldet »Falsche PIN« (4 s), »PIN eingeben« |
||
1235 [OK] |
meldet »PIN 3x falsch« (5 s); |
||
|
zieht die Karte ein |
||
zeigt die Willkommen-Botschaft |
Tab. 19–3 Testfälle für den Anwendungsfall »Authentifizieren«
Das Wort »Glass Box« lässt erkennen, dass der Programmcode bei dieser Art des Tests sichtbar ist; das Programm ist wie eine Maschine im gläsernen Gehäuse, deren Arbeit man von außen nicht direkt beeinflussen, aber beobachten kann. In jedem Glass-Box-Test geht es darum, beim Testen bestimmte, auf den Code bezogene Überdeckungen zu erreichen. Beispielsweise kann das Ziel sein, eine Anweisungsüberdeckung von 80 % zu erzielen. Dann wird getestet, bis 80 % der ausführbaren Befehle im Programm mindestens einmal ausgeführt worden sind.
Um festzustellen, ob dieses Ziel erreicht ist, braucht man unbedingt ein Werkzeug, das den Prüfling instrumentiert, d. h. mit zusätzlichen Anweisungen ausrüstet, die die notwendigen Zählungen realisieren. Die Zählerstände werden über beliebig viele Programmläufe akkumuliert und auf Wunsch in einer anschaulichen Form angezeigt; beispielsweise können alle Teile des Programms, die noch nie ausgeführt wurden, mit einer speziellen Farbe markiert sein. Dann kann man untersuchen, wie Testfälle beschaffen sein müssten, um diese »weißen Flecken auf der Landkarte« zu beseitigen.
Zwei sehr populäre, aber falsche Vorurteile über den Glass-Box-Test wollen wir von Anfang an bekämpfen:
Für einen Glass-Box-Test muss man den Code lesen und verstehen.
Falsch!
Dann wäre der Glass-Box-Test selbst kleinerer Programme (einige tausend Zeilen) aus Aufwandsgründen nicht zu bewältigen. Man muss nur diejenigen Stellen analysieren, die bislang noch nicht zum gewählten Überdeckungsmaß beitragen. Meist reicht es aus, den Zweck des betreffenden Code-Abschnitts zu erkennen.
Beim Glass-Box-Test kommt es nur auf die Messung der Überdeckung an.
Falsch!
Bei jedem Test ist der Soll-Ist-Vergleich das Wichtigste, auch beim Glass-Box-Test. Denn es nützt gar nichts, wenn das Programm zwar mit allen erdenklichen Überdeckungsmaßen zu 100 % getestet wurde, aber falsche Resultate liefert.
Da wir im Glass-Box-Test mit instrumentierten Programmen arbeiten, ist die Regel verletzt, dass wir das Programm für den Test nicht verändern dürfen (Abschnitt 19.3.3). Darum müssen die Eingaben des Glass-Box-Tests auch mit dem nicht instrumentierten Programm verarbeitet werden. Weichen die Ergebnisse der beiden Tests (mit instrumentiertem und unverändertem Code) voneinander ab, dann hat das unterschiedliche Zeitverhalten oder die unterschiedliche Speicherbelegung der beiden Varianten Einfluss auf das Resultat (was meist einen Fehler anzeigt). Wir können den Glass-Box-Test natürlich auch komplett durchführen und am Ende ohne Instrumentierung des Programms wiederholen, um zu prüfen, ob die Instrumentierung die Resultate verändert hatte.
Bei einem Glass-Box-Test kann Anweisungs-, Zweig- oder Termüberdeckung angestrebt werden. Liggesmeyer (2002) behandelt in seinem Buch ausführlich auch Glass-Box-Testverfahren, die einigen analytischen Aufwand und spezielle Werkzeuge erfordern. Solche Verfahren kommen, wenn überhaupt, nur in sicherheitskritischen Anwendungen zum Einsatz.
Die Anweisungsüberdeckung (Abschnitt 19.6.2) ist erreicht, wenn alle Anweisungen des Programms ausgeführt wurden.
Die Zweigüberdeckung (Abschnitt 19.6.3) ist erreicht, wenn bei allen Verzweigungen des Programms alle möglichen Wege (Zweige) durchlaufen wurden.
Die Termüberdeckung (Abschnitt 19.6.4) ist erreicht, wenn jeder logische Term, der den Ablauf in einer Verzweigung steuert, mit beiden möglichen Werten (TRUE und FALSE) wirksam geworden ist.
Diese Überdeckungen können mit Werkzeugen gemessen werden: Die Anweisungen, Zweige und Terme sind leicht zu erkennen und zu zählen, und sie können jeweils auch im Quellprogramm durch Zählanweisungen »instrumentiert« werden. Darum gibt es für alle gängigen Programmiersprachen Werkzeuge für die Anweisungs- und Zweigüberdeckung. Für die Termüberdeckung wären sie ebenso möglich.
Die Termüberdeckung impliziert die Zweigüberdeckung und diese – wenn das Programm keine pathologischen Eigenschaften (»toten Code«) aufweist – die Anweisungsüberdeckung.
In der Literatur wird meist auch die Pfadüberdeckung behandelt. Sie spielt aber aus Gründen, die in Abschnitt 19.6.6 erläutert werden, keine Rolle.
Die unterschiedlichen Überdeckungsarten können mit Hilfe eines Ablaufgraphen anschaulich dargestellt werden. Ein Ablaufgraph entspricht einem Flussdiagramm, bei dem alle unverzweigten Programmsequenzen durch Abstraktion zu einer einzigen Anweisung reduziert werden. Nur die Verzweigungen (IF, CASE, WHILE etc.) und Zusammenführungen des Ablaufs bleiben erhalten.
Als Beispiel für die nachfolgenden Erläuterungen der Auswahlkriterien dient die einfache Prozedur BerechneBonus, die auf Basis der Parameter saldo, sparRate und des ursprünglichen Bonuswerts (Parameter bonus) den neuen Bonuswert berechnet. Der Code der Prozedur und ihr Ablaufgraph sind in Abbildung 19–9 dargestellt. Eine kompliziertere Berechnung des Bonus in vielen Schritten hätte auf den Ablaufgraphen keine Auswirkung.
Da wir hier nur den Code betrachten, fehlt eine Spezifikation für das Beispiel; darum wirkt die Angabe der Soll-Resultate künstlich. In einem echten Test müssen die Soll-Resultate natürlich aus der Spezifikation abgeleitet werden.
Abb. 19–9 Prozedur BerechneBonus mit Ablaufgraph
Für die Anweisungsüberdeckung wird wie für alle Überdeckungsmaße nur der ausführbare Code betrachtet. Wenn der Prüfling keinen »toten« Code enthält, also Code, der nicht erreichbar ist, kann eine vollständige Anweisungsüberdeckung erzielt werden. In unserem Beispiel gelingt das durch einen einzigen Testfall.
Testfall |
Eingaben |
Anweisungen |
Soll-Resultat bonus |
|||||
saldo |
sparRate |
bonus |
B1 |
Sa |
B2 |
Sc |
||
F1 |
4000 |
150 |
20 |
X |
X |
X |
X |
200 |
In der Praxis ist es schwierig, eine Anweisungsüberdeckung von mehr als 80 % bis 85 % zu erreichen. Einer höheren Überdeckung stehen zwei Schwierigkeiten entgegen:
Programme enthalten defensiven Code, der nur ausgeführt wird, wenn in anderen Programmteilen Fehler auftreten. Formal gesehen handelt es sich dabei um toten Code; praktisch ist er sinnvoll, weil er dazu beiträgt, Wartungsfehler zu erkennen.
In Programmen müssen auch sehr ungewöhnliche Fälle abgefangen und behandelt werden. Oft ist es sehr schwierig, solche Fälle im Test zu simulieren. Das gilt besonders für Abhängigkeiten von Datum und Uhrzeit; es ist außerordentlich riskant und darum nicht zu empfehlen, die Uhr des Systems für einen Test zu verstellen. Darum kann man Sonderbehandlungen für bestimmte Zeiten (z. B. für die Zeit vom 24. Dezember bis 1. Januar) kaum testen.
Entsprechende Einschränkungen gelten natürlich auch für alle anderen Überdeckungsmaße.
Bei der vollständigen Zweigüberdeckung wird jeder Zweig, d. h. jede Verbindung zwischen den Knoten des Ablaufgraphen, mindestens mit einem Testfall durchlaufen.
Die Zweigüberdeckung ist ein strengeres Kriterium als die Anweisungsüberdeckung, wenn der Prüfling leere Zweige enthält, also Zweige, die keine Anweisungen haben. Sie entstehen durch bedingte Anweisungen (IF-THEN ohne ELSEZweig) und durch Zweige einer CASE-Anweisung, die keine Aktionen enthalten.
Mit dem Testfall F1 erreichen wir in unserem Beispiel die leeren Zweige B und D nicht, erzielen also nur eine Zweigüberdeckung von 50 %. Durch einen zweiten Testfall können wir die volle Zweigüberdeckung erreichen.
Eingaben |
Zweige |
Soll-Resultat bonus |
||||||
saldo |
sparRate |
bonus |
A |
B |
C |
D |
||
F1 |
4000 |
150 |
20 |
X |
|
X |
|
200 |
F2 |
100 |
20 |
2 |
|
X |
|
X |
2 |
Durch eine vollständige Zweigüberdeckung stellen wir sicher, dass jeder Ausgang einer Verzweigung durchlaufen wurde. Wir wissen aber bei komplexen Bedingungen nicht, ob die einzelnen logischen Bedingungen wirksam geworden sind. Betrachten wir als Beispiel eine Verzweigung:
if (A AND (B OR (C AND D))) OR E then ...
Darin stehen A, B, C, D und E für einfache (atomare) logische Terme, also Variablen, Funktionen oder Ausdrücke des Typs Boolean. Die Zeile könnte also beispielsweise lauten:
if (Anfang AND (a=b OR (a>0 AND b>0))) OR Sonderfall then ...
Die vollständige Zweigüberdeckung ist bereits erreicht, wenn A (=Anfang) immer FALSE, E (=Sonderfall) einmal FALSE und einmal TRUE war. Wir haben also trotz Zweigüberdeckung keinen Hinweis darauf, ob alle einzelnen Terme sich so auswirken, wie es der Programmierer beabsichtigt hat. Um das zu prüfen, brauchen wir die Termüberdeckung.
Vollständige Termüberdeckung ist erreicht, wenn jeder Term mindestens einmal den gesamten Ausdruck FALSE und einmal TRUE gemacht hat.
Im Beispiel oben gilt das nur für den Term E. Ist A FALSE, so ist der Wert des gesamten Ausdrucks gleich dem Wert von E. E steuert in diesem Fall den Ablauf, E ist wirksam.
Wir definieren also: Ein Term ist dann wirksam, wenn die Änderung seines Wertes (von TRUE nach FALSE oder umgekehrt) dazu führt, dass sich auch der Wert des gesamten Ausdrucks ändert.
Das kann man für eine bestimmte Belegung der übrigen Terme einfach testen, indem man den Ausdruck zweimal auswertet (mit TRUE und FALSE für den betrachteten Term). Ändert sich dadurch der Wert des gesamten Ausdrucks, so ist dieser Term wirksam. Man kann die Wirksamkeit aber auch in einem Zuge für alle Terme des Ausdrucks feststellen, indem man den Baum, der den Ausdruck repräsentiert (Abb. 19–10), analysiert. Dabei gilt:
Für eine bestimmte Belegung der atomaren Terme hat jeder Knoten den Wert
TRUE oder FALSE und die Eigenschaft »wirksam« (w) oder »unwirksam« (u).
Die Wurzel des Baumes (d. h. der logische Ausdruck insgesamt) ist wirksam.
Ein Unterknoten eines UND-Knotens ist genau dann unwirksam, wenn der
UND-Knoten unwirksam oder dessen anderer Unterknoten FALSE ist.
Ein Unterknoten eines ODER-Knotens ist genau dann unwirksam, wenn der
ODER-Knoten unwirksam oder dessen anderer Unterknoten TRUE ist.
Eine Negation (NOT) kehrt nur den Wert um, hat aber im Übrigen für die nachfolgende Betrachtung keine Bedeutung.
Abb. 19–10 Baumdarstellung des logischen Ausdrucks (A AND (B OR (C AND D))) OR E
Die Erklärung dazu ist einfach: Nur ein Wert, der sich »nach oben« auswirkt, ist im Sinne der Definition wirksam, nimmt also Einfluss auf den Ablauf des Programms.
In vielen Sprachen gibt es Operatoren mit Shortcut-Semantik (z. B. in ADA and then/or else, in JAVA &&/||). Dabei wird der rechte Knoten nicht ausgewertet, wenn der linke allein das Ergebnis bestimmt. In diesem Fall ist der linke unabhängig vom rechten so wirksam wie der übergeordnete Knoten. Nehmen wir an, dass im Beispiel K2 einen UND-Operator mit Shortcut-Semantik enthält und wirksam ist. Dann ist A mit dem Wert FALSE unabhängig von K3 wirksam.
Wir müssen den Baum für jeden Testfall also zweimal traversieren, einmal in Nachordnung, um die atomaren Terme mit Werten zu belegen und jedem UNDbzw. ODER-Knoten den Wert TRUE oder FALSE zuzuordnen, dann in Vorordnung, um jeden Knoten als wirksam oder unwirksam zu erkennen. So stellen wir für jeden atomaren Term fest, ob er wirksam ist und damit zur Termüberdeckung beiträgt.
Praktisch muss man also für jeden atomaren Term M zwei Wahrheitswerte MF und MT speichern; wird in einem Test festgestellt, dass M mit dem Wert FALSE (TRUE) wirksam ist, so wird MF (MT) auf TRUE gesetzt. Am Ende der Tests kann die Termüberdeckung als Anteil der auf TRUE gesetzten MF und MT abgelesen werden.
Wir können damit definieren: Ein Test erreicht die Termüberdeckung t, wenn bei den n atomaren Termen, die im Programm enthalten sind, t · 2n verschiedene wirksame Terme festgestellt wurden.
Betrachten wir als Beispiel wieder den oben verwendeten Ausdruck:
if (A AND (B OR (C AND D))) OR E then ...
Vier Testfälle (beschrieben durch die jeweilige Belegung von A bis E) führen zu folgenden Belegungen und Überdeckungen (vgl. Abb. 19–10):
Testfall |
Aspekt |
A |
B |
C |
D |
E |
K4 |
K3 |
K2 |
K1 |
wirksame Terme |
1 |
Belegung |
T |
T |
F |
T |
F |
F |
T |
T |
T |
AT,BT |
Wirksamkeit |
T |
T |
F |
F |
F |
F |
T |
T |
T |
(alle neu) |
|
2 |
Belegung |
T |
F |
F |
T |
F |
F |
F |
F |
F |
BF,CF,EF |
Wirksamkeit |
F |
T |
T |
F |
T |
T |
T |
T |
T |
(alle neu) |
|
3 |
Belegung |
T |
F |
T |
T |
F |
T |
T |
T |
T |
AT,CT,DT |
Wirksamkeit |
T |
F |
T |
T |
F |
T |
T |
T |
T |
(neu bis auf AT) |
|
4 |
Belegung |
F |
F |
T |
F |
T |
F |
F |
F |
T |
ET |
Wirksamkeit |
F |
F |
F |
F |
T |
F |
F |
F |
T |
(neu) |
Jeder in einem Testfall wirksame atomare Term ergibt einen Beitrag 1 von 2 · 5, also 10 %. Testfall 1 erzielt 20 % Termüberdeckung, Testfall 2 30 %, Testfall 3 ebenfalls 30 %. AT ist sowohl durch den Testfall 1 als auch durch den Testfall 3 abgedeckt. Wir erzielen also mit den Tests 1 bis 3 insgesamt 70 % Termüberdeckung.
Drei Fälle fehlen uns noch, nämlich AF, DF und ET. Da es in den Tests 1 bis 3 gar keine entsprechende Belegung für A, D und E gab, setzen wir im Testfall 4 A und D auf FALSE, E auf TRUE. Das bringt uns aber nur einen Treffer, ET, weil in dieser Belegung A und D unwirksam sind. Allgemein kann man leicht (durch Induktion) zeigen, dass für einen Ausdruck mit n atomaren Termen mindestens n+1 Fälle erforderlich sind, um vollständige Termüberdeckung zu erreichen.
Wir haben im Beispiel nur eine einzige Verzweigung betrachtet. In einem echten Programm mit mehreren Verzweigungen sinkt natürlich das Gewicht der einzelnen Verzweigungen. Will man mit der Termüberdeckung speziell die Verzweigungen mit komplexen Bedingungen, also mindestens zwei atomaren Termen, erfassen, zählt man nur diese (»Multitermüberdeckung«). Zählt man dagegen auch die einfachen Bedingungen mit (»if a < 0 then ...«), so bekommt man die Term-/Zweigüberdeckung; diese entspricht, wenn es keine Multiterme gibt, exakt der Zweigüberdeckung.
Diese Entsprechung zwischen Zweig- und Termüberdeckung gilt aber nicht, wenn das Programm Mehrfachverzweigungen (SWITCH- oder CASE-Anweisungen) enthält. Sie werden in der Zweigüberdeckung mitgezählt (bei n Ausgängen mit dem Gewicht n), in der Termüberdeckung aber nicht, denn sie enthalten keine logischen Ausdrücke.
Enthält der Proband keine logischen Ausdrücke mit mehr als einem Term, dann wird die Termüberdeckung zur Zweigüberdeckung.
In der Literatur wird eine der Termüberdeckung sehr ähnliche Überdeckung als modifizierte Bedingungs-/Entscheidungsüberdeckung (modified condition/decision coverage) bezeichnet (Chilenski, Miller, 1994). MC/DC, ein wichtiges Kriterium bei der Prüfung von Software für die Luftfahrt, unterscheidet sich von der Termüberdeckung in folgenden Punkten:
MC/DC ist nur für die vollständige Überdeckung definiert, nicht für teilweise Überdeckungen.
Wie bei der Termüberdeckung muss jeder atomare Term sowohl mit dem Wert TRUE als auch mit dem Wert FALSE wirksam geworden sein. Bei MC/ DC ist zudem gefordert, dass bei diesen beiden Tests alle übrigen atomaren Terme unverändert geblieben sind. Bei der Termüberdeckung genügt es, wenn der betrachtete Term in beiden Fällen wirksam war.
Die Shortcut-Semantik bei der Auswertung logischer Ausdrücke (also der Abbruch einer Auswertung, wenn das Resultat feststeht) erfordert zusätzliche Festlegungen oder Änderungen in Richtung Termüberdeckung.
Unsere Termüberdeckung ist in diesen Punkten also etwas großzügiger (und damit einfacher) als die modifizierte Bedingungs-/Entscheidungsüberdeckung.
Ein grundsätzliches Problem entsteht durch die Ausnahmebehandlung (Exception Handling). Die Anweisungüberdeckung erfordert nur, dass alle Exception Handlers ausgeführt werden. Weniger klar ist die Interpretation der Zweigüberdeckung: Wird die Ausnahme im Programm explizit ausgelöst, dann erfordert die Zweigüberdeckung, dass jede Auslösung auch getestet wird. Erfolgt die Auslösung aber an irgendeiner Stelle (etwa im Fall eines arithmetischen Überlaufs), so läuft diese Forderung ins Leere.
Wenn es zwischen verschiedenen Termen eine inhaltliche Kopplung gibt (weil auf dieselben Variablen Bezug genommen wird), kann es unmöglich werden, die Termüberdeckung zu erreichen. Wir haben eine ähnliche Situation vor uns wie mit der Anweisungsüberdeckung bei totem Code: Das Programm kann und sollte vereinfacht werden.
Alle Überdeckungen, die im Glass-Box-Test gemessen werden können, betreffen elementare Bestandteile der Programme: Anweisungen, Verzweigungen und Bedingungen in den Verzweigungen; Pfade passen dazu nicht. Als Pfad bezeichnet man die vollständige Sequenz der Schritte von Anfang bis Ende einer Programmausführung; jede Ausführung eines Programms folgt genau einem Pfad. Die Pfadüberdeckung ist der Anteil der ausgeführten Pfade an der Gesamtzahl der Pfade.
Enthält ein Programm n Verzweigungen, aber keine Schleifen, dann gibt es darin höchstens 2 n Pfade. Durch Schleifen, die Verzweigungen enthalten, explodiert die Zahl der Pfade. Gibt es im Programm eine WHILE-Schleife, so ist die Zahl der Pfade im Allgemeinen nicht definiert, weil die Zahl der Iterationen nicht festgelegt ist; aber auch eine FOR-Schleife, die genau einhundertmal durchlaufen wird und eine Verzweigung enthält, kann bis zu 2 100 (> 1030) Pfade enthalten.
Allerdings ist in der Regel nur ein kleiner Teil dieser Pfade tatsächlich erreichbar, denn die Bedingungen in den Verzweigungen und Schleifen sind meist voneinander abhängig. Hängt in der oben beschriebenen FOR-Schleife die Verzweigung im Rumpf von der Laufvariablen i ab (z. B. davon, ob i eine gerade Zahl ist), dann gibt es nicht 2 100 Pfade, sondern nur einen einzigen. Da die Frage, ob ein bestimmter Pfad erreichbar ist oder nicht, zu den nicht entscheidbaren Problemen gehört, haben wir keine Möglichkeit festzustellen, auf welche Gesamtheit die Pfadüberdeckung bezogen werden muss. Sie ist also nur in Spezialfällen messbar.
Wenn wir Äquivalenzklassen bilden, fragen wir uns, welche Eingabedaten das genau gleiche Verhalten des Programms auslösen, d. h. denselben Pfad verwenden. Darum ist die Ausführung jedes möglichen Pfades gleichbedeutend mit dem Test eines Repräsentanten aus jeder Äquivalenzklasse. Da wir die Pfadüberdeckung nicht messen können, bringt uns dieser Begriff keinen Vorteil.
Nachdem sich gezeigt hat, dass die Pfadüberdeckung für die Praxis untauglich ist, stellt sich die Frage, ob das Kriterium so verändert werden kann, dass es anwendbar und nützlich wird. Man kann sich auf ein viel bescheideneres Überdeckungskriterium zurückziehen, das auf einer pragmatischen Behandlung der Schleifen beruht. Beizer (1983) empfiehlt, Schleifen wie folgt zu testen:
1. Umgehung der Schleife (Rumpf wird nicht ausgeführt)
2. eine Iteration (Rumpf wird genau einmal ausgeführt)
3. zwei Iterationen (Rumpf wird genau zweimal ausgeführt)
4. eine typische Anzahl von Iterationen
5. maximale Anzahl der Iterationen
Natürlich liegt hier eine Heuristik zu Grunde, die nicht zwingend sinnvoll ist. In vielen Fällen sind auch gar nicht alle fünf Möglichkeiten gegeben, beispielsweise dann nicht, wenn es sich um eine Laufschleife mit fester Wiederholungszahl handelt.
Auf dieser Grundlage wurde die sogenannte einfache Pfadüberdeckung definiert. Sie beruht auf dem Ansatz, bei Schleifen nicht zwischen Pfaden zu unterscheiden, die sich nur durch die Zahl der Iterationen unterscheiden. Wir müssen also nur die Pfade im Inneren des Rumpfes analysieren und im Übrigen nur die beiden Fälle 1 und 4 (oder statt 4 den Fall 2, 3 oder 5) aus der Liste oben behandeln.
Leider ist das fundamentale Problem damit nicht gelöst, wir kennen nach wie vor die Gesamtheit der möglichen Pfade nicht. Das wird verständlich, wenn man das Beispiel in Abbildung 19–9 so verändert, dass die beiden bedingten Anweisungen von wesentlich komplizierteren Ausdrücken abhängen, die sich auf die gleichen Variablen des Programms stützen. Nehmen wir an, dass in tausend Testfällen der Pfad A-D nie durchlaufen wurde. Dann liegt die Vermutung nahe, dass dieser Pfad wegen der Datenabhängigkeiten nicht erreichbar ist. Beweisen können wir das im Allgemeinen nicht. Wir wissen darum nicht, ob wir ohne Durchlaufen von A-D 75 % oder 100 % einfache Pfadüberdeckung erreicht haben.
Betrachtet man nicht einzelne Befehle, sondern Befehlsgruppen, z. B. Prozeduren, so kann man die Überdeckungskriterien des Glass-Box-Tests auf solche Gruppen übertragen. Damit entstehen die folgenden Makro-Überdeckungskritierien:
Programmeinheiten-Überdeckung
Jede Programmeinheit wird mindestens einmal aufgerufen.
Aufrufüberdeckung
Jeder aufrufbare Teil der Einheiten eines Programms wird wenigstens einmal aufgerufen (z. B. jede Methode einer Klasse).
Programmpfad-Überdeckung
Jede mögliche Ausführungssequenz der Programmeinheiten wird mindestens einmal durchlaufen.
Die Einwände gegen die Pfadüberdeckung gelten natürlich auch für die Programmpfad-Überdeckung.
Es ist verlockend, die mühsame Auswahl von Testfällen durch die Verwendung von Zufallswerten überflüssig zu machen, also die Eingaben nicht aufwändig auszuwählen, sondern von einem Programm erzeugen zu lassen. Selbst wenn Testfälle, die auf diese Weise generiert wurden, weniger erfolgreich sind als systematisch gewählte, kann man diesen Nachteil durch eine wesentlich höhere Zahl von Testfällen ausgleichen, denn die Kosten pro Testfall sind wesentlich geringer.
In der Praxis gibt es gegen diesen Ansatz, der sich von den oben vorgestellten radikal unterscheidet, drei Einwände:
Es ist sinnlos, mit völlig beliebigen Mausklicks und Tastendrücken einen Benutzer simulieren zu wollen; die Testeingaben müssen eine gewisse Ähnlichkeit mit denen eines echten Benutzers haben. Entsprechendes gilt auch für Systeme, die in einen technischen Prozess eingebettet werden. Darum ist es notwendig, ein Benutzungsprofil zu entwickeln, also eine statistische Beschreibung der Einwirkungen von außen auf die Software. Die Zufallseingaben werden dann so generiert, dass ihre statistischen Eigenschaften der Vorgabe entsprechen. Die Entwicklung eines solchen Profils und eines Testdatengenerators ist aber in der Regel sehr aufwändig.
Zu einem Testfall gehört ein Soll-Resultat. Bei einem Test, dessen Eingaben automatisch erzeugt werden, sollte auch die Auswertung des Tests automatisch erfolgen. Darum wird ein Orakel benötigt, also ein Programm, das entscheiden kann, ob das Resultat richtig ist. Natürlich ist es im Allgemeinen keineswegs klar, wie ein solches Orakel diese Entscheidung treffen kann.
Punktfehler und Bereichsfehler mit kleinem Bereich werden sehr wahrscheinlich nicht gefunden. Sie können aber mit hohen Risiken verknüpft sein.
Vermutlich ist es vor allem der erste Punkt, der verhindert, dass der Test mit Zufallsdaten große Bedeutung erlangt. Die Literaturlage ist dünn; immer wieder wird eine Publikation von Duran und Ntafos (1984) zitiert, die allerdings Beispiele verwenden, in denen das Orakel trivial ist (Sortierung oder Suche). Einige Artikel zu diesem Thema findet man über die Webseite von Robinson (o.J.), darunter ist auch ein Artikel von Nyman (o.J.), der über eine praktische Anwendung bei Microsoft berichtet; er spricht beim Test mit Zufallswerten von »Monkey Testing«.
Im Cleanroom-Prozess (Abschnitt 10.5) ist diese Form des Tests als Standardverfahren vorgesehen. Allerdings steht dabei nicht die Fehlersuche im Vordergrund; der Test liefert primär eine Aussage zur Zuverlässigkeit des Programms (siehe Abschnitt 10.5.2, Teil Der statistische Test).
In diesem Abschnitt zeigen wir zunächst an einem winzigen, durchaus akademischen Beispiel, wie die Teststrategien angewandt werden, um Fehler zu erkennen. Anschließend wird an einem echten – wenn auch immer noch kleinen – Beispiel gezeigt, wie ein Test praktisch aussieht.
Das folgende winzige Programm zeigt, wie die verschiedenen Teststrategien und Überdeckungsmaße dabei helfen können, Fehler aufzudecken. Natürlich liegt hier ein typisches Demonstrationsprogramm vor, die Funktionalität der Prüflinge und die Fehler darin sind viel zu simpel, aber sie zeigen gerade dadurch, welche Möglichkeiten bei komplexeren Prüflingen bestehen. Als Programmiersprache wurde hier ADA gewählt, aber das ist natürlich ohne Belang.
Prüflinge sind die folgenden neun Funktionen, die jeweils nur die Summe zweier Zahlen liefern sollen. Bis auf die erste enthalten alle Funktionen Fehler.
package Probanden is
function Summe1 (A, B : Integer) return Integer;
function Summe2 (A, B : Integer) return Integer;
function Summe3 (A, B : Integer) return Integer;
function Summe4 (A, B : Integer) return Integer;
function Summe5 (A, B : Integer) return Integer;
function Summe6 (A, B : Integer) return Integer;
function Summe7 (A, B : Integer) return Integer;
function Summe8 (A, B : Integer) return Integer;
function Summe9 (A, B : Integer) return Integer;
end Probanden;
Um diese Funktionen zu testen, benötigen wir einen Testrahmen. Dies leistet eine einfache Programmeinheit. Sie stellt die Prozedur TestSumme zur Verfügung, die jeden Testfall für alle zu prüfenden Funktionen ausführt und das Ergebnis protokolliert. Die Resultate der Funktionen werden mit der korrekten Summe verglichen, bei einem erfolgreichen Test – also bei einem Fehler – wird ein X ausgegeben, sonst ein Minus-Zeichen.
package TestRahmen is
procedure TestSumme (A, B : Integer; TFBezeichner : String);
-- fuehrt alle Funktionen aus und protokolliert den Testfall
end TestRahmen;
----------------------------------------
with Probanden; use Probanden;
with Ada.Text_IO; use Ada.Text_IO;
package body TestRahmen is
package IIO is new Integer_IO(Integer);
procedure TesteFunktion (a, b, nr : Integer) is
-- fuehrt die Funktion Summe<nr> aus, vergleicht Ist mit Soll und
-- notiert das Ergebnis
res : Integer;
begin
begin
case nr is
when 1 => res := Summe1(a, b);
when 2 => res := Summe2(a, b);
when 3 => res := Summe3(a, b);
when 4 => res := Summe4(a, b);
when 5 => res := Summe5(a, b);
when 6 => res := Summe6(a, b);
when 7 => res := Summe7(a, b);
when 8 => res := Summe8(a, b);
when 9 => res := Summe9(a, b);
when others => null;
end case;
if (res = a+b) then Put('-'); else Put('X'); end if;
exception
when others => Put('E');
-- unvorhergesehene Ausnahmen werden angezeigt
end;
end TesteFunktion;
procedure TestSumme (a, b : Integer; TFBezeichner : String) is
begin
Put (TFBezeichner & " ");
for i in 1 .. 9 loop
TesteFunktion (a, b, i); -- Ausfuehren der Funktion Summe<i>
end loop;
Put (" "); IIO.Put (a);Put (" "); IIO.Put (b); New_Line;
end TestSumme;
end TestRahmen;
Als Testtreiber dient das Programm TesteFunktionen. Wir können mit Hilfe der Prozedur TestSumme direkt die Testfälle formulieren. Zunächst definieren wir typische spontane Black-Box-Testfälle mit dem ebenfalls typischen Resultat, dass alles in Ordnung zu sein scheint; die neun Striche signalisieren »kein Befund«.
with TestRahmen; use TestRahmen;
with Ada.Text_IO; use Ada.Text_IO;
procedure TesteFunktionen is
MaxInt : Integer := Integer'Last;
MinInt : Integer := Integer'First;
begin
Put_Line ("Intuitiver Black-Box-Test");
Put_Line ("=========================");
TestSumme (5, 12, "T01");
TestSumme (-500, -500, "T02");
TestSumme (999, 1, "T03");
TestSumme (-1000, +1000, "T04");
end TesteFunktionen;
Unser Testtreiber liefert die folgende Ausgabe:
Intuitiver Black-Box-Test
=========================
T01 --------- 5 12
T02 --------- -500 -500
T03 --------- 999 1
T04 --------- -1000 1000
Dann werden, immer noch als Black-Box-Test, einige Grenz- und Sonderfälle geprüft.
Put_Line ("Test von Grenzwerten und Sonderfaellen");
Put_Line ("======================================");
TestSumme (0, 0, "T05");
TestSumme (0, MaxInt, "T06");
TestSumme (MaxInt, 0, "T07");
TestSumme (0, MinInt, "T08");
TestSumme (MinInt, 0, "T09");
TestSumme (MinInt, MaxInt, "T10");
TestSumme (MaxInt, MinInt, "T11");
Diese Testfälle sind, wie die folgende Ausgabe zeigt, bei der Funktion Summe2 in zwei Fällen erfolgreich.
Test von Grenzwerten und Sonderfaellen
======================================
T05 --------- 0 0
T06 --------- 0 2147483647
T07 -X------- 2147483647 0
T08 --------- 0 -2147483648
T09 --------- -2147483648 0
T10 --------- -2147483648 2147483648
T11 -X------- 2147483648 -2147483648
Allgemein ist zur Auswahl der Testfälle zu sagen, dass Testdaten vermieden wurden, die durch einen unwahrscheinlichen Zufall einen Fehler anzeigen oder nicht anzeigen. Ein Beispiel wäre ein Programm, das zu einem Datum das Datum des folgenden Tages liefern soll. Es könnte sein, dass das Programm für genau ein Datum nicht korrekt arbeitet und genau dieses Datum im Test zufällig verwendet wird. Umgekehrt könnte das Programm stets das gleiche Datum ausgeben und im Test trotzdem nicht auffallen, weil zufällig der einzige Tag eingegeben wird, für den das Ergebnis stimmt. Statistisch sind diese beiden Sonderfälle ohne Bedeutung.
Für den Glass-Box-Test brauchen wir den Programmcode der Prüflinge. Eine Messung der Anweisungsüberdeckung zeigt, dass insgesamt drei Anweisungen der Prüflinge bislang noch nicht ausgeführt wurden. Diese Anweisungen liegen in den Funktionen Summe3 und Summe8. Diese sowie die Funktionen Summe1 und Summe2 sind nachfolgend wiedergegeben. Die Markierungen S1, S2 und S3 zeigen die nicht ausgeführten Anweisungen.
function Summe1 (a, b : Integer) return Integer is
begin
return (a + b);
end Summe1;
-----------------------------------------------
function Summe2 (a, b : Integer) return Integer is
begin
if a = MaxInt then return (b / 2);
else return (a + b);
end if;
end Summe2;
------------------------------------------------
function Summe3 (a, b : Integer) return Integer is
begin
if a = (MaxInt / 2) then return (b / 2); --S1
else return (a + b);
end if;
end Summe3;
------------------------------------------------
function Summe8 (a, b : Integer) return Integer is
s : Integer;
begin
if (a = MaxInt - 8) then s := 1; else s := 0; end if; --S2
if (b = MinInt + 8) then s := s+1; end if; --S3
return (a + b + s / 2);
end Summe8;
Um die vollständige Anweisungsüberdeckung zu erreichen, erweitern wir den Testtreiber um die folgenden drei Testfälle.
Put_Line ("Anweisungsueberdeckung");
Put_Line ("======================");
TestSumme (MaxInt/2, 1000, "T12"); -- S1 in Summe3
TestSumme (MaxInt-8, -888, "T13"); -- S2 in Summe8
TestSumme (8888, MinInt+8, "T14"); -- S3 in Summe8
Damit erzielen wir 100 % Anweisungsüberdeckung. Der neue Testtreiber liefert zusätzlich die folgende Ausgabe:
Anweisungsueberdeckung
======================
T12 --X------ 1073741823 1000
T13 --------- 2147483639 -888
T14 --------- 8888 -2147483640
Wir haben einen Fehler in Summe3 entdeckt.
Eine Messung der Zweigüberdeckung ergibt, dass zwei Zweige bislang noch nicht überdeckt wurden, nämlich die leeren ELSE-Zweige in den Funktionen Summe4 und Summe5.
function Summe4 (a, b : Integer) return Integer is
s : Integer;
begin
s := b;
if a /= 4444 then s := a; end if;
return (s + b);
end Summe4;
------------------------------------------------
function Summe5 (a, b : Integer) return Integer is
s : Integer := 0;
begin
if (a /= 555) and (b /= -555) then s := a + b; end if;
return s;
end Summe5;
Durch zwei weitere Testfälle wird die Zweigüberdeckung erreicht und ein Fehler im leeren Zweig der Funktion Summe4 angezeigt.
Put_Line ("Zweigueberdeckung");
Put_Line ("=================");
TestSumme (4444, 3333, "T15"); --ELSE-Zweig in Summe4
TestSumme (555, -555, "T16"); --ELSE-Zweig in Summe5
Zweigueberdeckung
=================
T15 ---X----- 4444 3333
T16 --------- 555 -555
Die Termüberdeckung erfordert, dass die elementaren Terme einzeln den Wert einer Bedingung bestimmen. Dieses gilt für die Terme in den Bedingungen, die in Summe5 und Summe6 angegeben sind. Betrachten wir zuerst die Bedingung in Summe5.
function Summe5 (a, b : Integer) return Integer is
s : Integer := 0;
begin
if (a /= 555) and (b /= -555) then s := a + b; end if;
return s;
end Summe5;
Einige Testfälle, z. B. Testfall 15, hatten bereits die Kombination der beiden Terme mit den Werten TRUE und TRUE enthalten, wir müssen also noch FALSE-TRUE und TRUE-FALSE testen, um die Termüberdeckung für Summe5 zu erreichen; der Testfall 16 (FALSE-FALSE) bringt für die Termüberdeckung nichts.
Put_Line ("Termueberdeckung");
Put_Line ("================");
TestSumme (555, 5555, "T17"); -- Kombination FALSE-TRUE
TestSumme (5555, -555, "T18"); -- Kombination TRUE-FALSE
Die gleiche Analyse muss für Summe6 durchgeführt werden.
function Summe6 (a, b : Integer) return Integer is
s : Integer := a + b;
begin
s := a + b;
if (a = -b) or (b = 6666) then s := 0; end if;
return s;
end Summe6;
Da hier die beiden Terme durch or verknüpft sind, brauchen wir den Fall FALSE-TRUE; TRUE-FALSE ist bereits durch T16, FALSE-FALSE z. B. durch T15 erledigt.
TestSumme (66, 6666, "T19"); -- Kombination FALSE-TRUE
Die neuen Testfälle führen zu folgender Ausgabe:
Termueberdeckung
================
T17 ----X---- 555 5555
T18 ----X---- 5555 -555
T19 -----X--- 66 6666
Summe5 erweist sich als fehlerhaft, wenn genau einer der beiden Terme FALSE ist (T17 und T18), Summe6, wenn nur der zweite TRUE ist.
Nun wollen wir noch untersuchen, inwieweit wir die Pfade durch die Prüflinge mit Testfällen überdecken können. Dies betrifft lediglich die Funktionen Summe7 und Summe8. Für Summe7 ist die Pfadüberdeckung praktisch nicht erreichbar, es wären allein dafür 2147483553 Testfälle nötig.
function Summe7 (a, b : Integer) return Integer is
s, a1 : Integer;
begin
if (b < 0) and (a >= 77) then
s := b + 77; a1 := a;
loop
s := s + 1; a1 := a1 - 1;
exit when a1 <= 77;
end loop;
return s;
else
return (a + b);
end if;
end Summe7;
Wir können jedoch Testfälle konstruieren, die dazu führen, dass die Schleife (eine REPEAT-Schleife) nur genau einmal durchlaufen wird, d. h., die Abbruchbedingung ist von Beginn an erfüllt (dies empfiehlt sich generell für diese Schleifenart). Dazu erweitern wir den Testtreiber um zwei weitere Testfälle.
Put_Line ("Schleifentest Summe7");
Put_Line ("====================");
TestSumme (77, -7777, "T20");
TestSumme (78, -7777, "T21");
Schleifentest Summe7
====================
T20 ------X-- 77 -7777
T21 --------- 78 -7777
Testfall 20, mit dem wir uns der einfachen Pfadüberdeckung nähern, deckt den Fehler in Summe7 auf.
Für die Funktion Summe8 ist die Pfadüberdeckung erreichbar, da es nur vier Pfade durch diese Funktion gibt.
function Summe8 (a, b : Integer) return Integer is
s : Integer;
begin
if (a = MaxInt - 8) then s := 1; else s := 0; end if;
if (b = MinInt + 8) then s := s+1; end if;
return (a + b + s / 2);
end Summe8;
Drei Pfade sind bereits durch die bisherigen Testfälle abgedeckt (z. B. durch T01, T13 und T14). Übrig bleibt der Pfad, der durchlaufen wird, wenn beide Bedingungen wahr sind. Dieses bewirkt der folgende Testfall:
Put_Line ("Pfadueberdeckung Summe8");
Put_Line ("=======================");
TestSumme (MaxInt-8, MinInt+8, "T22");
Der Fehler in dieser Funktion wird damit ebenfalls aufgedeckt.
Pfadueberdeckung Summe8
=======================
T22 -------X- 2147483639 -2147483640
Keines der Überdeckungskriterien, die wir mit diesen Testfällen erfüllt haben, garantiert jedoch, dass alle Fehler entdeckt werden. Das zeigt die Funktion Summe9, bei der der Wert des ersten Parameters unter Umständen ein falsches Resultat hervorruft.
function Summe9 (a, b : Integer) return Integer is
s : Integer;
begin
s := MaxInt / 2 + 9;
if a < s then return (a + b);
else return (a mod (s+1) + b + s + 1);
end if;
end Summe9;
Der ELSE-Zweig wird nur dann durchlaufen, wenn a größer oder gleich s ist. Ist a größer, so wird die Modulo-Operation durch die folgende Addition von (s+1) kompensiert. Nur wenn a gleich s ist, wird ein Resultat erzeugt, dass um s+1 zu hoch ist. Ob es dabei zu einer Exception kommt, hängt von b und vom Laufzeitsystem ab. Diesen Fehler zeigt der folgende Testfall:
Put_Line ("Fehlerfall Summe9");
Put_Line ("=================");
TestSumme (MaxInt/2 + 9, 9999, "T23");
Fehlerfall Summe9
=================
T23 --------X 1073741832 9999
Warum wurde dieser Fehler aber nicht schon durch die anderen Testfälle aufgedeckt? Der Grund dafür liegt bei diesem Fehler im Zusammenspiel von Arithmetik und Programmablauf. Dieses Zusammenspiel ist aber keine exotische Besonderheit dieses Programms, sondern das wesentliche Merkmal der Von-Neumann-Rechner, mit denen wir seit Beginn der Informatik arbeiten.
Unser Beispiel zeigt, dass sich Fehler unter gewissen Umständen durch Tests aufdecken lassen. Dass die Fehler hier absichtlich und plakativ eingebaut waren, ist für die Praxis kein Trost, denn dort entstehen erfahrungsgemäß unabsichtlich weitaus subtilere Fehler, die entsprechend schwer zu finden sind. In diesem Sinne sind die Beispiele hier durchaus verharmlosend. Um sie auf die Praxis zu übertragen, muss man sich also vorstellen, dass es um kompliziertere Funktionen und um größere Prüflinge geht.
In anderem Sinne erscheint die Lage eher dramatisiert: In der Praxis kommen sehr viele ganz banale Fehler vor. Diese zeigen sich schon, wenn der betroffene Code überhaupt einmal ausgeführt wird. Darum ist die vollständige Anweisungsüberdeckung sehr viel erfolgreicher, als es die Beispiele hier erscheinen lassen.
Nachfolgend wird der Test für ein reales, im weitesten Sinne brauchbares Programm entwickelt. Das Vorgehen ist praxisnah, d. h., wir streben nicht nach einem perfekten Test, sondern suchen pragmatisch eine sinnvolle und vom Aufwand her angemessene Lösung. Der Testvorbereitung liegt eine wie üblich knappe, keineswegs völlig klare oder vollständige Spezifikation zu Grunde.
Das Beispiel zeigt, dass wir mit den anschaulichen Verfahren der Eingabe-, Ausgabe- und Funktionsüberdeckung im Wesentlichen die Äquivalenzklassen identifizieren, die hier angemessen sind. Die Bildung der Äquivalenzklassen ist also kein separates Konzept, sondern die Abstraktion des Kriteriums, mit möglichst wenigen Testfällen Repräsentanten aller wichtigen Fälle zu erkennen. Die Menge der Testfälle hier ist größer als unbedingt nötig, es ist aber fraglich, ob es sich lohnt, sie mit großem Aufwand weiter einzuschränken. Denn ein entbehrlicher Testfall schadet nicht, der Aufwand für den Test wird dadurch kaum höher.
Zu realisieren ist ein Programm VBS zur Verwaltung eines binären Suchbaums, in dem Namen gespeichert sind. Zur Verwaltung gehören die Operationen einfügen, löschen und ausgleichen. Der Baum soll zu Beginn leer sein; nach Programmende ist er verloren, wird also nicht persistent gespeichert.
Ein Name ist spezifiziert als eine Folge von 1 bis 10 Buchstaben und Ziffern ohne Sonderzeichen. Der Baum soll maximal 20 Namen aufnehmen.
Weiterhin wird gefordert, dass VBS eine primitive zeilenorientierte Bedienschnittstelle und eine einfache Ausgabe des Baums auf dem Bildschirm haben muss. Die Bedienschnittstelle soll folgenden Anforderungen genügen:
Als Eingabeaufforderung (Prompt) ist das Zeichen »>« am Zeilenanfang auszugeben, die Eingabe erfolgt in derselben Zeile und wird mit RETURN abgeschlossen. Leerzeichen sind an keiner Stelle erlaubt, sie führen zu einer Fehlermeldung. VBS meldet sich mit einer Anfangsmeldung und verabschiedet sich mit einer Endemeldung.
Wird nach dem Prompt ein gültiger Name eingegeben, so wird er eingefügt oder, falls er bereits vorhanden war, gelöscht. Durch Eingabe eines Gleichheitszeichens wird der Baum ausgeglichen, mit einem Fragezeichen wird er angezeigt.
Mit einem Punkt wird der Programmlauf beendet; wenn der Baum nicht leer ist, wird er angezeigt. Falls fehlerhafte Eingaben gemacht wurden (Eingaben, die eine Fehlermeldung hervorrufen), wird mit der Endemeldung ihre Anzahl angezeigt.
Die genannten Eingaben werden quittiert, explizit oder durch Ausgabe des nächsten Prompts. Jede ungültige Eingabe soll eine spezifische Fehlermeldung hervorrufen und den Baum unverändert lassen.
Das Programm VBS akzeptiert die Eingabe von Namen und der Kommandos »=«, ».«, »?«. Außerdem ist die Eingabe falscher Zeichen und falscher Namen möglich. Wir können demnach die in der folgenden Tabelle beschriebenen Äquivalenzklassen (ÄK) für die Eingaben definieren.
Eingabe |
Definition der Klasse gültiger Daten |
ÄK-Nr. |
korrekter Name |
1 ≤ Lng ≤ 10 und Zch |
K1 |
Kommando, |
? |
K2 |
falsches Kommando |
Eingabe, beginnend mit Zch ∉ {a..Z, 0..9, =, ?,.} |
K5 |
Name mit falschen Zeichen |
Eingabe, beginnend mit Zch |
K6 |
zu langer Name |
Eingabe, beginnend mit Zch |
K7 |
Man beachte, dass sich die Klassen K6 und K7 überlappen, also streng genommen keine Klassen sind. Es handelt sich hier um den häufigen Fall, dass es der Implementierung überlassen wird, welcher von zwei Fehlern gemeldet wird.
Nun können wir Testfälle spezifizieren, die die Eingabeäquivalenzklassen abdecken. Wir verwenden dabei die folgenden Konventionen und Abkürzungen:
Ein Strich in der Spalte »Anfangszustand« (A-Zustand) besagt, dass VBS gestartet ist und noch läuft, im Übrigen in einem beliebigen Zustand ist.
Ausgaben sind durch einen Pfeil gekennzeichnet (»→ «). »→ Baum« bedeutet, dass der Baum in seinem aktuellen Zustand ausgegeben wird. »→ FZ falsche Eingaben« erscheint mit dem Zahlenwert von FZ an Stelle von FZ.
Der Fehlerzähler FZ und der Namenszähler NZ sind nach dem Start des Programms mit null initialisiert. »FZ +« und »NZ +« stehen für die Inkrementierung der Zähler.
»n« steht für einen syntaktisch korrekten Namen, »Lng« für seine Länge; »n+« / »n-« bedeutet, dass der Name n in der aktuellen Menge der Namen enthalten / nicht enthalten ist.
Die Menge der Namen (der Baum) bleibt, soweit nicht Namen hinzugefügt oder gelöscht werden, von Schritt zu Schritt erhalten, bis VBS beendet wird.
ÄK-Nr. |
A-Zustand |
Eingabe |
Soll-Resultat (in Klammern Zustandsänderungen) |
|
E1 |
K1 |
NZ < 20, n- |
n |
(n+, NZ +) |
E2 |
K7 |
– |
n, Lng = 10, + 1 Zeichen |
→ »Name zu lang« (FZ +) |
E3 |
K6 |
– |
n, Lng ≤ 9, + 1 Zeichen (nicht Buchst. od. Ziffer) |
→ »falsche Zeichen in Namen« (FZ +) |
E4 |
K2 |
– |
? |
→ Baum |
E5 |
K3 |
– |
= |
(aktueller Baum, ausgeglichen) |
E6 |
K5 |
– |
* |
→ »falsches Kommando« (FZ +) |
E7 |
K4 |
– |
. |
falls NZ > 0: → Baum |
Die Ausgaben von VBS sind Anfangsmeldung, Prompt, der aktuelle Baum, Fehlermeldungen »Name zu lang«, »falsche Zeichen in Namen«, »zu viele Namen«, »falsches Kommando« sowie die Endemeldung mit Fehlerzahl und mit aktuellem Baum. Die Anfangsmeldung erfordert keinen eigentlichen Testfall, nur den Programmstart. Wir benötigen für die Ausgabeüberdeckung folgende Testfälle, die bis auf A6 bereits in den Testfällen E1 bis E7 enthalten sind:
Testfall |
A-Zustand |
Eingabe/Verweis |
Soll-Resultat (in Klammern Zustandsänderungen) |
A1, A2 |
VBS läuft nicht |
(Programmstart) |
→ »Start VBS«, → »>« (FZ = 0, NZ = 0) |
A3 |
– |
wie E4 |
→ Baum |
A4 |
– |
wie E2 |
→ »Name zu lang« (FZ +) |
A5 |
– |
wie E3 |
→ »falsche Zeichen in Namen« (FZ +) |
A6 |
NZ = 20, n- |
n |
→ »zu viele Namen« (FZ +) |
A7 |
– |
wie E6 |
→ »falsches Kommando« (FZ +) |
A8 |
FZ > 0,NZ > 0 |
wie E7 |
→ Baum,→ »Ende VBS«, |
Die Funktionen sind neben Start und Ende das Einfügen, Löschen, Ausgleichen und Ausgeben des Baums, außerdem die Erzeugung von Fehlermeldungen. Die Funktionstestmatrix zeigt, dass diese Funktionen bis auf das Löschen bereits durch die bisher definierten Testfälle abgedeckt sind. (Die Funktion Start ist weggelassen, da sie durch jeden Testfall ausgeführt wird. Die verschiedenen Fehlermeldungen werden durch die Ausgabeüberdeckung kontrolliert.)
Testfälle |
||||||||
E1 |
E2 |
E3 |
E4 |
E5 |
E6 |
E7 |
A6 |
|
Einfügen |
X |
|
|
|
|
|
|
|
Löschen |
|
|
|
|
|
|
|
|
Ausgleichen |
|
|
|
|
X |
|
|
|
Ausgeben |
|
|
|
X |
|
|
|
|
Fehlermeldung zeigen |
|
X |
X |
|
|
X |
|
X |
Ende |
|
|
|
|
|
|
X |
|
Wir fügen daher als weiteren Testfall hinzu:
Testfall |
A-Zustand |
Eingabe/Verweis |
Soll-Resultat |
F1 |
n+ |
n |
(n-, NZ dekrementiert) |
Um Fehler an den Bereichsrändern zu erkennen, betrachten wir auch die Grenzfälle. Für die Äquivalenzklasse K1 identifizieren wir als Grenzwerte für Lng (Anzahl der Zeichen) 0 und 1 sowie 10 und 11. Zusätzlich betrachten wir die Anforderung, dass die Anzahl der Namen zwischen 0 und 20 liegen kann. Die Grenzwerte sind am unteren Rand -1 und 0, am oberen Rand 20 und 21. Den ungültigen Wert am jeweiligen unteren Rand der betrachteten Äquivalenzklassen (0 bzw. -1) können wir im Test allerdings nicht erreichen, da wir keine Möglichkeit haben, einen leeren Namen einzugeben oder aus dem leeren Baum etwas zu löschen. Diese Überlegungen führen zu den folgenden Testfällen:
Testfall |
geprüfter Grenzfall |
A-Zustand |
Eingabe |
Soll-Resultat |
G1 |
Name der Länge 1 |
NZ < 20, n- |
n, Lng = 1 |
(n+, NZ +) |
G2 |
Name der Länge 10 |
NZ < 20, n- |
n, Lng = 10 |
(n+, NZ +) |
G3 |
Name der Länge 11 |
– |
n, Lng = 11 |
→ »Name zu lang« (FZ +) |
G4 |
Eingabe in leeren Baum |
NZ = 0 |
n |
(n+, NZ = 1) |
G5 |
Eingabe des 20. Namens |
NZ = 19, n- |
n |
(n+, NZ = 20) |
G6 |
Eingabe des 21. Namens |
NZ = 20, n- |
n |
→ »zu viele Namen« (FZ +) |
G7 |
Löschen d. einzigen Namens |
NZ = 1, n+ |
n |
(NZ = 0) |
G8 |
Löschen im vollen Baum |
NZ = 20, n+ |
n |
(n-, NZ = 19) |
G9 |
Ausgeben des leeren Baums |
NZ = 0 |
? |
→ Baum (leer) |
G10 |
Ausgeben des vollen Baums |
NZ = 20 |
? |
→ Baum (mit 20 Namen) |
G11 |
Ausgleichen d. leeren Baums |
NZ = 0 |
= |
(leerer Baum) |
G12 |
Ausgleichen d. vollen Baums |
NZ = 20 |
= |
(Baum, ausgeglichen) |
Die Testfälle lassen sich zu einer einzigen Testsequenz zusammenfügen. Dabei kann man auch dafür sorgen, dass Zustandsänderungen, die keine Ausgabe hervorrufen, durch nachfolgende Kommandos angezeigt werden. Der Zustand des Baums kann mit »?« jederzeit ausgegeben werden; für die beiden Zähler FZ und NZ gibt es keine entsprechende Anweisung. Uns genügt es, dass wir die Zähler an mehreren Stellen kontrollieren können; andernfalls müssten wir, da FZ nur am Programmende sichtbar ist, das Programm immer wieder beenden und neu starten. In der folgenden Tabelle steht das Komma für die Eingabe RETURN. Bei den Soll-Resultaten ist der Prompt »>« nicht erwähnt; er folgt, außer nach Programmende, auf jedes RETURN (ggf. nach irgendwelchen Ausgaben). Ein Semikolon in der rechten Spalte trennt die durch die einzelnen Eingaben abgedeckten Testfälle.
Testschritt |
Eingabe |
Soll-Resultat (Ausgabe) |
abgedeckte Testfälle |
1 |
(Programmstart) |
→ »Start VBS« |
A1, A2 |
2 |
007Bond, ? |
→ Baum (mit einem Knoten »007Bond«) |
G4, E1; A3 |
3 |
007Bond, ? |
→ Baum (leer) |
G7, F1; E4, G9 |
4 |
=, ? |
→ Baum (leer) |
G11, E5; G9 |
5 |
1234567Bond |
→ »Name zu lang« |
E2, A4, G3 |
6 |
Otto+ottO |
→ » falsche Zeichen in Namen« |
E3, A5 |
7 |
N, N2, ..., N19, ? |
→ Baum (mit 19 Knoten wie eingegeben) |
G1, G4, E1 |
8 |
# |
→ »falsches Kommando« |
E6, A7 |
9 |
1234567890, ? |
→ Baum (mit 20 Knoten wie eingegeben) |
G2, G5; G10 |
10 |
=, ? |
→ Baum (wie nach Schritt 9, aber ausgegl.) |
G12, E5; G10 |
11 |
N21 |
→ »zu viele Namen« |
G6, A6 |
12 |
N2, . |
→ Baum (wie nach Schritt 10, aber ohne N2), |
F1, G8; |
Der Black-Box-Test zeigt einige Fehler, darunter auch den, dass die Fehlermeldungen in der Implementierung weniger differenziert sind als gefordert. So liefern die Testschritte 5, 6 und 8 alle die gleiche Meldung »Falsche Eingabe«.
Eine Messung im realisierten Programm zeigt, dass 10 von 102 Anweisungen nicht ausgeführt werden. Der Black-Box-Test erreicht also nur etwa 90 % Anweisungsüberdeckung. Von den 52 Zweigen des Programms werden 7 ebenfalls nicht erreicht, was einer Zweigüberdeckung von 87 % entspricht. Die Analyse der nicht ausgeführten Anweisungen zeigt, dass
a) eine Anweisung in einem ELSE-Zweig nicht ausgeführt wird, die einen Fehler meldet, wenn am Zeilenanfang ein Kommandozeichen steht, dahinter aber die Zeile noch nicht endet,
b) eine Anweisung in einem ELSE-Zweig nicht ausgeführt wird, in dem ein Knoten gelöscht wird, wenn dieser rechts einen leeren, links einen nichtleeren Unterbaum hat,
c) der einzige Aufruf einer Lösch-Prozedur, die einen Knoten löscht, wenn dieser zwei nichtleere Unterbäume hat, nicht ausgeführt wird. Damit wird der Code dieser Prozedur auch nicht ausgeführt.
Der Code zu den Punkten b und c ist im nachstehenden Code-Fragment unterstrichen.
procedure Loeschen(Opfer: BaumT) is
----------------------------------------------------------------
procedure SuchenLoeschen(B: in out BaumT) is -- Suche im Re. UB
-- Baum und Opfer sind global aus EinfLoesch / Loeschen
begin
if B.Lub = null then -- Nachbar gefunden
Baum := B; -- Opfer wird durch Nachbarn ersetzt
B := B.Rub; -- dieser durch seinen re. UB
Baum.Lub := Opfer.Lub; -- Nachbar uebernimmt...
Baum.Rub := Opfer.Rub; -- die Soehne des Opfers
else
SuchenLoeschen(B.Lub);
end if;
end SuchenLoeschen;
----------------------------------------------------------------
begin -- Knoten gefunden, loeschen
if (Baum.Lub = null) or (Baum.Rub = null) then -- einfacher Fall
if (Baum.Lub = null) then
Baum := Baum.Rub;
else
Baum := Baum.Lub;
end if;
else -- schwieriger Fall, beide UB vorhanden
SuchenLoeschen(Baum.Rub);
end if;
NKnoten := NKnoten - 1;
end Loeschen;
Wir können auf Basis dieser Analyse zwei weitere Testschritte (13, 14) konstruieren: Im ersten wird die falsche Eingabe gemacht, im zweiten wird ein Baum so aufgebaut, dass die bislang noch nicht verwendeten Lösch-Operationen ausgeführt werden.
Damit haben wir 100 % Anweisungsüberdeckung erzielt. Die Messung im Programm zeigt, dass dadurch auch fast alle Zweige erreicht wurden, nur zwei Zweige wurden nicht durchlaufen:
1. Wenn es keine Eingabefehler gab, darf die Meldung, wie viele Eingabefehler aufgetreten sind, nicht erscheinen.
2. Wenn der Baum am Programmende leer ist, dann muss nichts getan werden (leerer ELSE-Zweig).
Diese Zweige, und damit 100 % Zweigüberdeckung, erreichen wir mit einem weiteren Testschritt (15).
Testschritt |
Eingabe |
Soll-Resultat (Ausgabe) |
13 |
(Programmstart) ?? |
→ »falsches Kommando« |
14 |
1, 2, 3, 4, 5, 6, 7, ? |
→ Baum (linear entartet) |
15 |
. |
»Ende VBS« |
Die Termüberdeckung wird hier nicht gesondert betrachtet; es gibt nur einen Multiterm (if (Baum.Lub = null) or (Baum.Rub = null) then in Loeschen), der drei Belegungen erfordert (FALSE-FALSE, FALSE-TRUE, TRUE-FALSE). Diese drei Fälle sind durch das Löschen der 4, 2 und 6 abgedeckt.
Das Beispiel ist im Wesentlichen echt, es wurde nicht als Testbeispiel entwickelt. Entsprechend verlief auch der Test ganz echt – und zeigte Fehler an. Natürlich lassen sich noch weitere sinnvolle Testfälle zufügen. Beispielsweise sind hier zwar die Grenzfälle getestet, nicht aber Fälle fern der Grenzen wie Namen mit 12 oder 33 Zeichen.
Die knappe Aufgabenstellung am Anfang dieses Abschnitts ist weit von einer vollständigen Spezifikation entfernt. Trotzdem liegen die Probleme, die durch die Vorbereitung des Tests zutage treten, vornehmlich im Inhalt, die Spezifikation müsste nicht präzisiert, sondern verbessert werden. So fällt beim Testen auf, dass ein leerer Baum angezeigt wird, indem einfach nichts ausgegeben wird. Man kann nicht behaupten, dass die Spezifikation etwas anderes verlangt, aber für den Benutzer ist diese Reaktion verwirrend, eine Meldung der Art »Baum ist leer« wäre zweckmäßig. Ebenso wäre es nach der Erfahrung des Tests zweckmäßig, wenn nach jeder Eingabe eines Namens angezeigt würde, ob dieser nun eingefügt oder gelöscht wurde.
Da hier das Thema Test im Vordergrund steht, wurden die Mängel ignoriert, teilweise auch einfach beseitigt, in einem realen Projekt wären nun Problemmeldungen zu schreiben. Denn die Entwickler und Tester sollten ihre Erkenntnisse weder ungenutzt lassen noch frei bestimmen, was in der Spezifikation zu ändern ist.
Eine automatische Durchführung erfordert, dass der Test ohne Interaktion ausgeführt wird, also mit der Eingabe aus einer Datei und der Ausgabe in eine Datei. Dann kann das Ergebnis auch in der Wartungsphase zum Regressionstest herangezogen werden. Das ist bei einer Ausgabe, die keine Grafik enthält, kein echtes Problem.
Das Beispiel ist instruktiv, weil es in allen untersuchten Kategorien des Tests spezielle Erkenntnisse liefert. Die vollständige Anweisungsüberdeckung wird ohne großen Aufwand erreicht, vollständige Zweigüberdeckung ist damit nahezu gegeben. Das ist nicht typisch. Dieses Programm ist durch seine rekursive Struktur geprägt, die dazu führt, dass ein sehr kompakter Code auf viele Sonderfälle ohne eigentliche Sonderbehandlung richtig reagiert. Dadurch gibt es nur wenige »dunkle Ecken« im Programm, und entsprechend einfach ist der Test.
Hier zeigt sich, dass kleine, elegante Programme nicht nur hinsichtlich Speicherplatz und Rechenzeit, sondern auch beim Testaufwand sparsamer sind als »pompöse« Programme (ein Ausdruck von Niklaus Wirth).
This is a brief introduction to certification of aviation software. It is intended for software developers who would like to produce aviation software but who have no idea how to get started. What you will learn is that you probably don’t want to do it.
Birds Project, Introduction to DO-178B,
(Birds Project, o.J.)
Die Impulse in Richtung höherer Zuverlässigkeit kommen nicht aus der Informatik, sondern aus den Anwendungen der Informatik. In den Siebzigerjahren waren es die amerikanischen Puppenhersteller, die Geld in die Forschung über den Programmtest steckten. Sie lieferten damals ihre ersten Puppen aus, die dank einem Prozessor sprechen, trinken und sich nass machen konnten. Da die Gesetze zum Schutz der Verbraucher in den USA eine zeitlich unbegrenzte Rücknahme fehlerhafter Produkte verlangen, war das Risiko durch Software für die Puppenindustrie beträchtlich.
Heute ist die treibende Kraft die Luftfahrt. Die internationale RTCA (Radio Technical Commission for Aeronautics) spielt die Rolle eines TÜVs, auch für die in der Luftfahrt eingesetzte Software. DO-178B, Software Considerations in Airborne Systems and Equipment Certification, ist der De-facto-Standard für alle Software, die in der Luftfahrt eingesetzt wird. Je nach den Risiken, die mit einem System verbunden sind, wird es in eine der Klassen A (katastrophal) bis E (keine Auswirkungen) eingeordnet. Für die Entwicklung gibt es Vorschriften, deren Strenge von der Risikoklasse abhängt. Darin schreibt die RTCA für die höheren Risikoklassen auch die anspruchsvollsten Testverfahren vor, die heute technisch beherrscht werden.
Auf anderen Gebieten, vor allem in der Automobiltechnik, spielt DIN EN IEC 61508, Funktionale Sicherheit sicherheitsbezogener elektrischer/elektronischer/programmierbarer elektronischer Systeme, mit den vier Safety Integrity Levels 1 bis 4 eine ganz ähnliche Rolle wie die DO-178 B in der Luftfahrt. Wir erwarten, dass sich dieser Trend in Zukunft durchsetzen wird:
Software wird in Risikoklassen eingeordnet. Wo Risiken keine Rolle spielen (und nur dort), ist das Software Engineering eine Privatsache der Entwickler.
Aufwändige, von Hand durchzuführende Verfahren haben nur in Extremprojekten eine Chance, nicht zuletzt, weil auch ihre Überprüfung hohe Qualifikation und hohen Aufwand erfordert. Für alle anderen Systeme brauchen wir Routine-Verfahren, die gut von Werkzeugen unterstützt werden.