Das Clojure-Alphabet – Teil 7

G wie Generatives Testen

G wie Generatives Testen

Das Clojure-Alphabet – Teil 7

G wie Generatives Testen


In dieser Ausgabe unserer Kolumne betrachten wir Unit-Tests in Clojure im Generellen und versuchen aufzuzeigen, wie uns generatives Testen beim Schreiben robuster und aussagekräftiger Testszenarien unterstützen kann.

Unit-Tests beschreiben kein exaktes Testverfahren, sondern empfehlen einen Testgegenstand. Unit-Tests legen also nicht fest, wie etwas zu testen ist, sondern was: Einheiten. Was wiederum eine Einheit ist, hängt von der zugrunde liegenden Softwarearchitektur und auch von der genutzten Programmiersprache ab, denn die Programmiersprache bestimmt den kleinsten Baustein, aus dem wir größere Lösungen zusammensetzen – und damit die kleinste Einheit, die wir testen können: Objekte bei objektorientierten Programmiersprachen, Funktionen bei funktionalen Programmiersprachen. Als Testgegenstände kommen traditionell und in aufsteigendem Abstraktionsgrad Funktionen, Objekte (sofern vorhanden), Module, Komponenten oder Dienste in Frage. Da Clojure eine funktionale Programmiersprache ist, beginnen wir mit einem Unit-Test für eine Funktion.

Testdomäne

Ein Programm, das den Rückgabewert einer Funktion testet, ist ein Programm wie jedes andere. Trotzdem gibt es drei gute Gründe dafür, uns für Testprogramme in eine eigene Domäne zu begeben und sie eben nicht wie andere Programme zu behandeln. Der erste Grund betrifft unsere geistige Haltung: Wir sollten uns jederzeit bewusst sein, dass wir gerade einen Test schreiben, denn Tests sollen und müssen von einfacher Natur sein. Werden Tests komplex, haben wir nur ein weiteres Stück komplexe Software, die wir wiederum testen müssten. Wir sollten uns zudem immer der gegenseitigen Abhängigkeit von Test und Testgegenstand bewusst sein, um beide Enden zu pflegen, wenn wir die Software weiterentwickeln.

Der zweite Grund, uns in eine explizite Testdomäne zu begeben, ist technischer Natur: Um Unit-Tests herum hat sich eine Infrastruktur gebildet, die es uns zum einen erlaubt, Tests an verschiedensten Stellen automatisch durchführen lassen zu können: kontinuierlich während der Entwicklung, beim Einchecken von Code in ein Repository oder im Rahmen eines Continuous Integration Process, um nur einige Möglichkeiten zu nennen. Zum anderen erhalten Build-Tools so die Möglichkeit, Testcode zu erkennen und nebst seinen Abhängigkeiten automatisch aus Artefakten rauszuhalten, die für den produktiven Einsatz gedacht sind (Production oder Release Build). Diese Infrastruktur setzt Schnittstellen voraus, die wir bedienen müssen, wenn wir sie nutzen wollen. Nutzen wir Werkzeuge und Bibliotheken aus der Testdomäne, sind die Chancen sehr hoch, dass die am weitesten verbreiteten und die gängigsten Schnittstellen bereits bedient werden. Der letzte Grund ist die Ausdrucksstärke: Wenn wir Tests schreiben, möchten wir mit den zugehörigen Begriffen arbeiten: Was ist ein Test? Was ist eine Testbedingung? Was ist eine Testvoraussetzung? Je konkreter wir diese Dinge benennen – und vor allem später beim Lesen wiedererkennen können – desto einfacher und sicherer können wir sie durchdringen und pflegen.

Test DSL (Domain Specific Language)

In LISP-Dialekten, zu denen Clojure gehört, ist es ein übliches Vorgehen, eine domänenspezifische Sprache (Domain Specific Language, DSL) für wiederkehrende Problemlösungen zu entwerfen. Da in der funktionalen Programmierung oft nach möglichst ausdrucksstarkem Code gestrebt wird, entstehen diese DSLs nicht immer geplant, sondern manchmal auch als angenehmes Nebenprodukt.

Wie wir in der letzten Ausgabe dieser Kolumne gesehen haben, werden Clojure-Programme durch funktionale Komposition aufgebaut. Wir schreiben also Funktionen, die aus Verkettungen anderer Funktionen bestehen, und können somit beliebige Komplexität mit einem ausdrucksstarken Funktionsnamen versehen, der das zu lösende Problem gut beschreibt. Darüber hinaus gibt es in Clojure nicht viel Syntax, die berücksichtigt werden muss. Clojure-Code benötigt also wenig „Zeremonie“. Das macht sich vor allem beim Lesen von Code bemerkbar: Da wir uns im Clojure-Code nicht mit Kontrollstrukturen, Instanziierung von Objekten oder der Deklaration und Zuweisung von Variablen beschäftigen müssen, kann sich der absolute Großteil unseres Codes – und damit auch wir – auf das zu lösende Problem konzentrieren. Diese beiden Eigenschaften machen Clojure zu einem geeigneten Werkzeug für das Schreiben von ausdrucksstarken DSLs. Da ist es naheliegend, die Problemdomäne Testing mit einer eigenen DSL zu versehen, die im Namespace clojure.test zu finden ist. Dieser Namespace enthält Funktionen, um Unit-Tests in Clojure zu schreiben. Ein simpler Unit-Test ist in Listing 1 zu sehen.

Listing 1

(deftest test-addition  
  (testing "Addieren zweier positiver Zahlen"
    (is (= 5 (+ 2 2)))
    (is (= 10 (+ 5 5))))
 
  (testing "Addieren positiver und negativer Zahlen"  
    (is (= 2 (+ 5 -3)))
    (is (= 0 (+ 2 -2)))))

Da clojure.test uns eine DSL für das Erstellen von Testfällen bereitstellt, lässt sich dieses Codebeispiel gut nachvollziehen: Zunächst drückt deftest aus, dass es sich hierbei um die Definition eines Unit-Tests handelt, der mit dem Namen test-addition versehen ist – der Test behandelt also Addition. Dieser Ausdruck enthält zwei Testfälle, die mit einem Aufruf der Funktion testing ausgeführt werden. Der erste Parameter von testing ist ein sprechender Text, der den zu testenden Fall beschreibt...