Einführung
Testideen basieren auf Fehlermodellen, dem Verständnis, welche Fehler in einer Software plausibel sind, und
dem Wissen, wie solche Fehler mit Tests aufgedeckt werden können. Diese Richtlinie zeigt, wie Sie Testideen aus
Booleschen Ausdrücken und Vergleichsausdrücken entwickeln können. Zunächst leitet Sie Verfahren aus einer Betrachtung
von Codebeispielen ab, beschreibt dann aber auch, wie die Verfahren anzuwenden sind, wenn der Code noch nicht
geschrieben wurde oder nicht verfügbar ist.
Boolesche Ausdrücke
Schauen Sie sich das folgende Codefragment aus einem (imaginären) System an, mit dem die Detonation einer Bombe
gesteuert werden soll. Der Code gehört zum Sicherheitssystem und steuert die Betätigung des Auslösers zur rechten Zeit.
if (publicIsClear || technicianClear) {
bomb.detonate();
}
Der Code ist falsch. || müsste durch && ersetzt werden. Dieser
Fehler hat schlimme Folgen. Die Bombe detoniert nicht erst, wenn sowohl der Techniker als auch die Zuschauer in
Sicherheit sind, sondern wenn entweder der Techniker in Sicherheit ist (technicianClear) oder die Zuschauer in
Sicherheit sind (publicIsClear).
Mit welchem Test ließe sich dieser Programmfehler finden?
Schauen wir uns einen Test an, bei dem der Auslöser betätigt wird, wenn Techniker und Zuschauer in Sicherheit sind. Der
Code bewirkt, dass die Bombe detoniert. Der richtige Code (unter Verwendung von &&) würde jedoch auch eine Detonation bewirken. Dieser Test ist also nicht geeignet, den
genannten Fehler zu finden.
Wenn sich Techniker und Zuschauer direkt neben der Bombe befinden, zeigt der fehlerhafte Code ebenfalls die gewünschte
Wirkung, denn die Bombe detoniert nicht.
Um den Fehler aufzudecken, benötigen wir einen Anwendungsfall, bei dem die Auswertung des Codes in der bestehenden Form
zu einem anderen Ergebnis führt als die Auswertung des korrekten Codes. Ein solcher Fall wäre gegeben, wenn die
Zuschauer in Sicherheit sind, der Techniker sich jedoch noch in der Nähe der Bombe befindet. In der folgenden Tabelle
sind alle Tests zusammengefasst:
publicIsClear
|
technicianClear
|
Code in bestehender Form...
|
Korrigierter Code...
|
|
true
|
true
|
Detonation
|
Detonation
|
Der Test ist sinnlos (für diesen Fehler).
|
true
|
false
|
Detonation
|
keine Detonation
|
sinnvoller Test
|
false
|
true
|
Detonation
|
keine Detonation
|
sinnvoller Test
|
false
|
false
|
keine Detonation
|
keine Detonation
|
Der Test ist sinnlos (für diesen Fehler).
|
Die beiden mittleren Tests sind geeignet, diesen speziellen Fehler zu finden. Da die Tests redundant sind, reicht es
aus, einen von ihnen durchzuführen, um den Fehler zu finden.
Es gibt weitere Fehlermöglichkeiten für den Ausdruck. Nachfolgend sind häufige Fehler in Booleschen Ausdrücken
aufgelistet. Die Fehler auf der linken Seite können alle mit dem hier beschriebenen Verfahren erkannt werden, die
Fehler auf der rechten Seite jedoch nicht mit Sicherheit. Das Verfahren kann also nicht alle Fehler aufdecken, ist aber
dennoch sinnvoll und hilfreich.
Erkannte Fehler
|
Möglicherweise nicht erkannte Fehler
|
Falscher Operator: a || b sollte durch a&&b ersetzt werden.
|
Falsche Variable verwendet: a&&b&&c sollte durch
a&& x&&d ersetzt werden.
|
Fehlende oder falsche Negation: a||b sollte durch !a||b oder ! a||b durch a||b ersetzt werden.
|
Zu einfacher Ausdruck: a&&b sollte durch a&&b&&c ersetzt werden.
|
Fehlende Klammern im Ausdruck: a&&b||c sollte durch a&&(b||c) ersetzt werden.
|
Ausdrücke, die mehr als einen Fehler aus der linken Spalte enthalten
|
Zu komplexer Ausdruck: a&&b&&c sollte durch a&&b
ersetzt werden.
(Dieser Fehler ist nicht sehr wahrscheinlich, aber mit Tests, die für die übrigen Fehler
geeignet sind, leicht feststellbar.)
|
|
Wie können diese Ideen genutzt werden? Nehmen wir an, uns liegt ein Boolescher Ausdruck wie a&&!b vor. Für diesen Ausdruck könnten Sie eine Wahr-Falsch-Tabelle wie die folgende
konstruieren:
a
|
b
|
a&&!b
(Code in bestehender Form)
|
Sollte vielleicht wie folgt lauten:
a||!b
|
Sollte vielleicht wie folgt lauten:
!a&&!b
|
Sollte vielleicht wie folgt lauten:
a&&b
|
...
|
true
|
true
|
false
|
true
|
false
|
true
|
...
|
true
|
false
|
true
|
true
|
false
|
false
|
...
|
false
|
true
|
false
|
false
|
false
|
false
|
...
|
false
|
false
|
false
|
true
|
true
|
false
|
...
|
Wenn Sie sich alle Varianten durchgearbeitet haben, werden Sie feststellen, dass nur die erste, zweite und vierte
Möglichkeit in Frage kommen. Mit dem dritten Ausdruck lassen sich keine Fehler feststellen, die nicht auch mit den
anderen Ausdrücken erkannt werden können, so dass der dritte Ausdruck nicht benötigt wird. (Wenn Sie es mit
komplizierteren Ausdrücken zu tun haben, kommen die Vorteile der Ermittlung von nicht notwendigen Fällen schnell zum
Tragen.)
Natürlich würde niemand ernstlich eine solche Tabelle erstellen, und Sie werden froh sein, dass Ihnen diese Arbeit
erspart bleibt. Bei einfachen Ausdrücken lassen sich die erforderlichen Fälle leicht überblicken. (Schauen Sie sich den
nächsten Abschnitt an.) Wenn Sie es mit komplexeren Ausdrücken wie A&&B||C zu tun haben,
sollten Sie sich den Abschnitt Testideen für Ausdrücke mit kombiniertem logischem UND und logischem ODER ansehen. Er
listet Testideen für Ausdrücke mit zwei oder drei Operatoren auf. Bei noch komplexeren Ausdrücken können Sie ein Programm zum Generieren von Testideen
verwenden.
Tabellen für einfache Boolesche Ausdrücke
Wenn der Ausdruck A&&B lautet, testen Sie Folgendes:
A
|
B
|
true
|
true
|
true
|
false
|
false
|
true
|
Wenn der Ausdruck A||B lautet, testen Sie Folgendes:
A
|
B
|
true
|
false
|
false
|
true
|
false
|
false
|
Wenn der Ausdruck A1 && A2 && ... && An
lautet, testen Sie Folgendes:
A1, A2... und An sind wahr (true).
|
A1 ist falsch (false), der Rest ist wahr (true).
|
A2 ist falsch (false), der Rest ist wahr (true).
|
...
|
An ist falsch (false), der Rest ist wahr (true).
|
Wenn der Ausdruck A1 || A2 || ... || An lautet, testen Sie
Folgendes:
A1, A2... und An sind falsch (false).
|
A1 ist wahr (true), der Rest ist falsch (false).
|
A2 ist wahr (true), der Rest ist falsch (false).
|
...
|
An ist wahr (true), der Rest ist falsch (false).
|
Wenn der Ausdruck A lautet, testen Sie Folgendes:
Wenn Sie also a&&!b testen müssen, können Sie die erste der obigen Tabellen anwenden,
die Richtung von b umkehren (da b negiert wird), und erhalten die folgende Liste mit
Testideen:
-
A true, B false
-
A true, B true
-
A false, B false
Vergleichsausdrücke
Das folgende Codebeispiel enthält wiederum einen Fehler:
if (finished < required) {
siren.sound();
}
< sollte wie folgt lauten: <=. Solche Fehler kommen ziemlich häufig
vor. Sie können wie bei Booleschen Ausdrücken eine Tabelle mit Testwerten konstruieren und sich anschauen, bei welchen
Werten der Fehler erkannt wird:
finished
|
required
|
Code in bestehender Form...
|
Korrigierter Code...
|
1
|
5
|
Sirene ertönt
|
Sirene ertönt
|
5
|
5
|
Sirene ertönt nicht
|
Sirene ertönt
|
5
|
1
|
Sirene ertönt nicht
|
Sirene ertönt nicht
|
Ganz allgemein lässt sich sagen, der Fehler wird erkannt, sobald finished=required gilt. Aus der
Analyse plausibler Fehler können wir die folgenden Regeln für Testideen verwenden:
Wenn der Ausdruck A<B oder A>=B lautet, ist Folgendes zu testen:
A=B
|
A etwas kleiner als B
|
Wenn der Ausdruck A>B oder A<=B lautet, ist Folgendes zu testen:
Was bedeutet hier "etwas größer/kleiner"? Wenn A und B ganze Zahlen sind, sollte A um eins kleiner oder größer als B
sein. Wenn es sich um Dezimalzahlen handelt, sollte A nur unwesentlich kleiner oder größer als B sein (was nicht
zwingend bedeutet, dass die Abweichung nur bei einer Stelle nach dem Komma liegen darf.)
Regeln für Kombinationen aus Booleschen Ausdrücken und
Vergleichsausdrücken
Die meisten Vergleichsoperatoren kommen wie im folgenden Beispiel auch in Booleschen Ausdrücken vor:
if (finished < required) {
siren.sound();
}
Die Regeln für Vergleichsausdrücke würden zu den folgenden Testideen führen:
-
finished ist gleich required
-
finished ist etwas kleiner als Ausdruck required
Die Regeln für Boolesche Ausdrücke würden zu den folgenden Testideen führen:
-
finished < required sollte wahr (true) sein.
-
finished < required sollte falsch (false) sein.
Es ist nöglich, dass finished etwas kleiner als required ist, und finished < required ist wahr (true). Da das Ergebnis beide Male true ist, muss diese Testidee nicht
aufgeschrieben werden.
Wenn der Ausdruck falsch (false) ist, macht es keinen Sinn, finished ist gleich required, finished < required als Testidee zu notieren, da das Ergebnis
beide Male false ist.
Wenn ein Vergleichsausdruck keine Booleschen Operatoren enthält (&& und ||), können Sie die Tatsache, dass sich gleichzeitig um einen Booleschen Ausdruck handelt, ignorieren.
Bei Kombinationen von Booleschen Operatoren und Vergleichsoperatoren wird es etwas komplizierter. Schauen Sie sich dazu
folgenden Ausdruck an:
if (count<5 || always) {
siren.sound();
}
Aus dem Vergleichsausdruck können Sie Folgendes ableiten:
-
count ist etwas kleiner als 5
-
count ist gleich 5
Aus dem Booleschen Ausdruck können Sie Folgendes ableiten:
-
count<5 true, always false
-
count<5 false, always true
-
count<5 false, always false
Diese Ergebnisse können zu drei genaueren Testideen zusammengefasst werden. (Beachten Sie, dass count hier eine ganze Zahl ist.)
-
count=4, always false
-
count=5, always true
-
count=5, always false
Wie Sie sehen, wird count=5 zweimal verwendet. Es mag sinnvoller erscheinen, den Ausdruck mit
der 5 nur einmal und zusätzlich eine Gleichung mit einem anderen Wert zu verwenden, bei der das Ergebnis falsch ist (z.
B. count<5. Es ist einen Versuch wert, jedoch nicht ungefährlich, denn dabei kann Ihnen
leicht ein Fehler unterlaufen. Nehmen wir an, Sie probieren Folgendes aus:
-
count=4, always false
-
count=5, always true
-
count<5 false, always false
Gehen wir nun davon aus, dass es einen Fehler gibt, der nur bei count=5 festgestellt werden
kann, so dass der Wert 5 im Ausdruck count<5 zu dem Ergebnis "false" führen, der richtige
Code jedoch das Ergebnis "true" haben würde. Dieses Ergebnis false wird jedoch gleich im Anschluss durch logisches ODER
mit dem Wert von "always" verknüpft und ergibt so wieder "true". Der Wert des gesamten Ausdrucks ist somit korrekt,
obwohl der Wert des Unterausdrucks (Vergleichsausdrucks) falsch war. Der Fehler bleibt unerkannt.
Der Ausdruck "always=true" führt dazu, dass das Problem verdeckt wird, was nicht geschieht, wenn Sie die Kombination
von count=5 und always=false testen.
Ähnliche Probleme können auftreten, wenn sich der Vergleichsausdruck auf der rechten Seite eines Booleschen Operators
befindet.
Da sich kaum genau feststellen lässt, welche Unterausdrücke exakt sein müssen und welche allgemein sein können, ist es
am besten, wenn alle Unterausdrücke exakt sind. Alternativ können Sie das oben erwähnte Programm für Boolesche Ausdrücke verwenden.
Er erzeugt korrekte Testideen für beliebige Kombinationen aus Booleschen Ausdrücken und Vergleichsausdrücken.
Testideen ohne Code
Wie im Abschnitt Konzept:
Test-First-Design erläutert, sollten Tests vor der Codeimplementierung entworfen werden. Die Verfahren werden zwar
ausgehend von Codebeispielen entwickelt, jedoch in der Regel ohne Code angewendet. Wie ist das möglich?
Bestimmte Designartefakte, z. B. Zustands- und Ablaufdiagramme, verwenden Boolesche Ausdrücke als Wächter. Diese Fälle
sind eindeutig und leicht zu handhaben. Fügen Sie einfach die Testideen für Boolesche Ausdrücke zur Prüfliste der
Testideen für das Artefakt hinzu. Vergleichen Sie hierzu den Abschnitt Richtlinie für Arbeitsergebnisse: Testideen für Zustandsdiagramme und
Ablaufdiagramme.
Tückisch sind dagegen Fälle mit Booleschen Ausdrücken, die eher implizit als explizit enthalten sind. Dies kommt häufig
in Beschreibungen von APIs vor. Schauen Sie sich als ein Beispiel die folgende Methode an:
List matchList(Directory d1, Directory d1,
FilenameFilter excluder);
Das Verhalten dieser Methode könnte wie folgt beschrieben werden:
Die Methode gibt eine Liste der absoluten Pfadnamen aller Dateien zurück, die in beiden Verzeichnissen enthalten
sind. Dabei werden alle Unterverzeichnisse berücksichtigt. [...] Dateinamen, die mit dem excluder übereinstimmen, werden von der zurückgegebenen Liste ausgeschlossen. Der excluder wird
nur auf die Basisverzeichnisse angewendet, nicht jedoch auf die Namen der Dateien in Unterverzeichnissen.
Die Wörter UND und ODER kommen nicht vor. Ein Dateiname wird aber nur dann in die zurückgegebene Liste aufgenommen,
wenn er im ersten Verzeichnis UND im zweiten Verzeichnis erscheint UND entweder in einem Unterverzeichnis
enthalten ODER nicht explizit ausgeschlossen ist. Als Code könnte dies wie folgt aussehen:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
Die Testideen für diesen Ausdruck sind in der folgenden Tabelle zusammengefasst:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
false
|
false
|
false
|
true
|
false
|
false
|
Der generelle Ansatz für die Erkennung impliziter Boolescher Ausdrücke in Texten ist, zunächst die beschriebenen
Aktionen aufzulisten (z. B. "gibt einen passenden Namen zurück"). Anschließend können Sie einen Booleschen Ausdruck für
die Fälle schreiben, in denen eine der Aktionen ausgeführt wird. Leiten Sie dann aus allen Ausdrücken Testideen ab.
In diesem Prozess können unterschiedliche Meinungen auftauchen. Eine Person könnte beispielsweise den oben verwendeten
Booleschen Ausdruck aufschreiben. Eine andere Person könnte dagegen der Meinung sein, dass es eigentlich zwei
verschiedene Aktionen gibt. Zunächst findet das Programm passende Namen und anschließend filtert es diese Namen.
Anstelle eines Ausdrucks hätten wir dann zwei:
-
Übereinstimmung finden:
-
Dies geschieht, wenn sich eine Datei im ersten Verzeichnis befindet UND im zweiten Verzeichnis eine Datei
desselben Namens enthalten ist.
-
Übereinstimmung filtern:
-
Dies geschieht, wenn sich die übereinstimmenden Dateien auf der Ausgangsebene befinden UND ihr Name dem
excluder entspricht.
Diese unterschiedlichen Herangehensweisen können verschiedene Testideen und so auch verschiedene Tests nach sich
ziehen. Die Unterschiede werden jedoch höchstwahrscheinlich nicht von großer Relevanz sein. Anstatt sich lange Gedanken
zu machen, welcher Ausdruck wohl der richtige ist und welche Alternativen es gibt, sollten Sie die Zeit lieber in
andere Verfahren und die Erstellung weiterer Tests investieren. Sollten Sie sich dennoch genauer für die Unterschiede
interessieren, lesen Sie hier weiter.
Die zweite Person würde zwei Gruppen von Testideen ableiten.
Testideen zur Suche nach einer Übereinstimmung:
-
Datei im ersten Verzeichnis, Datei im zweiten Verzeichnis (true, true)
-
Datei im ersten Verzeichnis, Datei nicht im zweiten Verzeichnis (true, false)
-
Datei nicht im ersten Verzeichnis, Datei im zweiten Verzeichnis (false, true)
Testideen zum Filtern einer Übereinstimmung (nachdem sie gefunden wurde):
-
übereinstimmende Dateien befinden sich auf der Ausgangsebene, ihr Name entspricht dem excluder (true, true)
-
übereinstimmende Dateien befinden sich auf der Ausgangsebene, ihr Name entspricht nicht dem excluder (true, false)
-
übereinstimmende Dateien befinden sich auf einer untergeordneten Ebene, ihr Name entspricht dem excluder (false, true)
Nehmen wir an, diese beiden Gruppen von Testideen werden miteinander kombiniert. Die Ideen der zweiten Gruppe sind nur
von Relevanz, wenn eine Datei in beiden Verzeichnissen enthalten ist, und können somit nur mit der ersten Idee aus der
ersten Gruppe kombiniert werden. Das führt uns zu Folgendem:
Datei im ersten Verzeichnis
|
Datei im zweiten Verzeichnis
|
Ausgangsebene
|
Übereinstimmung mit excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
Zwei der Ideen für die Suche nach einer Übereinstimmung erscheinen nicht in dieser Tabelle. Wir können sie wie folgt
hinzufügen:
Datei im ersten Verzeichnis
|
Datei im zweiten Verzeichnis
|
Ausgangsebene
|
Übereinstimmung mit excluder
|
true
|
true
|
true
|
true
|
true
|
true
|
true
|
false
|
true
|
true
|
false
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
Leere Zellen zeigen an, dass die Spalten nicht von Bedeutung sind.
Jetzt sieht die Tabelle so ähnlich wie die der ersten Person aus. Die Ähnlichkeit wird noch deutlicher, wenn dieselbe
Terminologie verwendet wird. Die Tabelle der ersten Person enthält eine Spalte "inLower" und die der zweiten Person
eine Spalte "Ausgangsebene". Wenn wir die Spalten konvertieren, indem wir die Richtung der Werte umkehren, erhalten wir
die folgende Version der zweiten Tabelle:
appearsInFirst
|
appearsInSecond
|
inLower
|
excluded
|
true
|
true
|
false
|
true
|
true
|
true
|
false
|
false
|
true
|
true
|
true
|
true
|
true
|
false
|
-
|
-
|
false
|
true
|
-
|
-
|
Die drei ersten Zeilen sind mit der Tabelle der ersten Person identisch. Die letzten beiden Zeilen weichen nur in
sofern ab, als sie Werte enthalten, die in der Tabelle der ersten Person fehlen. Dies lässt Rückschlüsse darauf zu, wie
der Code geschrieben wurde. Die erste Person ist von einem komplizierten Booleschen Ausdruck ausgegangen:
if (appearsInFirst && appearsInSecond &&
(inLowerLevel || !excluded)) {
add to list
}
Die zweite Person geht von verschachtelten Booleschen Ausdrücke aus:
if (appearsInFirst && appearsInSecond) {
// Übereinstimmung gefunden
if (inTopLevel && excluded) {
// Übereinstimmung filtern
}
}
Beide unterscheiden sich dahingehend, dass mit den Testideen der ersten Person zwei Fehler gefunden werden, die sich
mit den Testideen der zweiten Person nicht feststellen lassen, weil sie in dessen Ideen nicht zum Tragen kommen.
-
In der ersten Implementierung kann es einen Klammerfehler geben. Ist es richtig oder falsch, || in Klammern zu setzen? Da es in der zweiten Implementierung weder Klammern noch || gibt, kann der Fehler nicht vorkommen.
-
Mit den zu testenden Anforderungen für die erste Implementierung wird überprüft, ob der zweite Ausdruck && durch || ersetzt werden sollte. In der zweiten Implementierung
wird das explizite && durch das implizite && ersetzt.
An sich gibt es somit keinen Ersetzungsfehler (Ersetzung von && durch ||). (Es könnte sein, dass die Verschachtelung nicht korrekt ist, aber das kann mit diesem
Verfahren nicht erkannt werden.)
|