Testgetriebene Entwicklung mit Access

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:

  • Im ersten Fall benötigt die erste Klasse eine Eigenschaft oder Funktion der zweiten Klasse, um einen bestimmten Wert zu ermitteln. Im Idealfall lässt sich die zweite Klasse dabei durch eine Dummy-Implementierung ersetzen, die den gewünschten Wert liefert – dabei handelt es sich um einen so genannten “Stub”. Im zweiten Fall löst die Interaktion der beiden Klassen die änderung einer Eigenschaft oder Verhaltensweise der zweiten Klasse aus, die für den Test der ersten Klasse wichtig ist. Will man für die zweite Klasse einen Dummy verwenden, reicht es nicht aus, wenn dieser einfach auf Anfrage einen bestimmten Wert liefert. Statt dessen muss man die Auswirkung der Interaktion zwischen den Klassen prüfen können. Ein solcher Dummy ist ein wenig komplizierter und heißt in der Fachsprache “Mock”.
  • 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:

  • Sie erstellen die Daten mit jedem Test durch geeignete SQL-Skripte neu und löschen diese anschließend wieder. Testframeworks enthalten geeignete Methoden, um die notwendigen Anweisungen unterzubringen.
  • Wenn die zu entwickelnden Klassen selbst Methoden enthalten, um die notwendigen Daten anzulegen, stellen Sie die Testdaten doch einfach im Rahmen der Tests der entsprechenden Klassen zusammen! Vermutlich finden sich auch Methoden zum Löschen von Daten in den Klassen, die Sie zum Entfernen der Testdaten verwenden können.
  • 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:

  • Vorausschauend planen: Wenn Sie vor jedem Programmierschritt einen Test erstellen, setzen Sie sich intensiver mit dem Ziel ausei-nander.
  • Die Wahrscheinlichkeit, nach der Erstellung einigen Codes festzustellen, dass Sie eigentlich am Ziel vorbeiprogrammiert haben, ist geringer.
  • Schritt für Schritt statt Entwicklung im Multitasking-Stil: Erst wenn der vorherige Test positiv ausfällt (und die damit verbundene Code-änderung keine älteren Tests scheitern lässt), dürfen Sie einen neuen Test und neue Funktionalität hinzufügen. Vorteil: Sie arbeiten immer nur an einer Baustelle; wenn ein oder mehrere Tests durch neuen Code fehlschlagen, wissen Sie sofort, woran es liegt.
  • Absicherung: Dadurch, dass Sie mit jedem neuen Test auch alle anderen Tests ausführen, sind Sie immer sicher, dass Sie durch Hinzufügen neuer Funktionen oder Refactoring nichts Funktionierendes zerstören.
  • Durch den automatisierten Ablauf der Tests können Sie sich jederzeit davon überzeugen, dass noch alles entsprechend der Spezifikation funktioniert.
  • Hinzu kommen die folgenden Vorteile:
  • Mit jedem Test stellen Sie sich eine neue Aufgabe, die Sie schnell erfüllen können – außer, Sie haben den Anspruch an den Test zu hoch angesetzt.
  • Sie haben eine Menge kleiner Erfolgserlebnisse.
  • Sie können jederzeit, wenn Sie einen Test erfolgreich durchgeführt haben, Pause oder Feierabend machen in dem Gefühl, dass die Anwendung im aktuellen Zustand wie gewünscht läuft.
  • Tests sind eine sehr genaue Formulierung von Anfordungen. Sie können mit ihnen sehr schnell feststellen, ob die tatsächlichen Anforderungen umgesetzt wurden.
  • Tests sind Dokumentation: Wenn Sie für alle Methoden, Eigenschaften und Ereignisse einer Klasse Tests schreiben, können Entwickler, die sich anschließend mit Weiterentwicklungen oder änderungen der Anwendung beschäftigen, diese Tests als Dokumentation heranziehen.
  • Nachdem die theoretischen Grundlagen Ihr Interesse geweckt haben, lernen Sie nun den praktischen Ablauf kennen.

    Sie haben das Ende des frei verfügbaren Textes erreicht. Möchten Sie ...

    TestzugangOder haben Sie bereits Zugangsdaten? Dann loggen Sie sich gleich hier ein:

    Schreibe einen Kommentar