Zufallsbasiertes Testen (QuickCheck)

Werbung
Westfälische Wilhelms-Universität Münster
Ausarbeitung
Zufallsbasiertes Testen (QuickCheck)
im Rahmen des Seminars Software Engineering
Christian Binkhoff
Abgabetermin: 2008-12-10
Themensteller: Prof. Dr. Herbert Kuchen
Betreuer: Prof. Dr. Herbert Kuchen
Institut für Wirtschaftsinformatik
Praktische Informatik in der Wirtschaft
Inhaltsverzeichnis
1
Einleitung................................................................................................................... 1
2
Grundlagenkapitel...................................................................................................... 2
3
2.1
Haskell .............................................................................................................. 2
2.2
Monaden ........................................................................................................... 3
2.3
Testen................................................................................................................ 6
QuickCheck ............................................................................................................... 8
3.1
Konzept............................................................................................................. 8
3.2
Grundlegende Implementierung ....................................................................... 9
3.2.1
3.2.2
3.3
3.3.1
3.3.2
Bedingte Properties..................................................................................... 11
Überwachung der Inputparameter .............................................................. 12
Selbstdefinierte Generatoren........................................................................... 14
Eigene Generatoren..................................................................................... 15
Binärer Suchbaum....................................................................................... 16
4
Bewertung von QC .................................................................................................. 21
5
Anhang..................................................................................................................... 22
5.1
BinaerBaum .................................................................................................... 22
5.2
QuickCheck .................................................................................................... 22
5.3
Testfaelle......................................................................................................... 23
Literaturverzeichnis ........................................................................................................ 25
II
Kapitel 1: Einleitung
1 Einleitung
Das Testen von konzipierten Programmen und von Software im Allgemeinen hat hohe
Relevanz, um Qualität und Robustheit gewährleisten zu können. Auf Grund dessen und
des Umfangs einer durchzuführenden Testphase entstehen, bezogen auf die
Entwicklung, hohe Kosten:
Testing accounts “[..] for up to 50% of the cost of software development”. [CH00, S. 1]
Das führt zu der Tatsache, dass automatisches Testen in den Fokus der Betrachtung
rückt, um den Testaufwand reduzieren zu können. Zufallsbasierte Testmethoden
implizieren kein starres Definieren von Testfällen, so dass ein Testlauf durchschnittlich
in kürzerer Zeit möglich ist. [CH00, S. 1] Eine besondere Eignung erfahren deklarative
Programmiersprachen im Bereich des zufallsbasierten Testens [CH00, S. 1]. Das
Grundlegende Ziel dieser Arbeit ist die Darstellung und Bewertung eines Tools als
Umsetzung dieser Testmethodik.
Das Grundlagenkapitel gliedert sich in drei Abschnitte, wobei zunächst der Begriff der
deklarativen Programmiersprachen erläutert und eine bestimmte Sprache Haskell kurz
vorgestellt und anschließend eine Technik der Monaden veranschaulicht wird, mit der
Nachteile dieser Sprache beseitigt werden und später eine Grundlage für automatische
Tests geschaffen wird. Der dritte Teil des Grundlagenkapitels beschäftigt sich mit dem
Begriff Testen im Allgemein und hat die Einordnung des Prinzips des zufallsbasierten
Testens, gemessen an ihrem Einsatz, als Ziel.
Der Hauptteil erläutert das Tool QuickCheck als zufallsbasiertes Testinstrument, das auf
der Sprache Haskell aufbaut. Zu allererst wird das Konzept von diesem Tool vorgestellt,
anschließend erfolgt ein Einblick in die Syntax der Beschreibung eines Tests und ihrer
Implementierung. Der dritte Teil Selbstdefinierte Generatoren veranschaulicht die
Notwendigkeit und Möglichkeit des Einsatzes von eigenen Generatoren und soll die
Berücksichtigung von Monaden verdeutlichen. Eine praktische Illustration soll durch
ein abschließendes, umfassendes Beispiel zu binären Suchbäumen erfolgen.
Das inhaltlich letzte Kapitel Bewertung von QC hat eine abschließende Rekapitulation
von QuickCheck mit Vor- und Nachteilen und Darstellung von
Erweiterungsmöglichkeiten bzw. anderen Einsatzgebieten zum Ziel.
1
Kapitel 2: Grundlagenkapitel
2 Grundlagenkapitel
2.1 Haskell
Haskell ist eine deklarative Programmiersprache, d. h. die Konstruktion eines
Programms bzgl. eines Problems erfolgt nicht explizit, sondern geschieht durch die
Formulierung von Definitionen bzw. Funktionen im mathematischen Sinn. Es liegt dann
an der bereitgestellten Software, bei der Ausführung des Programms zu evaluieren, also
die entsprechenden Definitionen abzurufen und umzusetzen. [Bi98, S. 1] Daher sind in
Haskell entwickelte Programme zunächst nicht zustandsbehaftet. Erfolgt eine
Ausführung eines Programms, so werden die Funktionen mit entsprechenden
Eingabeparametern evaluiert und eine Ausgabe entsprechend der Definition gemacht.
Das bedeutet, dass bei jedem neuen Aufruf mit denselben Eingabewerten die von
Haskell durchgeführte Evaluation zu demselben Ergebnis kommt. Die zu Grunde
liegende Definitionsstruktur bleibt unberührt. Auf Grund der Tatsache der impliziten
Definition eines Problems kommt häufig das Instrument der Rekursion zum Einsatz,
wie das folgende Beispiel illustrieren soll, das für ein Eingabewert n zum Ergebnis der
Summe [0..n] führt. Die erste Zeile stellt dabei die Deklaration der Funktion, genannt
„Spezifikation“ [Bi98, S. 25], dar, die zweite hingegen die Struktur der Funktion bzw.
die Deklaration des Problems:
aufsummieren
aufsummieren
|n<0 =
|n==0 =
|n>0 =
:: Int -> Int
n
n + aufsummieren (n+1)
0
n + aufsummieren (n-1)
Wird die Funktion später aufgerufen, beispielsweise der Form „aufsummieren 10“, so
werden alle Zahlen von 0 bis 10 aufsummiert und als Ergebnis „55“ zurückgeliefert.
Haskell selbst übernimmt dabei die Evaluierung bzgl. der Funktion.
Die Deklaration von Funktionen kann in Modulen erfolgen und fördert die
Übersichtlichkeit. Haskell unterstützt neben den üblichen einfachen Typen, wie z. B.
Boolean, Integer oder Float die Spezifikation von Datenstrukturen, wie beispielsweise
von Listen und Bäumen. [Bi98, S. 29, S. 75]
Eine Liste ist beispielsweise spezifiziert als:
data List α = Nil | Cons α (List α )
[Bi98, S. 92],
2
Kapitel 2: Grundlagenkapitel
wobei α für den Typen der in der Liste enthaltenen Elemente und „Nil“ für eine
nullelementige Liste steht. Im eigentlichen Programm ist α beispielsweise durch „a“
zu ersetzen. „Cons α “ bezeichnet das Voranstellen eines Elements des Typen α vor
eine andere Liste vom Typ α , die Datenstruktur ist somit rekursiv.
Eine einfache binäre Baumstruktur beschreibt sich wie folgt:
data Btree α = Leaf α | Fork (Btree α ) (Btree α ) [Bi98, S. 179].
Das Prinzip des Aufbaus einer solchen Baumstruktur ähnelt dem der vorgestellten Liste
in sofern, als dass der Baum entweder aus einem in der Rekursionskette endenden
Element „Leaf“ oder aus „Fork“, einer Verzweigung über zwei folgende Bäume besteht.
Das Element „Leaf“ hält dabei ein Element des Typs α . [Bi98, S. 179] Für solch einen
Datentypen können Funktionen spezifiziert werden, um beispielsweise das Erstellen
von einer Baumstruktur aus einer Liste zu gewährleisten, mit der Spezifikation:
erstelleBaum :: [ α ] -> Btree α
.
2.2 Monaden
Das Hinzuziehen der Monaden spielt für Haskell in sofern eine Rolle, als dass durch sie
unter anderem Interaktionen, wie beispielsweise die zwischenzeitliche Ein-/Ausgabe,
und zustandsbehaftetes Programmieren ermöglicht wird. Ursprünglich besteht ein
Haskell-Programm aus einer Struktur von Funktionen, die bei jedem Aufruf
unveränderlich bleibt, d. h. es können keine Zustände zwischengespeichert werden.
Monaden ermöglichen die Umsetzung von Prinzipien, wie sie in imperativen
Programmiersprachen, wie z. B. Java, vorherrschen. [Bi98, S. 325] Somit wird
Funktionalität hinzugefügt, die weniger deterministisch ist, da beispielsweise
Zwischeneingaben Einfluss auf die Weiterverarbeitungen haben können.
Die Bezeichnung Monaden entspringt nicht direkt aus der Informatik, sondern wurde
erstmals in der Mathematik im Bereich der Kategorientheorie entwickelt. Im Rahmen
von Untersuchungen zur Semantik von Programmiersprachen stellte sich später heraus,
dass in funktionalen Programmiersprachen etablierte Mechanismen zur Eingabe und
Ausgabe den Axiomen der Monadentheorie der Mathematik entsprechen. Danach
wurde der Begriff Monaden für die Techniken im Bereich der Informatik eingesetzt, die
eben diesen Axiomen entsprechen. [PH06, S. 217]
3
Kapitel 2: Grundlagenkapitel
Die Kategorientheorie der Mathematik, der der Begriff der Monaden zugehörig ist,
beschreibt Kategorien als Objekte mit Verbindungen innerhalb einer Kategorie, in Form
von Pfeilen, zueinander, wobei eine Komposition von Objekten ermöglicht sein muss.
Diese Komposition muss assoziativ sein, so dass beispielsweise gilt: (a b) c == a (b c).
Ferner muss eine Identitätsfunktion, also ein Identitätspfeil auf ein Objekt, gegeben
sein, so dass der Wertebereich der Objekte dem der Ergebnisse der eingesetzten
Identitätsfunktion gleicht: id(o) == o. In funktionalen Programmiersprachen werden
Objekte durch Typen und Pfeile durch Funktionen repräsentiert, erfüllen die
Voraussetzungen der Theorie und können ihr damit zugeordnet werden. [PH06, S. 218
f] Denn die Komposition von Funktionen, also die Folgeeinsetzung, ist assoziativ, da sie
zustandlos sind. Kategorien sind in diesem Rahmen Typklassen. [PH06, S. 218]
Monaden sind eine spezielle Ausprägung von Funktoren, die in der Kategorientheorie
Platz finden. Funktoren sind als Abbildungen von Kategorien auf Kategorien zu
verstehen und im Rahmen von funktionalen Programmiersprachen als eine von Typen
mit den jeweiligen Funktionen auf entsprechende Typen und Funktionen. Eine
Abbildung von Typen auf Typen beschreibt das Konzept des generischen bzw.
polymorphen Typen. Zur Überführung von Funktionen der einen auf die andere
Kategorie wird ein bestimmter Map-Operator „*“ verwendet. [PH06, S. 219 f]
Prinzipiell liegt die Idee zu Grunde, Funktionalität typunabhängig zu machen.
Die allgemeine Spezifikation für einen Funktor sieht wie folgt aus:
SPECIFICATION Functor = {
TYPE F: Type -> Type
FUN _*: [ α :Type, β :Type]|->( α -> β )->(F α ->F β )
PROP id* = id
PROP (g.f)* = (g*).(f*)
}. [PH06, S. 219]
Dabei dient die erste innere Zeile als Typabbildung und die zweite als Überführung der
Funktion α -> β von ihrer ursprünglichen Kategorie in eine andere, entsprechend dem
angegeben Typ. Die dritte und vierte Zeilen stellen Eigenschaften des Funktors dar und
legen die Identitätsfunktion als solche in der neuen Kategorie fest und definieren die
Überführung einer Komposition in eine neue Kategorie als Komposition der
Überführung der einzelnen Funktionen („.“ ist hier der Kompositionsoperator).
„Generell lassen sich Monaden einfach charakterisieren: Eine Monade ist ein
polymorpher Typ M α zusammen mit vier speziellen Funktionen *, lift, flatten und &.“
4
Kapitel 2: Grundlagenkapitel
[PH06, S. 220] An diesem Zitat wird deutlich, dass Funktoren, definiert als polymorphe
Typen mit einer Überführungs- bzw. Map-Funktion, eine Verallgemeinerung von
Monaden sind bzw. diese umgekehrt spezielle Funktoren darstellen, die über zusätzliche
Funktionen „lift“, „flatten“ und „&“ verfügen. Mit diesen drei neu hinzugekommenen
Funktionen soll auf typunabhängiger Basis (den Funktoren) eine Sequenzialisierung und
zustandsbehaftete Programmierung ermöglicht werden:
SPECIFICATION Monad = {
EXTEND Functor RENAMING F As M
FUN lift: [ α :Type]|-> α -> M α
FUN flatten: [ α :Type]|-> M (M α )-> M α
FUN _&: [ α :Type, β :Type]|-> M α ->( α -> M β )-> M β
}. [PH06, S. 221]
Die Funktion „lift“ dient zur Überführung des Basistyps α in den spezifizierten bzw.
instanziierten Monadentypen [PH06, S. 220], was beispielsweise die Definition von α
als Parameter zur Ein-/ bzw. Ausgabe für den instanziierten Monadentypen IO, der von
Haskell zu solchen Interaktionszwecken bereitgestellt wird, sein kann. [Bi98, S. 329]
Denkbar ist auch die Überführung eines α -Typs, z. B. Int, in eine Sequenz,
beispielsweise eine einelementige Liste. Die Funktion „flatten“ hingegen dient der
Zusammenführung von verschachtelten Monaden und damit der „Beebnung“ von
diesen. [PH06, S. 223] Bezogen auf eine Liste bedeutet dies, dass diese in einer Liste
verkettet werden. Mit der Funktion „&“ wird der Typ, der innerhalb einer Monade M
verwendet wird (-> α ), an eine gleiche Monade mit dem Typ β übergeben und
schließlich eine Monade von eben diesem Typ β zurückgegeben. Bezogen auf das
Beispiel M=IO und in einem Kontext, wo eine Eingabe von Objekten vom Typ Char
[Bi98, S. 35] bezweckt werden soll, dient die Funktion „&“ der Weiterreichung der
jeweils momentan eingegebenen Characters, um sie geschlossen als Monade von einer
Characterliste (String) vorzufinden, wobei diese in der Definition durch β repräsentiert
wird.
Ein in Haskell implementiertes Beispiel zur Eingabe eines Strings der Länge n sei im
Kontext eines Beispiels für „IO“ als Monade M gegeben durch:
return :: α -> IO α
getChar:: IO Char
readn :: Int -> IO String
readn 0 = return []
readn (n+1) = getChar >>= q
where q c = readn n >>= r
5
Kapitel 2: Grundlagenkapitel
where r cs = return (c:cs)
. [Bi98, S. 327f] Die Funktion “return” spiegelt die vorgestellte Funktion “lift” für einen
Monadentypen M, also auch “IO”, wieder. „getChar“ ist eine von Haskell bereitgestellte
Funktion zum lesen eines Characters. Der Operator „>>=“ setzt für zwei Monaden des
Typs α und β die Funktion „&“ (M α ->( α -> M β )-> M β ) um, reicht also für
M=IO, β =String und α =Char über getChar eben diesen Character an die Monade q
weiter und führt zur Ausgabe eines Strings. Das weitergereichte α wird mit „c“ und bei
dem zweiten Gebrauch der Funktion „&“ wird die weitergereichte Liste von Characters
mit „cs“ bezeichnet, die über return gemeinsam als Liste von Characters zurückgegeben
werden. Die vereinfachte Schreibweise für die Funktion „readn“ sieht wie folgt aus:
readn 0 = return []
readn (n+1) = do c <- getChar
cs <-readn n
return (c:cs)
Das Konzept der Monaden ist in Haskell integriert und kann als Modul für bestimmte
Programme importiert und verwendet werden.
2.3 Testen
Grundlegend werden beim Testen von Software zwei Arten relevant, auf der einen Seite
das Black-Box Testing, auf der anderen das Glass-Box Testing.
Beim Black-Box Testing bleibt dem Tester die Implementierung verborgen, bzw. sie ist
für die Umsetzung nicht relevant. Lediglich eine Spezifikation der zu Grunde liegenden
Funktionalität ist notwendig, um den zu erwartenden Output eines Programms oder
Moduls abschätzen zu können. Bei dem Einsatz eines Black-Box Tests wird so
verfahren, dass stets Paarweise Testdaten erzeugt werden – für den In-/ und Output
einer zu überprüfenden Komponente. [FK08, S. 1] Die Generierung der Parameter für
den Input kann willkürlich oder nach einem Kalkül erfolgen, um beispielsweise
bestimmte Extremfälle, wie z. B. im Falle eines Integerparameters die „0“, abzudecken.
Die zugehörigen Outputparameter werden auf Grundlage der generierten
Inputparameter und der Spezifikation der Komponente berechnet. Die Komponente
bzw. das Programm wird schließlich mit den Inputparametern aufgerufen und das
Ergebnis mit den berechneten Soll-Outputparametern verglichen. Damit ist klar, dass
der Garant für ein Resultat eines Black-Box-Tests, ob ein Programm fehlerfrei
6
Kapitel 2: Grundlagenkapitel
funktioniert oder nicht, die Qualität des Kalküls ist, mit der die Inputparameter
berechnet werden. Ein solches Kalkül muss nicht zwingend starr definiert sein, so dass
eine zufallsbasierte Generierung, das sog. „Random Testing“ [BS01, S. 62], von
Inputparametern stattfinden kann. Im Rahmen von deklarativen Programmiersprachen
bietet sich eine zufallsbasierte Methodik an [CH00, S. 1], weil die Spezifikation der zu
Grunde liegenden Funktionalität oftmals nahe liegt, Black-Box Testing also generell
geeignet ist, und durch Schätzung der Verteilungen der Inputparameter eine
repräsentative Datenmenge zum Testen geschaffen werden kann. Sinnvollerweise
sollten bei der Verteilungsschätzung dennoch Extremfälle berücksichtigt werden.
Zufallsbasierte Testmethoden sind schnell und müssen im Rahmen deklarativer
Programmiersprachen prinzipiell keine möglichen Seiteneffekte durch Zustände
berücksichtigen und haben den Vorteil, dass neue Durchläufe nach Modifikationen bzw.
Verbesserungen an der betrachteten Komponente meist einfach möglich sind. [CH00, S.
1]
Gestaltet sich die Spezifikation von Programmen bzw. Komponenten schwierig, basiert
es beispielsweise auf einem komplexen Algorithmus, der nicht leicht oder gar nicht auf
bestimmte Verhaltensweisen reduziert werden kann, so bietet sich Glass-Box Testing
an. [FK08, S. 1] Dabei wird der zu Grund liegende Code analysiert und systematisch ein
Konzept für Testfälle entworfen. Solch eine Vorgehensweise kostet Zeit, insbesondere
wenn es sich um größere Softwaremodule handelt. Daher besteht die Möglichkeit, beide
Testverfahren miteinander zu kombinieren. [FK08, S. 1] Realisierungsformen des
White-Box Testing sind das sogenannte Control-Flow Testing und Data-Flow Testing.
Beim Control-Flow Testing werden alle Verzweigungsmöglichkeiten der zu testenden
Komponente durch mindestens einen Testfall abgedeckt, wohingegen beim Data-Flow
Testing sog. Chains für alle möglichen, jedoch vorfindbaren Permutationen gebildet
werden, die drei Informationen halten: eine Variable, eine Wertzuweisung und
Benutzung, wobei sie bei der Benutzung zwischenzeitlich nicht überschrieben worden
sein darf. [FK08, S. 1] Wird eine Variable überschrieben, gibt es mit derselben Variable
eine neue Chain, mit eben dieser Wertzuweisung.
7
Kapitel 3: QuickCheck
3 QuickCheck
QuickCheck ist ein Modul, dass in der Programmiersprache Haskell entwickelt und in
ihr als solches einsetzbar ist. Es gilt mit rund 300 Codezeilen als „lightweight“Programm und ist für Haskellprogramme universal einsetzbar. Dafür orientieren sich
ihre Grenzen im Rahmen der Anwendbarkeit an denen der Mächtigkeit der Sprache
Haskell. [CH00, S. 1] Auf Grund ihrer Anwendungssprache Haskell zählt QuickCheck
zu einer Methodik zum Testen deklarativer Programmiersprachen. Die Methodik stellt
sich in sofern dar, als dass sie zufallsbasiert und im Bereich Black-Box Testing
anzusiedeln ist. Die Generierung von Inputparametern erfolgt demnach zufallsbasiert
auf Grundlage von Verteilungen, die durch ausgewählte bzw. selbst definierte
Generatoren repräsentiert werden. Diese Festlegung nimmt der Benutzer in der Sprache
Haskell vor. Ferner liegt es an ihm, eine sog. Property zu definieren, wobei die zu
prüfende Funktion validiert und ein Wert vom Typ Boolean zurückgegeben wird. Die
Syntax zur Beschreibung von Properties wird von QuickCheck bereitgestellt, beruht
allerdings auf dem Klassensystem von Haskell. [CH00, S. 1]
3.1 Konzept
Abb. 1: Ablauf einer Prüfung mit QuickCheck
Soll eine Property durch QuickCheck geprüft werden, so stößt der Benutzer den
Prozess, wie in Abb. 1 dargestellt, durch einen Aufruf von QuickCheck mit der Property
als Parameter aus dem Haskell-Interpreter an. QuickCheck sucht auf Grundlage der
angegebenen Inputparameter der spezifizierten Property die zugehörigen, festgelegten
8
Kapitel 3: QuickCheck
Generatoren und führt diese aus. Die Property nimmt nun Bezug auf die interessierende
Funktion und validiert sie, indem beispielsweise ein alternativer Algorithmus dagegen
getestet oder das Ergebnis der Funktion auf Konsistenz geprüft wird. Liefert die
Property „false“ zurück, so ist unter Annahme der korrekt spezifizierten Property die
interessierende Funktion fehlerhaft, wohingegen der Wert „true“ bestätigt, dass die
Funktion für den speziellen Fall der Inputparameterkonstellation zum korrekten
Ergebnis führt. Ist der Wert „false“, so bricht die Überprüfung der Property ab und gibt
das fehlerhafte Beispiel aus. Bei dem Wert „true“ wird Bezug zu den maximal
durchzuführenden Durchläufen genommen. Der Wert für dieses Maximum ist
einstellbar, standardmäßig jedoch bei „100“. Ist der Wert erreicht, so meldet
QuickCheck, dass kein Fehler gefunden wurde – andernfalls wird eine weitere Iteration
angestoßen. [CH00, S. 1 f] Um eine Funktion als fehlerfrei zu postulieren, müssen
mehrere Durchläufe gemacht werden, damit verschiedene Inputparameter zur Geltung
kommen. Absolute Sicherheit kann nicht gegeben werden, da evtl.
Inputparameterkombinationen nicht ausgewählt wurden, die jedoch beim Einsetzen in
die Funktion zu einem falschen Ergebnis führen.
3.2 Grundlegende Implementierung
Folgendes Beispiel soll das Zusammenspiel von QuickCheck und Haskell und die
Syntax der Spezifizierung einer Property illustrieren:
In einem Programm „multiIterativ“ soll eine Multiplikation von zwei Werten „a“ und
„b“ in Haskell umgesetzt werden, wobei auf den Multiplikationsoperator („*“), mit
Ausnahme für den Fall von einem Wert gleich „-1“, verzichtet werden soll, so dass das
Ergebnis iterativ berechnet wird (z. B. statt „2*3“ ist die Vorgehensweise „3+3“ oder
„2+2+2“). Eine Implementierungsform kann die folgende sein:
import QuickCheck
multiIterativ :: Int -> Int -> Int
multiIterativ a b
| a<0 = (-1)*b + multiIterativ (a+1) b
| a==0 = 0
| a>0 = b + multiIterativ (a-1) b
prop_MultiIterativ :: Int -> Int -> Bool
prop_MultiIterativ a b =
multiIterativ a b == a*b
9
Kapitel 3: QuickCheck
Der Code ist in einem Modul gespeichert und in einem Haskell-Interpreter
hineingeladen, so dass die Funktion „multiIterativ“ mit den beiden Parametern aufrufbar
ist. Durch die Zeile „import QuickCheck“ wird das zugehörige Modul mit ins
momentan geöffnete geladen, so dass die Funktionalität zum Testen bereitsteht.
„prop_MultiIterativ“ stellt eine Property von QuickCheck dar, die Bezug auf das
Programm „multiIterativ“ nimmt, dieses also validiert bzw. testet. An der Spezifikation
wird deutlich, dass der Ausgabewert vom Typ Boolean ist. Die Validation erfolgt hier
einfach durch die Gegenüberstellung des Ergebnisses der Funktion zur Berechnung mit
dem (*)-Operator. Nun kann im Interpreter über das Kommando „quickCheck
prop_MultiIterativ“ die Überprüfung durch QuickCheck angestoßen werden. Dabei
benutzt QuickCheck für den zweifach in der Spezifikation der Property angegebenen
Typen „Int“ einen Standard-Generator. Im Hintergrund werden die standardmäßig mit
100 eingestellten Durchläufe vorgenommen und als Ergebnis folgendes ausgegeben:
Main> quickCheck prop_MultiIterativ
OK, passed 100 tests.
Die einzelnen Testfälle werden bei positivem Ausgang der Durchläufe nicht angezeigt.
Sollen diese dennoch ausgegeben werden, so kann das Kommando „verboseCheck“
[CH00, S. 6] anstelle von „quickCheck“ verwendet werden und es erfolgt eine
Darstellung aller generierter Inputparameter.
Angenommen, die definierte Funktion „multiIterativ“ sei falsch, indem in der ersten
Zeile ihrer Implementierung für den Fall a<0 die Multiplikation von b mit (-1) nicht
erfolgt. Eine Ausführung von QuickCheck mit Test der Property kommt zu folgendem
Ergebnis, wobei die Ausführung gestoppt wird, sobald ein Boolean „false“ als Ergebnis
zurückgegeben wird:
Main> quickCheck prop_MultiIterativ
Falsifiable, after 4 tests:
-2
2
Zusätzlich wird die Anzahl der Durchläufe angegeben, bis der Abbruch erfolgt und die
Inputparametern werden ausgegeben, die zu der Falsifizierung der Property geführt
haben.
Im dargestellten Beispiel gestaltet sich die Validation einfach. Bei der Überprüfung
eines Algorithmus, beispielsweise zur Sortierung einer Liste, wird die Spezifizierung
etwas komplexer. Eine Vorgehensweise ist z. B. das gegenüberstellen des zu testenden
10
Kapitel 3: QuickCheck
Algorithmus zu einem erprobten, der fehlerfrei ist, eine andere kann sich durch die
Überprüfung der Bedingung, das jedes Folgeelement in der Liste größer als sein
Vorgänger ist, darstellen. Gegeben sei folgendes Beispiel zum aufwärts sortieren einer
beliebigen Liste von Int-Werten:
minL :: [Int] -> Int -> Int
minL [] min = min
minL (x:r) min = if (x<min) then
minL r x
else
minL r min
sortUp :: [Int] -> [Int]
sortUp [] = []
sortUp (x:r) = (minL r x) : sortUp r
sortedUp :: [Int] -> Bool
sortedUp [] = True
sortedUp [x] = True
sortedUp (x1:x2:r) = x1<=x2 && sortedUp (x2:r)
listValid :: ([Int], [Int]) -> Bool
listValid (l1,l2) = sortedUp l1 && (length l1 == length l2)
prop_SortUp :: [Int] -> Bool
prop_SortUp l =
listValid(sortUp l, l)
Die Funktion „sortUp“ sortiert eine ihr übergebene Liste, wobei auf die Hilfsfunktion
„minL“, die hier als fehlerfrei unterstellt wird, zurückgegriffen wird. Die Spezifikation
der Property bezieht sich indirekt auf die Funktion „sortUp“, indem die Funktion
„listValid“ zur Überprüfung einer sortierten Liste aufgerufen wird. Dabei wird
analysiert, ob die liste monoton steigend ist und ob die Anzahl der Elemente der
unsortierten der der sortierten Liste entspricht.
3.2.1 Bedingte Properties
Bei der Festlegung einer Property kann nicht immer gewährleistet werden, dass eine
Funktion einer bestimmten Validation für alle Eingabewerte entspricht. Soll
beispielsweise eine Funktion „sumToN“ zur Berechnung der Summe von [0..n] für
n ∈ Z getestet werden und wird bei der Validation des Ergebnisses die Gauß’sche
Summenformel [Ne69, S. 172 f] gegenübergestellt, so muss für negative bzw. positive
Werte von n differenziert werden. Im Folgenden wird die Funktion lediglich für positive
Werte getestet:
sumN :: Int -> Int
sumN n
| n<0 = n + sumN (n+1)
| n==0 = 0
11
Kapitel 3: QuickCheck
| n>0 = n + sumN (n-1)
prop_SumNFalse :: Int -> Bool
prop_SumNFalse n =
sumN n == div (n*(n+1)) 2
prop_SumN :: Int -> Property
prop_SumN n =
n>=0 ==> sumN n == div (n*(n+1)) 2
Die Property „prop_SumNFalse“ wird den Test abbrechen, weil bei der Generierung
vom Wert für „n“ auch negative in Frage kommen, die Formel von Gauß jedoch ein
anderes Ergebnis liefert. Abhilfe schafft hier „prop_SumN“, wobei lediglich eine
Validation stattfindet, sofern die angegebene Bedingung „n>=0“ erfüllt ist. Der Typ
„Boolean“ innerhalb der Spezifikation der Property als Rückgabewert ändert sich in
„Property“, da die Verfahrensweise eine andere ist. Es werden alle Inputwerte
verworfen, die nicht der Bedingung entsprechen und zwar bis je Durchlauf eines Zyklus
passende gefunden werden. Allerdings gibt es auch hier eine Grenze für die maximale
Anzahl totaler Durchläufe bzw. Versuche, dessen Standardwert bei „1000“ liegt.
[CH00, S. 2 f]
3.2.2 Überwachung der Inputparameter
Im Rahmen von Tests von Properties durch QuickCheck lassen sich Permutationen von
Inputparametern klassifizieren. Das lässt eine Überwachung der Testdurchführung zu,
um Einblick in die Verteilung von Testfällen für die jeweiligen Parameter zu erhalten.
Es existieren zwei Befehle classify und collect, um ein sog. „Monitoring“ von Testfällen
durchzuführen. [CH00, S. 3] Dabei wird kein Einfluss auf die eigentliche Definition der
Property genommen, sondern lediglich eine Aggregierung bzgl. festgelegter
Bedingungen oder einer Funktion vorgenommen. Schlägt der Test fehl, wird keine
Aggregierte Ausgabe der Inputparameter gemacht. Der Ausgabetyp bei Verwendung
einer Klassifizierung ist stets „Property“. [CH00, S. 3] Der Befehl classify klassifiziert
Inputparameter mit einer bestimmten, anzugebenden Bezeichnung entsprechend einer
Bedingung, also einem boolschen Wert, d. h. die Bedingung führt zur einfachen
Klassifizierung von Inputparametern. Folgendes triviales Beispiel, indem keine Prüfung
einer Funktion stattfindet, soll zeigen, wie die Verteilung der Generierung eines IntWertes „x“ durch QuickCheck aussieht:
prop_RV :: Int -> Property
prop_RV x =
classify (x>0) "positiv" $
classify (x<0) "negativ" $
12
Kapitel 3: QuickCheck
classify (x==0) "neutral" $
x == x
Die Ausgabe des Aufrufs “quickCheck prop_RV” stellt sich wie folgt dar:
Main> quickCheck prop_RV
OK, passed 100 tests.
50% positiv.
36% negativ.
14% neutral.
Für die Bezeichnung „neutral“, also für „x==0“, ist ein sehr kleiner Prozentanteil zu
erwarten, für „positiv“ und „negativ“ ein in etwa gleich hoher, nahe bei „50%“, sofern
man annimmt, dass die Verteilung von Int-Werten gleichverteilt ist. Die beiden Klassen
„positiv“ und „negativ“ gleichen sich an, führt man den Testlauf mehrfach durch, doch
der relative Anteil von „neutral“ bleibt stets bei etwa „15%“. Das lässt vermuten, dass
QuickCheck mit vielen trivialen Inputwerten, hier für „x==0“, startet und dann
komplexer wird.
Der zweite Befehl collect nimmt keine einfache Klassifizierung bzgl. einer definierten
Bedingung vor, sondern erstellt ein Histogramm auf Basis einer Funktion, dessen
Eingabeparameter ein Inputparameter darstellt. [CH00, S. 3] Ein triviales Beispiel zur
Überprüfung der Länge generierter Listen sei das folgende, indem die Property wieder
trivial („True“) ist und keinen Test einer Funktion beinhaltet:
prop_LL :: [Int] -> Property
prop_LL l =
length l <= 5 ==>
collect (length l) $
True
Bei der Ausführung werden lediglich Listen mit einer Länge bis zu “5” klassifiziert. Die
Ausgabe stellt sich wie folgt dar:
Main> quickCheck prop_LL
OK, passed 100 tests.
26% 0.
24% 1.
19% 2.
13% 4.
10% 5.
8% 3.
Auch hier wird deutlich, dass der Anteil trivialer Werte für eine Liste hoch ist, was zum
einen die leere, zum anderen die einelementige Liste ist.
13
Kapitel 3: QuickCheck
Das „Monitoring“ ist ein hilfreiches Verfahren, um beim Testen eines Programms einen
Überblick darüber zu erhalten, ob solche Testfälle berücksichtigt wurden, die sich der
Programmierer selbst vorstellt.
3.3 Selbstdefinierte Generatoren
Bei der Benutzung von QuickCheck kann es trotz standardmäßig vorhandener
Generatoren nützlich bzw. notwendig sein, eigene zu definieren, um ein repräsentatives
Ergebnis der Testläufe gewährleisten zu können. Es obliegt dem Benutzer, die
passenden Generatoren für seine in den Properties verwendeten Typen heranzuziehen,
damit ein möglichst breites Spektrum an Testfällen abgedeckt wird und keine für den
Einsatz relevanten Konstellationen wegfallen.
Die passende Verteilung ist der Schlüssel für ein repräsentatives Ergebnis. Im besten
Fall entspricht sie der der tatsächlich in der Praxis vorzufindenden Verteilung, wobei
diese meist schlecht abzuschätzen ist. Das führt zu einem häufigen Gebrauch einer
Gleichverteilung. [CH00, S. 1] Dennoch kann beispielsweise eine Eingrenzung des
relevanten Wertebereichs oder eine Änderung der Verteilung zu Gunsten bestimmter
Werte notwendig sein.
Im Falle der Verwendung von bedingten Properties besteht ein Risiko der Verzerrung
der Verteilung, das sich automatisch ergibt, wenn bestimmte Testfälle verworfen
werden, weil sie der Bedingung nicht entsprechen. [CH00, S. 3] Das Risiko ergibt sich
vor allem dann, wenn die Bedingung so restriktiv ist, dass fast nur solche Testfälle bzw.
Inputparameter zugelassen werden, die für das Testen der eigentlichen Funktion trivial
sind. Angenommen, Funktionen zur konsistenten Änderung einer sortierten List, wie
beispielsweise das Löschen eines Elements, sollen getestet werden. Voraussetzung für
die konsistente Änderung ist das zu Grunde liegen einer bereits sortierten Liste. Die
Property berücksichtigt demnach nur eine sortierte Liste als Inputerparameter. Bei
willkürlich generierten Listen sind sie bei zunehmender Länge mit wachsender
Wahrscheinlichkeit nicht sortiert und werden wegen der Bedingung verworfen, d. h.
kürzere Listen haben höhere Wahrscheinlichkeit, sortiert zu sein und damit beim Testen
berücksichtigt zu werden. Per Definition ist eine leere und einelementige Liste sortiert.
Im schlimmsten Fall kann es nun passieren, dass nur triviale Listen getestet werden, die
konsistente Operation als „fehlerfrei“ postuliert wird, obwohl sie bei mehrelementigen
Listen versagt. Ein Instrument zur Identifikation solcher Missstände ist das vorgestellte
14
Kapitel 3: QuickCheck
Monitoring, um das Ausmaß trivialer Testfälle überschauen zu können. Abhilfe zur
Gewährleistung eines repräsentativ durchgeführten Tests schafft nun die Definition
eigener Generatoren in QuickCheck, die beispielsweise lediglich sortierte Listen
generieren und mehr Potenzial haben, mehrere nicht-triviale Testfälle zu
berücksichtigen.
3.3.1 Eigene Generatoren
QuickCheck verfügt über eine Klasse „Arbitrary“:
class Arbitrary a where
arbitrary :: Gen a ,
wobei „a“ für einen zu definierenden Typ steht. Soll für einen bestimmten Typen ein
Generator entwickelt werden, der Werte dieses Typs generiert, so ist dieser Bestandteil
einer Instanz der Klasse. Das bedeutet, dass für jeden Typ eine eigene Instanz dieser
Klasse besteht, sofern für diesen ein Generator definiert werden soll. „Gen a“ ist dabei
ein in QuickCheck wie folgt definierter, abstrakter Typ und repräsentiert einen
Generator für Werte vom Typ „a“:
newtype Gen a = Gen (Rand -> a) . [CH00, S. 3 f]
Der abstrakte Generator muss bei Instanziierung der Klasse „Arbitrary“ für einen neuen
Typen festgelegt werden. Es wird deutlich, dass ein Generator als Funktion aufzufassen
ist, die auf Basis eines willkürlichen Anfangszufallswertes einen Wert vom Typ „a“
generiert. [CH00, S. 4] Für alle Standardtypen in Haskell bestehen bereits Instanzen der
Klasse „Arbitrary“. Darüber hinaus verfügt QuickCheck über einige Generatoren, wie
beispielsweise choose mit der Spezifikation
choose :: Random a => (a,a) -> Gen a .
Wird choose beispielsweise bei der Implementierung anderer Generatoren
herangezogen, um z. B. Werte vom Typ „Int“ in einem bestimmten Intervall [x,y] zu
generieren, so ist ein Einbau „choose (x,y)“ denkbar. Die Kombination solcher
Generatoren ist möglich, weitere sind beispielsweise oneof, der auf Grund einer Liste
angegebener Generatoren einen auf Basis einer Gleichverteilung auswählt, frequency,
der eine gewichtete Auswahl trifft oder size, der bei der Generierung von rekursiven
Datenstrukturen, beispielsweise Bäumen, zufallsbasiert ein Limit für die Tiefe setzt.
Werden Kombinationen bewirkt, so kann ein Benutzer einen eigenen Generator für
15
Kapitel 3: QuickCheck
seinen Typen festlegen, so beispielsweise zur Generierung eines Baumes oder einer
(sortierten) Liste.
Der abstrakte Typ „Gen“ ist in QuickCheck als eine Instanz der Haskell-Klasse
„Monad“ deklariert, so dass die Methoden „return“ und „>>=“ in QuickCheck
implementiert sind. Diese entsprechen denen im Grundlagenkapitel beschriebenen
Funktionen „lift“ und „&“. [CH00, S. 4]
return :: a -> Gen a
(>>=) :: Gen a -> (a->Gen b) -> Gen b
Die Nutzung von sog. “liftM”-Funktionen ist möglich, die der Überführung einer
Funktion bzw. Wertesequenz in eine des Typs der Monade dient und repräsentiert die
Monadenfunktion „*“. Somit ist bei der Definition eigener Generatoren die Nutzung der
Funktionalität der im Grundlagenkapitel beschriebenen Sachverhalte möglich. Dabei
bezeichnet „a“ einen generischen Typen. Die Nutzung wird anhand des Beispiels
„Binärer Suchbaum“ dargestellt.
Neben der Generierung von Typwerten kann ebenfalls die Generierung von zufälligen
Funktionen von Relevanz sein, sofern diese beispielsweise bei der Konzeption einer
Property benutzt werden. Die Konzeption kann beispielsweise so aussehen, dass geprüft
wird, ob Funktionen assoziativ sind. [CH00, S. 2] Bei der Definition wird die Klasse
„Coarbitrary“ herangezogen. Für nähere Informationen zur Definition von
Funktionsgeneratoren, siehe [CH00, S. 5 f].
3.3.2 Binärer Suchbaum
Ein „binärer Suchbaum“ [Bi98, S. 187-191] ist eine komplexe Datenstruktur, dessen
Aufbau sich hier wie folgt darstellt:
data BinBaum a = Leer | Knoten Int a (BinBaum a) (BinBaum a)
Dabei bezeichnet „Knoten“ einen in der Baumstruktur befindlichen Teil, der über einen
Schlüssel (hier „Int“), nach welchem die Sortierung des Baumes erfolgt, und eine daran
gebundene Information vom Typ „a“ verfügt. Zudem verzweigt ein solcher Knoten auf
einen linken und rechten Teilbaum, so dass der Baum rekursiv aufgebaut werden kann.
Existiert für einen Baum bzw. Teilbaum kein solcher, so bezeichnet „Leer“ einen leeren
Baum bzw. Teilbaum. Der definierte Baum verfügt über drei Funktionen „einfuegen“
„suche“ und „loesche“, die wie folgt spezifiziert sind:
16
Kapitel 3: QuickCheck
einfuegen :: Int->a->BinBaum a->BinBaum a
suche :: Int->BinBaum a->Maybe a
loesche :: Int->BinBaum a->BinBaum a .
Beim Einfügen eines Elements wird die Baumstruktur rekursiv durchlaufen, indem bei
einem einzufügenden Wert „i<j“, wobei „j“ den Schlüssel des aktuell fokussierte
Knoten bezeichnet, in den linken Teilbaum, bei „i>j“ in den Rechten verzweigt wird.
Das Einfügen geschieht schließlich durch das Ersetzen eines leeren Teilbaums „Leer“.
Im Falle „i==j“ darf nicht einfügt werden, ansonsten könnten Informationen von
dublizierten Schlüsseln über die Funktion „suche“ oder „loesche“ nicht berücksichtigt
werden. Die Funktion „suche“ durchläuft einen Baum rekursiv anhand eines ihr
übergebenen Schlüssels und versucht, den passenden Knoten zu finden. Falls dieser
nicht vorhanden ist, wird ein Null-Wert für den entsprechenden Typen „a“
zurückgegeben – daher der Rückgabewert „Maybe a“. Die Funktion „loesche“ löscht
einen Knoten entsprechend dem ihr übergebenen Schlüssel. Existiert dieser nicht, hat
die Funktion keinen Einfluss auf den Baum, ansonsten wird beim Löschen eines
Knotens unterschieden, wie die Ausprägungen der Teilbäume vorliegen. Sofern einer
der Teilbäume „Leer“ ist, so wird anstelle des gelöschten Knotens der jeweils
vorhandene „Knoten“ zurückgegeben. Falls beide Teilbäume „Leer“ sind, wird einfach
ein beliebiger zurückgegeben. Im Fall, dass beide Teilbäume mit der Ausprägung
„Knoten“ vorliegen, wird im linken Teilbaum der größte Schlüssel mit seiner
Information (Daten) herausgesucht und als Parameter für einen neuen Knoten, der
anstelle des zu löschenden Knotens tritt, übergeben. Anschließend muss der „kopierte“
Knoten gelöscht werden, damit der Baum seine volle Konsistenz erhält. Die genaue
Implementierung der Funktionalität im Modul „BinaerBaum“ ist im Anhang A zu
finden.
Die Datenstruktur mit seinen Funktionen soll nun auf Konsistenz geprüft werden. Dazu
muss ein Generator für den Inputparameter „BinaerBaum“ entworfen werden, der
konsistente Bäume liefert, auf denen schließlich Operationen, wie beispielsweise
„einfuegen“, getestet werden können. Dazu wird eine eigene Instanz von der Klasse
„Arbitrary“ im Modul QuickCheck gebildet, die den Generator „binBaumGen“ enthält
und binäre Bäume zurückgibt:
instance Arbitrary a => Arbitrary (BinBaum a) where
arbitrary = sized (binBaumGen (-100) (100))
.
17
Kapitel 3: QuickCheck
An dieser Stelle wird der Kombinator „sized“ verwendet, der einen Int-Wert auf einen
Generator anwendet und diesen zurückgibt. Der Int-Wert wird dabei zufällig generiert
und dient dem Generator als Limit, was nötig ist, damit er bei rekursiven Strukturen,
wie dem binären Baum, sicher endet. [CH00, S. 4] Die beiden Werte „-100“ und „100“
sind willkürlich gewählt und dienen bei der Generierung von Knoten als
Intervallgrenzen, damit die Konsistenz eines Baumes gewährleistet wird. Der Generator
„binBaumGen“ ist wie folgt implementiert:
binBaumGen :: Arbitrary a => Int ->Int -> Int -> Gen (BinBaum a)
binBaumGen intVon intBis 0 = return Leer
binBaumGen intVon intBis 1 =
do i <- choose (intVon,intBis)
liftM4 Knoten (return i) arbitrary (return Leer) (return Leer)
binBaumGen intVon intBis s | s > 1 && (intVon>intBis)=
return Leer
binBaumGen intVon intBis s | s > 1 && (intVon<=intBis)=
do {i <- choose (intVon,intBis);
m <- choose(0,s-1);
h1 <- binBaumGen intVon (i-1) m;
h2 <- binBaumGen (i+1) intBis (s-1-m);
liftM4 Knoten (return i) arbitrary (return h1) (return h2)}
Die Spezifikation des Generators setzt voraus, dass der im Baum verwendete Typ „a“
durch „Arbitrary“ spezifiziert bzw. instanziiert ist. Einer der drei übergebenen IntParameter, bezeichnet als „s“, wird über die „sized“-Funktion benutzt. Ist dieser Wert
„0“, so entspricht der Baum der Länge „0“ und es wird über die Monadenfunktion
„return“ ein BinBaum vom Typ „Leer“ generiert. Die Funktion „return“ entspricht der
im Grundlagenkapitel vorgestellten „lift“-Funktion, also der Überführung eines Typs in
einen Monaden. Besteht ein Wert s gleich „1“, so wird ein Baum mit zwei Teilbäumen
vom Typ „Leer“ generiert, wobei zunächst ein Wert „i“ entwickelt wird, der an die
Funktion „liftM4“ übergeben wird. Die Notation entspricht der im Grundlagenkapitel
vorgestellten Schreibweise zur Weiterreichung von Typwerten. Die Funktion „liftM4“
produziert schließlich den binären Baum, wobei dieser vom Typ „Gen (BinBaum a)“ ist
und deshalb auf einzelne Monadenfunktionen „return“ zurückgreift. Zudem produziert
der Generator einen Wert vom Typ „a“, repräsentiert durch „arbitrary“. Bei der
Generierung dieser rekursiven Struktur wird deutlich, dass durch die Monadentechnik
zustandsbasierte bzw. sequenzielle Programmierung auf Grundlage der Sprache Haskell
möglich wird, da die Konzeption eines vielschichtigen Datentypens mit aufeinander
Aufbauenden Zufallswerten möglich wird.
18
Kapitel 3: QuickCheck
Bei der Generierung von binären Bäumen bzw. Teilbäumen mit einer Größe s größer
„1“ muss überprüft werden, inwiefern die Intervallgrenzen noch Gültigkeit besitzen.
Besteht diese nicht mehr, darf kein Knoten erstellt werden. Ansonsten wird ein Knoten
mit einem Schlüssel generiert, der die Intervallgrenzen der darunter liegenden
Teilbäume beeinflusst, um die Konsistenz des Baumes zu wahren. Zudem wird ein
neuer Wert „m“ generiert, der Einfluss auf die Größe der Teilbäume hat. Und zwar sinkt
die Größe von Ebene zu Ebene um den Wert „1“, wobei sich die Größe je Ebene auf die
beiden Teilbäume verteilt. Die generierten Werte werden, wie bereits beschrieben, über
die „<-“ -Notation jeweils weitergereicht und über die Funktion „liftM4“ in den Knoten
überführt.
Die Funktionen des Moduls „BinaerBaum“ müssen getestet werden. Dazu wird in
einem neuen Modul, in das die Module „BinaerBaum“ und „QuickCheck“, wobei ein
Generator für binäre Baume enthalten ist, importiert werden, die Formulierung von
Properties vorgenommen. Explizit sollen die beiden komplexeren Methoden
„einfuegen“ und „loesche“ getestet werden. Dazu erfolgt eine Definition von zwei
Properties wie folgt:
prop_einfuegen :: Int -> Int -> BinBaum Int -> Bool
prop_einfuegen i a b = validEinfuegen i b (einfuegen i a b)
prop_loesche :: Int -> BinBaum Int -> Bool
prop_loesche i b = validLoesche i b (loesche i b)
Bei „prop_einfuegen“ werden zwei Int-Werte als Schlüssel und Information generiert,
die in den generierten BinBaum eingefügt werden sollen. Die Methode
„validEinfuegen“ ist eine Hilfsfunktion, die neben dem Schlüssel den alten und neuen
Baum als Parameter übergibt:
validEinfuegen :: Int -> BinBaum a -> BinBaum a -> Bool
validEinfuegen i baumAlt baumNeu = validBinBaum(baumNeu) &&
if (containes i (binBaumToList baumAlt)) then
(binBaumSize baumAlt) == ((binBaumSize baumNeu))
else
(binBaumSize baumAlt) == ((binBaumSize baumNeu)-1)
Die Validation ergibt „True“, wenn der neue BinBaum validiert werden kann, demnach
sortiert ist, und die Länge des Baumes um „1“ größer ist als die des Ausgangsbaums,
sofern der Schlüssel noch nicht existierte. Ansonsten muss sich die Länge entsprechen.
Diese Funktion greift wiederum auf die Hilfsfunktion „binBaumSize“, die die Größe
eines übergebenen BinBaum’s berechnet, und „binBaumToList“, die aus einem
19
Kapitel 3: QuickCheck
BinBaum eine Liste erstellt, zurück. Die genauen Funktionen sind im Anhang
angegeben.
Die Validationsfunktion von „prop_loesche“ ist analog zu der von „prop_einfuegen“
aufgebaut, wobei beim Längenvergleich des alten mit dem neuen Baum die Restriktion
geändert werden muss, so dass beim Vorhandensein des zu löschenden Schlüssels die
neue Baumlänge um „1“ kürzer sein muss.
Sinnvollerweise kann als erster Schritt des Testens zunächst der Generator für binäre
Bäume getestet werden, indem eine Property formuliert wird, die die zufällig von
QuickCheck produzierten Bäume lediglich auf Validation prüft. Es werden also noch
keine Funktionen, wie beispielsweise „einfuegen“ getestet. Zudem kann durch
„verboseCheck“ die Generierung der Bäume verfolgt werden. Sofern eine Anzeige der
Baumlänge wünschenswert ist, kann über das Kommando „collect(binBaumSize b) $“
die Verteilung der Länge der generierten Bäume eingesehen werden. Der Anteil der
Größe der generierten Bäume liegt im vorliegenden Beispiel für die Längen [0,1,2] bei
etwa 20-30%. Die Validation der generierten Bäume kann ebenfalls über eine bedingte
Property am Anfang selbiger erfolgen.
20
Kapitel 4: Bewertung von QC
4 Bewertung von QC
Im Hauptteil konnte gezeigt werden, dass QuickCheck ein probates Tool zum Testen
von Programmen ist, wobei diese ebenfalls über komplexe Datenstrukturen, wie
beispielsweise Bäume, verfügen können. Dabei hält sich der Aufwand zur Konzeption
in Grenzen. Lediglich die Definition von Generatoren und aussagekräftiger Properties
kann sich schwierig gestalten. Mit Hilfe der von QuickCheck bereitgestellten
Funktionalität ist jedoch ein breites Spektrum an Möglichkeiten zur Definition von
Generatoren gegeben. Zudem ist es von Vorteil, dass QuickCheck selbst ein HaskellModul ist und durch Kenntnis dieser Sprache eine einfache syntaktische Generierung
von Properties erfolgen kann. QuickCheck versucht den Nachteil des Risikos der
Verzerrung der zu Grunde liegenden Verteilungen von Inputparametern über das zur
Verfügung stellen von Monitoring-Funktionen zu beschränken. Im Rahmen dieser
Arbeit war QuickCheck sehr effizient, was auf seine einfache Konzeption
zurückzuführen ist, aber auch erfolgreich beim Testen von Funktionen und somit beim
Auffinden von Fehlern. Zwar kann keine Garantie gegeben werden, dass postuliert
fehlerfreie Funktionen tatsächlich fehlerfrei sind, und eine zufallsbasierte Methode mag
zunächst naiv erscheinen, dennoch hat sich QuickCheck auch in anderen Kontexten und
Anwendungsgebieten bewährt. [CH02, S. 1], [CH00, S. 9]
Erweiterungen von QuickCheck bestehen im Bereich der Fokussierung von
zustandsbehafteten Code als Grundlage des Testens, also der Annäherung der Sprache
Haskell an Funktionsweisen von imperativen Programmiersprachen. [CH02, S. 1]
Außerdem gibt es im Bereich QuickCheck als Umsetzung der Black-Box – Methodik
einige Limitationen, die durch die Berücksichtigung eines „Data-Flow“-orientierten
Konzepts, also im Bereich White-Box Testing, aufgehoben werden sollen. [FK08, S. 2]
Eine Limitation wird angesprochen, wobei z. T. schwierig zu formalisierende
Sachverhalte über boolsche Properties dargestellt werden müssen, eine andere, die das
Speichern generierter Testfälle nicht zulässt, was jedoch Performancesteigerung nach
sich ziehen würde. [FK08, S. 2]
In einem anderen Kontext wird eine Möglichkeit zur Generierung von Testfällen auf
Grundlage einer White-Box – Methodik in der imperativen Programmiersprache Java
vorgestellt. [ML04, S. 365-371]
21
Kapitel 5: Anhang
5 Anhang
5.1 BinaerBaum
-- Herangezogene Quellen:
-- - Herbert Kuchen (2007)
-http://www-wi.uni-muenster.de/pi/personal/kuchen.php
-classical binary search tree
-- - [Bi98, S. 187-191]
module BinaerBaum where
data BinBaum a = Leer | Knoten Int a (BinBaum a) (BinBaum a)
deriving (Show)
suche :: Int -> BinBaum a -> Maybe a
suche i Leer = Nothing
suche i (Knoten j b bl br)
| i<j = suche i bl
| i==j = suche i br
| i>j = Just b
einfuegen :: Int -> a -> BinBaum a -> BinBaum a
einfuegen i a Leer = Knoten i a Leer Leer
einfuegen i a (Knoten j b bl br)
| i<j = Knoten j b (einfuegen i a bl) br
| i==j = Knoten j b bl br
| i>j = Knoten j b bl (einfuegen i a br)
loesche :: Int -> BinBaum a -> BinBaum a
loesche _ Leer = Leer
loesche i (Knoten j b bl br)
| i<j = Knoten j b (loesche i bl) br
| i>j = Knoten j b bl (loesche i br)
| i==j = case (bl,br) of
(Leer,br) -> br
(bl,Leer) -> bl
(bl,br) -> let (i,a) = maxSubbaumValue bl
in Knoten i a (loesche i bl) br
maxSubbaumValue :: BinBaum a -> (Int,a)
maxSubbaumValue (Knoten j b _ br) =
case br of
Leer -> (j,b)
br -> maxSubbaumValue br
5.2 QuickCheck
----------------- instance declarations for own types --------instance Arbitrary a => Arbitrary (BinBaum a) where
arbitrary = sized (binBaumGen (-100) (100))
22
Kapitel 5: Anhang
binBaumGen :: Arbitrary a => Int -> Int-> Int -> Gen (BinBaum a)
binBaumGen intVon intBis 0 = return Leer
binBaumGen intVon intBis 1 =
do i <- choose (intVon,intBis)
liftM4 Knoten (return i) arbitrary (return
Leer) (return Leer)
binBaumGen intVon intBis s | s > 1 && (intVon>intBis)=
return Leer
binBaumGen intVon intBis s | s > 1 && (intVon<=intBis)=
do {i <- choose (intVon,intBis);
m <- choose(0,s-1);
h1 <- binBaumGen intVon (i-1) m;
h2 <- binBaumGen (i+1) intBis (s-1-m);
liftM4 Knoten (return i) arbitrary (return h1)
(return h2)}
----------------------------------------------------------------
5.3 Testfaelle
module Testfaelle where
import BinaerBaum
import QuickCheck
---------------------------------minL :: [Int] -> Int -> Int
minL [] min = min
minL (x:r) min = if (x<min) then
minL r x
else
minL r min
sortUp :: [Int] -> [Int]
sortUp [] = []
sortUp (x:r) = (minL r x) : sortUp r
sortedUp
sortedUp
sortedUp
sortedUp
:: [Int] -> Bool
[] = True
[x] = True
(x1:x2:r) = x1<=x2 && sortedUp (x2:r)
containes :: Int -> [Int] -> Bool
containes a [] = False
containes a (x:r) = if (a==x) then True
else containes a r
------binBaumToList :: BinBaum a -> [Int]
binBaumToList Leer = []
binBaumToList (Knoten i a bl br) = (binBaumToList bl) ++ [i] ++
(binBaumToList br)
23
Kapitel 5: Anhang
binBaumSize :: BinBaum a -> Int
binBaumSize Leer = 0
binBaumSize (Knoten i a bl br) = 1+ binBaumSize(bl) +
binBaumSize(br)
validBinBaum :: BinBaum a -> Bool
validBinBaum Leer = True
validBinBaum (Knoten i a bl br) =
sortedUp(binBaumToList (Knoten i a bl br)) &&
(binBaumSize(Knoten i a bl br) ==
length(binBaumToList(Knoten i a bl br)))
validEinfuegen :: Int -> BinBaum a -> BinBaum a -> Bool
validEinfuegen i baumAlt baumNeu =
validBinBaum(baumNeu) &&
if (containes i (binBaumToList baumAlt)) then
(binBaumSize baumAlt) == ((binBaumSize baumNeu))
else
(binBaumSize baumAlt) == ((binBaumSize baumNeu)-1)
validLoesche :: Int -> BinBaum a -> BinBaum a -> Bool
validLoesche i baumAlt baumNeu =
validBinBaum(baumNeu) &&
if (containes i (binBaumToList baumAlt)) then
(binBaumSize baumAlt) == ((binBaumSize baumNeu)+1)
else
(binBaumSize baumAlt) == ((binBaumSize baumNeu))
--------------------------------------prop_qt :: BinBaum Int -> Bool
prop_qt t = validBinBaum t
prop_einfuegen :: Int -> Int -> BinBaum Int -> Bool
prop_einfuegen i a b = validEinfuegen i b (einfuegen i a b)
prop_loesche :: Int -> BinBaum Int -> Bool
prop_loesche i b = validLoesche i b (loesche i b)
24
Literaturverzeichnis
[Bi98]
Richard Bird: Introduction to Functional Programming using Haskell, 2nd.
ed., Prentice Hall, 1998.
[BS01]
British Computer Society Specialist Interest Group in Software Testing
(BCS SIGIST): Standard for Software Component Testing, Working Draft
3.4, 2001.
[CH00]
Koen Claessen, John Hughes: QuickCheck: A Lightweight Tool for Random
Testing of Haskell Programs, In: ICFP, 2000.
[CH02]
Koen Claessen, John Hughes: Testing Monadic Code with QuickCheck, On:
Haskell Workshop, 2002.
[FK08]
Sebastian Fischer, Herbert Kuchen: Data-Flow Testing of Declarative
Programs, In: The 13th ACM SIGPLAN International Conference on
Functional Programming (ICFP), 2008.
[ML04]
Roger A. Müller, Christoph Lembeck, and Herbert Kuchen. A symbolic
Java virtual machine for test-case generation, In: IASTED Conf. on
Software Engineering, 2004.
[Ne69]
Otto Neugebauer: Vorlesungen über Geschichte der antiken
mathematischen Wissenschaften, 1. Aufl., Berlin, 1969.
[PH06]
Peter Pepper, Petra Hofstedt: Funktionale Programmierung, 1. Aufl.,
Berlin, 2006.
Herunterladen