André Minhorst, Duisburg und Uwe Schäfer, Essen
Die Schlagwörter Extreme Programming (XP), Unit-Testing, Test Driven Development, Refactoring oder Pair Programming geistern durch die Entwicklerwelt. Dabei ist Extreme Programming der Oberbegriff für die anderen und fasst diese und mehr zu einer neuartigen Philosophie der Softwareentwicklung zusammen. Ziel der dahinter stehenden Konzepte sind Projekte, die von kleinen Entwicklerteams durchgeführt werden. Da die meisten Leser dieses Beitrags vermutlich allein entwickeln, stellt dieser Beitrag ein elementares Konzept von XP heraus: das Test Driven Development (TDD), zu deutsch testgetriebene Entwicklung.
Extreme Programming ist ein Thema, mit dem man leicht mehrere hundert Seiten füllen könnte – wenn man nur die theoretischen Aspekte berücksichtigt.
Da dieser Platz leider nicht zur Verfügung steht, greifen wir die Teilbereiche auf, die auch Ein-Mann-Teams bei der Entwicklung von Access-Datenbanken Gewinn bringend nutzen können. Der Kern ist die “testgetriebene Entwicklung”, eng damit verbunden sind die Begriffe “Unit Test” und “Refactoring”.
Der vorliegende Beitrag soll möglichst praxisnah die Vorteile der testgetriebenen Entwicklung beschreiben. Dennoch sind einige einführende Worte erforderlich.
Hinweis
Im Internet und in der Literatur finden Sie eine Menge theoretischer Abhandlungen über diesen Themenkomplex. Wir möchten Ihnen neben den theoretischen Grundlagen ein Tool vorstellen, das Sie für die testgetriebene Entwicklung mit Access einsetzen können; außerdem lernen Sie, wie Sie dieses installieren und wie Sie Ihre ersten Schritte mit der testgetriebenen Entwicklung durchführen.
Test a little, code a little
Diese Entwicklungsmethode erfordert vom Programmierer eine Menge Disziplin, da sie voraussetzt, dass für jede Funktion einer Anwendung zunächst ein Test geschrieben wird.
Damit Sie sehen, ob der Test funktioniert – was der erste und wichtigste Schritt bei der testgetriebenen Entwicklung ist – schreiben Sie einen Test, der beim ersten Start scheitert.
Erst dann implementieren Sie die eigentliche Funktion.
Durch erneuten Start des Tests wird dann verifiziert, dass die Implementierung den Anforderungen des Tests genügt, dieser also nicht mehr fehlschlägt.
Hinweis
Schreiben Sie niemals mehr als einen neuen Test gleichzeitig! Vermutlich kennen Sie das Problem, Funktionen zu einer Anwendung hinzufügen oder ändern zu wollen, die Anpassungen an mehr als einer Stelle erfordern. Die wiederum beeinflussen andere Programmfunktionen oder machen diese gar untauglich. Wenn Sie jeweils nur einen Test gleichzeitig hinzufügen oder ändern, halten Sie auch den durch diese Anforderungen verursachten Aufwand minimal.
Kleine Schritte, einfache Wege
Jeder Test soll auf möglichst einfache Weise erfüllt werden. Wenn ein Test fordert, dass eine Funktion den Eingangswert “andré” in die Zeichenkette “André” umwandelt, dann schreiben Sie einfach eine Funktion, die den gewünschten Wert hartcodiert zurückgibt – das reicht für den ersten Ansatz, denn damit ist ja der erste Test bestanden! Wenn der zweite Test die Umwandlung eines zweiten, anderen Wertes einfordert, müssen Sie die Funktion natürlich anpassen, was Sie in dem Fall leicht mit einer bestimmten VB-Funktion tun können. Auf diese Weise stellen Sie sicher, dass die Definition der Anforderungen (durch den Test) möglichst vollständig ist und nicht von Ihrem Verständnis der Implementierung abhängt.
Einmal testen, immer testen
Natürlich bringt das ganze Testen nicht viel, wenn Sie einen Test nach erfolgreichem Bestehen aus den Augen verlieren und sich direkt dem nächsten Test zuwenden.
Deshalb fügen Sie jeden neuen Test zu den bereits erfüllten Tests hinzu und führen mit jedem neuen Test alle bestehenden Tests erneut durch. Auf diese Weise stellen Sie sicher, dass bereits erfüllte Anforderung durch neuen Code oder Codeänderungen unberührt bleiben.
Hinweis
Unit-Testing-Frameworks wie accessUnit bieten durch so genannte Testsuites die Möglichkeit, Tests nach beliebigen Gesichtspunkten zusammenzufassen. So können Sie etwa alle Tests, die nicht den gerade in Arbeit befindlichen Code betreffen, zusammenfassen und beispielsweise einmal am Tag ausführen, um unvorhergesehene Defekte der Software frühzeitig zu erkennen. Die Tests, auf deren Basis Sie gerade entwickeln, fassen Sie ebenfalls zusammen. Da Sie damit häufig testen (was dem Grundprinzip der testgetriebenen Entwicklung entspricht), sollten diese Test möglichst schnell abgearbeitet werden. Je schneller ein Test abläuft, desto geringer ist die Wahrscheinlichkeit, dass Sie ihn einmal aus “Zeitnot” auslassen.
Automatisierung ist Trumpf
Nach den ersten Abschnitten fragen Sie sich vermutlich wie jeder andere, der sich erstmalig mit dieser Thematik auseinandersetzt, wie die Tests überhaupt ablaufen. Die Antwort ist: Sie werden – genau wie normale Anwendungen auch – programmiert, und zwar als Abfolge von Prüfungen bestimmter Ausdrücke.
Wenn Sie beispielsweise eine Funktion testen möchten, die zwei Zahlen addiert, dann vergleichen Sie einfach das Ergebnis dieser Funktion mit dem zu erwartenden Ergebnis. Und damit Sie sich nur um die Festlegung dieser Tests und die Eingabe der erwarteten Ergebnisse kümmern müssen, gibt es so genannte Test-Frameworks. Mehr darüber erfahren Sie später im praktischen Teil dieses Beitrags.
Refactoring – alles bleibt besser
Der Begriff “Refactoring” ist eng mit der testgetriebenen Entwicklung verbunden. Refactoring ist eine Veränderung, Anpassung oder Verbesserung des Designs. Dabei müssen natürlich bestehende, durch Tests definierte Anforderungen auch nach dem Refactoring noch erfüllt werden.
Ein Ad-hoc-Programmierstil, der aus dem immer höheren Zeit- und Erfolgsdruck entsteht und möglicherweise auch im ersten Schritt zu einer lauffähigen Anwendung führt, garantiert großen Aufwand, wenn nachträglich zu behebende Fehler und/oder sich während der Entwicklung ändernde Anforderungen auftreten; auch die nachträgliche Optimierung einer Anwendung, die nicht die gewünschte Performance aufweist, führt sicher zu Kopfschmerzen beim Entwickler(team).
Die testgetriebene Entwicklung bietet wesentlich mehr Möglichkeiten, den bestehenden Code ohne Angst anzufassen: nämlich immer, wenn alle bis dato vorhandenen Tests zuverlässig laufen. Da Sie mit jedem Testlauf den neuen und alle bereits bestehenden Tests durchführen, erfahren Sie nicht nur, ob der neue Test erfolgreich ist oder scheitert, sondern auch, ob alles andere noch wie gewünscht funktioniert.
So können Sie den bestehenden und regelmäßig getesteten Code nach Lust und Laune refaktorisieren, solange – ja, solange die änderungen nicht bewusst ein anderes Ergebnis für einen beliebigen Test zurückliefern sollen. Das fiele dann nicht mehr unter den Begriff “Refactoring”; statt dessen heißt die Devise: Erst den Test schreiben beziehungsweise anpassen und dann die Funktionalität ändern.
Wenn Sie beispielsweise einen Vorgang, der in mehreren getesteten Routinen auftritt, in eine eigene Funktion auslagern und von den jeweiligen Routinen aus aufrufen möchten, können Sie das natürlich, ohne die Tests zu ändern, denn Sie lagern ja nur ein paar Zeilen in eine Funktion aus (Mathematiker würden hier von “Ausklammern” sprechen).
Noch besser wäre allerdings, Sie würden vorher Tests schreiben, welche die ausgelagerte Funktion auf Herz und Nieren prüfen. Damit wären Sie wieder bei der kleinsten Einheit – der “Unit”.
Unit Test – was heißt das
Der Begriff “Unit Test” ist so eng mit der testgetriebenen Entwicklung verknüpft, weil beide sich auf die kleinstmögliche Einheit beziehen. Wenn Sie kleinste Einheiten testen möchten, dann ist damit nicht eine Anwendung, auch kein Teil einer Anwendung wie ein Formular oder eine Klasse gemeint, sondern ein elementarer Bestandteil davon – eine Eigenschaft, eine Methode oder ein Ereignis, kurz: die “Unit under Test”.
Je kleiner die Einheiten sind, die Sie testen, desto schneller und leichter finden Sie fehlerhafte Stellen. Zumindest aber sollte es für jede testbare Schnittstelle Ihrer Klassen und Objekte einen oder mehrere Tests geben, die deren Funktionalität jederzeit sicherstellen können. Nur auf diese Weise können Sie sich auf das im vorherigen Abschnitt beschriebene “Refactoring” stürzen.
Alles auf einmal
Bei jeder größeren änderung oder Erweiterung sollten Sie alle vorhandenen Tests Ihrer Anwendung durchführen. Wichtig ist, dass jeder Aspekt Ihrer Anwendung für sich allein testbar ist, und zwar in beliebiger Reihenfolge, um Wechselwirkungen auszuschließen.
Dummys
Natürlich können Sie mit der testgetriebenen Entwicklung nicht nur Einheiten, sondern auch deren Interaktion testen – man spricht hier von Integrationstests. Das entspricht allerdings nicht dem Grundprinzip der testgetriebenen Entwicklung. Um dennoch die Wechselwirkung zwischen Klassen testen zu können, verwendet man verschiedene Arten von Dummies.
Das Testen ohne Wechselwirkung ist in manchen Fällen nicht so einfach, da auch die Interaktion zwischen Klassen getestet werden muss. Dabei gibt es zwei Varianten:
Hinweis
Mocks und Stubs werden im Rahmen dieses Beitrags nicht weiter erläutert.
Testdaten
Elementar wichtig für Tests sind Testdaten. Optimal wäre natürlich ein “echter” Testdatenbestand; wenn es sich um eine neue Anwendung handelt, ist dieser aber in der Regel nicht verfügbar. Um für alle Tests die gleiche Ausgangsposition zu schaffen, sollten Sie die vorhandenen Daten vorher auf einen fest definierten Stand bringen – am besten jedes Mal neu.
Dazu gibt es zwei Möglichkeiten:
Zusammenspiel und Vorzüge
Die vorhergehenden Abschnitte machen bereits deutlich, dass testgetriebene Entwicklung, Unit Tests und Refactoring ein eingespieltes “Team” sein müssen, wenn sie zum Erfolg führen sollen.
Zusammengefasst haben Sie die folgenden Vorzüge kennen gelernt:
Nachdem die theoretischen Grundlagen Ihr Interesse geweckt haben, lernen Sie nun den praktischen Ablauf kennen.