3: Systematisches Testen Testen ist ein Prozess, ein Programm mit der Absicht auszuführen, Fehler zu finden. Man muss destruktiv denken! Testen ist ungeeignet, die Fehlerfreiheit eines Programms nachzuweisen! Testen ist keine Form des Debuggens! Die systematische Auswahl von Testfällen ist eine schwierige kreative Arbeit. Testergebnisse sollen reproduzierbar sein. Das Durchführen von Tests kann man oft automatisieren. Und man sollte es automatisieren, wenn möglich. 150
Ich habe einen Fehler in meinem Programm gefunden. Was soll ich tun? NICHT: Den Fehler sofort beheben, den Testfall vergessen. Stattdessen: 1. Den Testfall T, der zu dem Fehler führte, schriftlich festhalten, um den Fehler reproduzieren können. ( ) 2. Den Test weiterführen und nach weiterern Fehlern suchen. 3. Den Testfall T in den Regressionstest aufnehmen! 4. Jetzt den Fehler korrigieren. 5. Anhand des Testfalls T nachprüfen, ob der Fehler tatsächlich korrigiert wurde. ( ) Leider gibt es auch Heisenbugs... 151
Kein Testen, aber auch nützlich: Untersuchung des Source-Codes Code-Inspectionen (strenge Regeln, Entscheidung über die Freigabe des Source-Codes) Eine Anomalie wie if ((options == ( WCLONE WALL)) && (current->uid = 0)) retval = -EINVAL; hat in dem Source-Code für ein sicheres System nichts zu suchen! Walk-Throughs (weniger strikte Regeln) Programming in Pairs Formale Verifikation ( in ein paar Wochen) 152
Testpsychologie Man muss die Fehler finden wollen! Idealerweise Trennung zwischen Programmautoren und Testern. Auch hilfreich: Test-Driven Developement (zuerst die Black-Box-Tests, danach die Implementation). Es ist gut, wenn ein Test einen Fehler findet. Erfolg = Fehler gefunden! 153
Regressionstest Batterie von Testfällen, die im Laufe eines Projekts wächst, weil für jeden neu nachgewiesenen Fehler ein Testfall aufgenommen wird. Möglichst viele Testfälle sollten automatisiert bearbeitet werden. Änderungen werden nur freigegeben, wenn der Regressionstest fehlschlägt (keine Fehler findet). Wird ein Fehler entdeckt, der vom Regressionstest bisher nicht gefunden wurde: Zuerst neuer Testfall für den Regressionstest. Erst dann Fehler beheben. (Das ist wichtig! Deshalb die Wiederholung des zuvor Gesagten.) Denn: Fehler kommen oft wieder! 154
Testfall Beschreibung einer Eingabe(-Bedingung) mit einem erwarteten Ergebnis Zweck Vorbedingung Erwartetes Ergebnis Aufräumarbeiten Bei der Durchführung des Tests: Vergleich des tatsächlichem mit dem Erwarteten Ergebnis, Protokollierung. Ggf. Verknüpfung mehrerer Testfälle zu einer Kette. 155
Beispiel: Wie sollte man diese Funktion testen? function Fibonacci(X: Positive) return Positive is Kind, Vater, Opa: Positive; begin if X <= 2 then return 1; else Vater := 1; Opa := 1; for Generation in 3.. X loop Kind := Vater + Opa; Opa := Vater; Vater := Kind; end loop; return Kind; end if; end Fibonacci; 156
3.1: White-Box Tests Tests, die mit Hilfe der Kenntnis eines (Unter-)Programms definiert werden, mit Hilfe des Kokntrollflussgraphen: Knoten: Ausführbare Anweisung (Zuweisung, Prozeduraufruf) zusätzlich je ein Start- und Zielknoten ggf. Zusammenfassung mehrerer zusammenhängender Anweisungen zu einem Knoten Kante: möglicher Programmfluss zwischen zwei Anweisungen Pfad: Kombination von Kanten, die vom Start- zum Zielknoten laufen 3.1: White-Box Tests 157
Kriterien für White-Box Tests Kontrollflussorientierte Tests: Anweisungsüberdeckung: Jeder Knoten des Kontrollflussgraphen wird mindestens einmal ausgeführt. Zweigabdeckung: Jede Kante des Kontrollflussgraphen wird mindestens einmal ausgeführt. Pfadabdeckung: Jeder Pfad im Kontrollflussgraphen wird mindestens einmal ausgeführt (theoretisches Kriterium, da meistens unendlich viele Pfade). Datenflussorientierte Tests (übernächste Folie) 3.1: White-Box Tests 158
Pfadabdeckung mit Schleifen Programme mit Schleifen können unendlich viele Pfade haben. Das würde undentlich viele Tests implizieren. Varianten der Pfadabdeckung mit endlich vielen Pfaden: Boundary Interior : Zwei Pfade pro Schleife: Verlassen der Schleife bei der ersten Gelegenheit (while-schleife wird keinmal durchlaufen, repeat-until-schleife wird einmal durchlaufen). Verlassen der Schleife bei der zweiten Gelegenheit (while-schleife einmal, repeat-until-schleife zweimal). Strukturierte Pfadabdeckung mit Parameter k. Für alle j {1,..., k}: Verlassen der Schleife bei der j-ten Gelegenheit. 3.1: White-Box Tests 159
Datenfluss Idee: Eine Variable V wird entweder verändert (z.b. V :=... ) oder lesend benutzt (z.b.... := V ). Daten fließen von der Veränderung zur Benutzung. Testen (Datenfluss-orientiert): Betrachte Pfade, die von einer Veränderung ohne weitere Veränderung zur Benutzung führen. 3.1: White-Box Tests 160
Datenflussorientierte Tests Unterscheide 1. def: Wertzuweisung an eine Variable 2. use: lesende Benutzung einer Variablen. Weitere Unterscheidung: 2.1 c(omputational)-use: Der Wert der Variablen ist Teil einer Berechnung 2.2 p(redicate)-use: Der Wert der Variablen beeinflusst eine Entscheidung 3.1: White-Box Tests 161
function Fibonacci(X: Positive) return Positive is Kind, Vater, Opa: Positive; begin def(x) if X <= 2 then p-use(x) return 1; else Vater := 1; def(vater) Opa := 1; def(opa) for Generation in 3..X def(generation), p-use(generation, X) loop Kind := Vater + Opa; c-use(vater, Opa), def(kind) Opa := Vater; c-use(vater), def(opa) Vater := Kind; c-use(kind), def(vater) end loop; return Kind; c-use(kind) end if; end Fibonacci; 3.1: White-Box Tests 162
Das all-defs Kriterum Für jede Variable V und jede def(v)-anweisung gibt es mindestens einen Pfad in den Testfällen, der bezüglich V definitionsfrei ist und in einer use(v)-anweisung mündet. Ein Pfad ist bezüglich V definitionsfrei, wenn auf dem Pfad keine def(v)-anweisung auftritt. Idee: Jede Variable V hat (hoffentlich!) einen semantischen Nutzen, der wenigstens in einem Testfall überprüft werden soll. 3.1: White-Box Tests 163
all-uses und weitere Kriterien all-uses: Für alle Variablen V und alle Paare (X, Y ) (X eine def(v)-anweisung und Y eine use(v)-anw.) gilt: Wenn mindestens ein bezüglich V definitionsfreier Pfad von X nach Y existiert, dann muss ein solcher Pfad in einem der Testfälle auftreten. Starkes Kriterium, aber seeeehr viele Testfälle. Sparsamer: all-p-uses (berücksichtigt nur p-use(v), ignoriert c-use) und all-c-uses (genau umgekehrt). Nicht klar was besser: all-p-uses oder all-c-uses. Künstliche Unterscheidung zw. p-use und c-use. 3.1: White-Box Tests 164
Testen: Die Praxis Verbreitet: Black-Box-Tests ( nächster Abschn.). Kontrollflussorientierte Tests oft genutzt (und z.t. vorgeschrieben) für die Entw. Sicherer Systeme. Typisches Vorgehen: Starte mit den Testfällen eines gründlich entwickelten Black-Box-Tests. Ermittle (mit Hilfe von Werkzeugen) die anfängliche Testabdeckung (bei reinen Black-Box Tests typischerweise 60 80%). Ergänze die Testfälle, um die Testabdeckung auf einen vorgegebenen Minimalwert zu bringen (typischerweise 90-95%). Datenflussorientierte Tests: theoretisches Konzept, in der Praxis (bisher?) selten, kaum Werkzeuge. 3.1: White-Box Tests 165
Mutationstesten Etwas unsystematische, aber wirkungsvolle Methode, die Vollständigkeit der Testbatterie zu testen. Grundlage: Kleine Änderungen ( Mutationen ) am Quelltext. Jeder Mutant enthält nur eine Abweichung, z.b. Ersetzen eines durch ein <, eines and durch ein or,... Vorzeichenfehler, Auskommentieren einzelner Statements, Vertauschen der Reihenfolge einzelner Statements,... Kriterium: Wenn irgend eine Mutation den Test besteht, hat man vermutlich nicht genug Testfälle, oder die falschen Testfälle. 3.1: White-Box Tests 166
3.2: Black-Box Tests Tests, die anhand der Spezifikation eines (Unter-)Programms definiert, werden, ohne das Programm selbst zu kennen. Techniken zur Erzeugung von Testfällen: Äquivalenzklassenbildung (sowohl für gültige, als auch für ungültige Eingaben) Grenzwertanalyse Beispiel: Wie testet man die ein (Unter-)Programm, das ein Datum in der Form (Tag, Monat, Jahr) einlesen und bei ungültigen Daten eine Ausnahme auslösen soll: 3.2: Black-Box Tests 167
Beispiel Kaffeeautomat package Kaffee_Automat is type Zustand is private; type Benutzer_Handlung is (Cent10, Cent20, Knopf); type Aktionen is (Muenze_Angenommen, Muenze_Abgelehnt, Alle_Muenzen_Zurueck, Gib_Kaffee_Aus); 3.2: Black-Box Tests 168
Kaffeeautomat (2) procedure Initialisiere (X: in out Zustand); procedure Agiere(X: in out Zustand; Eingabe: Benutzer_Handlung; Aktion: out Aktionen); private type Zustand is range 0.. 50; end Kaffee_Automat; 3.2: Black-Box Tests 169
Wie testen wir den Kaffeeautomaten? Spezifikationen, wie für derartige Automaten üblich. Insbesondere: Eingabe 10-Cent und 20-Cent Münzen Kaffee für 50 Cent, kein Überbezahlen Geldrückgabeknopf Gibt es Äquivalenzklassen, die wir hier nutzen können? 3.2: Black-Box Tests 170
3.3: Fallbeispiel: Fakultät Man gebe einen Black-Box-Test für die folgende Funktion an: function Fak1(N: Positive) return Positive is return N! 3.3: Fallbeispiel 171
Findet der Black-Box-Test den Fehler? function Fak2(N: Positive) return Positive is optimised variant of Fak1 begin case N is when 1 => return 1; when 2 => return 2; when 3 => return 6; when 4 => return 24; when 5 => return 120; when 6 => return 270; Error: 6!=720 when 7 => return 5040; when others => return N * Fak2(N-1); end case; end Fak2; 3.3: Fallbeispiel 172
Hier kann auch ein White-Box-Test versagen! function Fak3(N: Positive) return Positive is yet another optimised Fak1 Opt: array (Positive range 1..7) of Positive := (1,2,6,24,120,270,5040); begin if N in Opt Range then return Opt(N); else return N*Fak3(N-1); end if; end Fak3; 3.3: Fallbeispiel 173
3.4: Automatisches Testen Manuelles Testen ist arbeitsaufwändig, langweilig und... fehlerträchtig. Und wer will nach jeder (kleinen) Änderung den kompletten Regressionstrest durchführen... Um die Reproduzierbarkeit der Test zu gewährleisten, muss bei jeder Wiederholung des Tests (z.b. Regressionstest), der gleiche Input eingegeben werden. 3.4: Automatisches Testen 174
Der Test-Driver Generator testgen Ein einfach zu handhabendes Werkzeug, um Test-Driver zu erzeugen ist der Test-Driver Generator testgen (eine Weiterentwicklung von tg. Er dient als Beispiel für ein Werkzeug beim Testen. Für anspruchsvollere Aufgaben würde man z.b. AUnit verwenden. 3.4: Automatisches Testen 175
Arbeitsweise von testgen Eingabe von testgen ist ein Test-Script (z.b. beispiel.ts), Ausgabe der Quelltest eines Ada-Programms (z.b. beispiel.adb). Typische Test-Scripts bestehen aus 1. Einer globalen Kontext-Vereinbarung, bestehend aus with und use -Klauseln für Ada.Text_IO und dem getesteten Modul, und 2. einzelnen Testfällen. 3.4: Automatisches Testen 176
Die Syntax eines Test-Scripts Context with Ada.Text_IO; use Ada.Text_IO; with Datum_Manager; use Datum_Manager; ***** 30. Juni 2003 (kein Schaltjahr) Define Dat: Datum; Test Dat := Setze(30, Juni, 2003); Pass (Tag(Dat)=30) and (Monat(Dat)=Juni) and (Jahr(Dat)=2003) ***** 31. Juni 2003 (kein Schaltjahr) Define Dat: Datum; Test Dat := Setze(31, Juni, 2003); Pass exception Falsches_Datum 3.4: Automatisches Testen 177
Die Syntax eines Testfalls Testfall-Name, Define-, Test- und Pass-Vereinbarungen: ***** Mai, alle Tage, 2003 (kein Schaltjahr) Define Fehler: Natural := 0; Dat: Datum; Test for T in Tag_Typ loop Dat := Setze(T, Mai, 2003); if (Tag(Dat) /= T) or (Monat(Dat) /= Mai) or (Jahr(Dat) /= 2003) then Fehler := Fehler + 1; end if; end loop; Pass Fehler = 0 3.4: Automatisches Testen 178
Verhalten eines Test-Scripts (1) pass. (2) pass. (3) pass. (4) Februar, 1..28, 2003 (kein Schaltjahr) Script name: dat_man.ts ; Line:34... FAIL. (path => was taken, but predicate is FALSE) (5) pass. (6) pass. (7) pass. Script name dat_man.ts ; total test result: FAIL. Hurra! Der Test war erfolgreich Fehler gefunden! 3.4: Automatisches Testen 179