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.