Randomisierte Tests mit QuickCheck
Die Programmiersprache Racket (früher bekannt als MzScheme) kommt mit einer QuickCheck-Bibliothek zum randomisierten Testen, mit der sich die Testabdeckung in Programmen erhöhen lässt: statt dass sich der Programmierer zahlreiche Tests überlegt und ausprogrammiert, generiert die Bibliothek anhand einer Spezifikation zufällige Testfälle, testet sie gegen definierte Eigenschaften und berichtet Gegenbeispiele, falls welche gefunden werden.
Testen über randomisierte Zahlen
Angenommen, wir wollen testen, ob für die Additionsfunktion
add(a,b) == add(b,a)
gilt (also Kommutativität). Statt von Hand einige mehr
oder weniger interessante Fälle zu testen, können wir mit QuickCheck
die Eigenschaft definieren und gegen zahlreiche (pseudo-zufällig
gewählte) Testfälle testen lassen (hier in der Lehrsprache des Buchs
„Die Macht der Abstraktion“):
property-commutative-add
beschreibt, dass für alle Zahlen a
und
b
erwartet wird, dass (= (add a b) (add b a)
sein soll (in der
Scheme-typischen Präfixnotation). number
ist dabei der sogenannte
Generator, der die Beispielzahlen zufällig erzeugt.
Wenn man nun mit
die Eigenschaft testet, generiert die Bibliothek viele zufällige
Zahlenwerte a
und b
und prüft die Eigenschaft. Damit deckt sie
mehr Fälle ab, als sich ein Programmierer in der Regel ausdenkt.
Sollte die Eingeschaft nicht erfüllt sein, gibt es eine Meldung. Zum
Beispiel könnten wir fälschlicherweise annehmen, dass auch für die
Subtraktionsfunktion sub
Kommutativität gilt:
Beim Testen liefert Racket ein Gegenbeispiel:
Eigenschaft falsifizierbar mit a = 0.0 und b = 1.5
denn 0.0 - (-1.5)
ist nicht gleich (-1.5) - 0.0
.
Möchte man statt Fließkommazahlen nur exakte Zahlen prüfen, verwendet
man den Generator rational
. Alternativ erlaubt expect-within
statt =
die Angabe einer tolerierten Abweichung. So erlaubt etwa
(expect-within (add a b) (add b a) 0.001)
, dass die Additionen um
0.001 voneinander abweichen dürfen. Generell kann im Rumpf des
for-all
jeder Ausdruck stehen, der ein Bool liefert.
Mit dem Implikationspfeil ==>
lassen sich Eigenschaften formulieren,
die von einer Bedingung abhängen. Zum Beispiel beschreibt
dass wenn a
= b
und b
= c
, auch a
= c
sein soll. In
anderen Worten: die eigentliche Eigenschaft wird nur dann geprüft,
wenn für eine zufällige Belegung die Bedingung erfüllt ist.
Testen über randomisierte Strukturen
Auch die Eigenschaften von Strukturen können beschrieben werden. So
ist (list gen)
ein Generator für Listen zufälliger Länge mit
Elementen des Generators gen
. Mit
können wir also die Assoziativitäts-Eigenschaften beim Zusammenhängen
(concatenate
) von zwei Listen testen lassen. Hierbei prüft
expect
, dass zwei Werte gleich sind.
Testen über randomisierte Funktionen
Racket erlaubt es auch, Funktionen höherer Ordnung randomisiert zu testen, also Funktionen, die Funktionen als Argument oder Ergebnis haben. Ob zwei (verschiedene) Stück Code dasselbe tun, ist ein eigener Forschungszweig der Theoretischen Informatik, darum testen wir stattdessen, ob zwei verschiedene Stück Code bei der selben Eingabe die selbe Ausgabe liefern.
Als Beispiel definieren eine die Eigenschaft von curry
, die salopp
formuliert aus einer zwei-stelligen Funktion zwei ein-stellige
macht (zum Beispiel ist ((curry add) 3)
eine Funktion, die 3
addiert):
Hier ist (string string -> string)
die Signatur der zufällig
erzeugten Funktion: proc
nimmt zwei Strings als Argumente und
liefert ein String als Ergebnis. Was proc
mit den Argumenten macht,
ist nicht näher spezifiziert.
QuickCheck
Zum ersten Mal wurde
QuickCheck von Koen
Claessen und John
Hughes 1999 für Haskell
vorgestellt. Inzwischen gibt es QuickCheck für über 20 andere
Programmiersprachen, darunter für
C,
C++ und
Perl,
aber auch für
Java,
Clojure und
Scala.
Es gibt sogar mit quviq eine kommerzielle
Firma, die auf QuickCheck spezialisiert ist.
Zwar variiert die Qualität der Implementierungen etwas (zum Beispiel
fehlt es an manchmal an Determinismus oder Parallelisierbarkeit), doch
die Testabdeckung erhöhen können die Bibliotheken allemal.