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.

    Dazu sind einige Vorbemerkungen erforderlich: Die testgetriebene Entwicklung wurde zuerst in Zusammenhang mit objektorientierten Sprachen eingesetzt. Sie können diese Entwicklungsmethode natürlich auch für die Entwicklung mit prozeduralen Sprachen heranziehen. Es ist aber zu empfehlen, sich direkt mit der objektorientierten Entwicklung im Rahmen der Möglichkeiten von VBA auseinanderzusetzen (s. Beitrag Objektorientierte Entwicklung mit Access).

    Die meisten Quellen zum Thema testgetriebene Entwicklung enthalten in der Regel keine Hinweise zum Testen von Benutzeroberflächen. In gewisser Weise können Sie die testgetriebene Entwicklung aber dennoch dazu verwenden: Formulare, die ja den größten Teil der Benutzeroberfläche ausmachen, sind eigentlich ebenfalls Objekte mit Methoden, Eigenschaften und Ereignissen. Der einzige Unterschied zu einem aus einer herkömmlichen Klasse erzeugten Objekt ist, dass es eine Benutzeroberfläche hat. Wie Sie Formulare testgetrieben entwickeln, erfahren Sie in einem der Update-Magazine zu diesem Werk.

    Das Werkzeug: accessUnit

    Die Werkzeuge zum Durchführen von Unit Tests heißen Testframework. Die Namen der entsprechenden Testframeworks für die unterschiedlichen Programmiersprachen sind immer nach dem gleichen Muster aufgebaut und enden auf Unit. Für Java gibt es unter anderem JUnit, für .NET NUnit und für Visual Basic vbUnit. Das nachfolgend vorgestellte Testframework ist einer der jüngeren Vertreter, aber ähnlich aufgebaut wie die anderen: accessUnit.

    Hinweis

    Sie finden die bei Drucklegung aktuelle Version von accessUnit auf der beiliegenden CD. Um neuere Versionen und Update-Informationen zu erhalten, besuchen Sie einfach im Internet die Seite www.accessunit.de.

    accessUnit bietet eine grafische Benutzeroberfläche zur Darstellung des Ablaufs der Tests sowie der im Anschluss vorliegenden Testergebnisse (s. Abb. 1).

    Hinweis

    Das Testframework accessUnit funktioniert in der zum Zeitpunkt der Drucklegung dieses Textes vorliegenden Version mit Access 2000 und höher.

    Abb. 1: Das Testframework accessUnit im Einsatz

    Installation von accessUnit

    accessUnit liegt in Form eines Formulars, eines Makros und einiger Klassenmodule vor, die in der Datenbankdatei accessUnit.mdb zu finden sind.

    Sie können das Unit-Testing-Framework nachträglich in eine Datenbank einbinden oder eine neue Datenbank damit entwickeln.

    Im ersten Fall importieren Sie einfach alle Objekte der Datenbank accessUnit.mdb in die Zieldatenbank. Das Framework steht dann sofort zur Verfügung.

    Falls Sie eine neue Datenbank mit Hilfe des Unit-Testing-Frameworks entwickeln möchten, erstellen Sie einfach eine Kopie der Datenbank accessUnit.mdb und speichern Sie diese unter dem gewünschten Namen.

    Elemente von accessUnit

    Vor dem ersten Beispiel sollen Sie noch kurz die wichtigsten Elemente des accessUnit-Frameworks kennen lernen.

    Die Benutzeroberfläche besteht aus dem Formular frmTestrunner, das eine Schaltfläche zum Starten der Tests und Steuerelemente zur Ausgabe der Testergebnisse liefert.

    Option Compare Database
    Option Explicit
    Public Sub Suite(objTestsuite As Object)
        objTestsuite.AddTest New clsSampleTest
    End Sub

    Quellcode 1

    Public Function TestsuiteWrapper(strTestsuitename _    As String) As Object
        Select Case strTestsuitename
            Case "clsTestsuite"
                Set TestsuiteWrapper = New clsTestsuite
        End Select
    End Function

    Quellcode 2

    Option Compare Database
    Option Explicit
    Public Sub Setup()
    End Sub
    Public Sub Teardown()
    End Sub
    Public Property Get Fixturename() As String
        Fixturename = "clsSampleTest"
    End Property
    Public Sub Test1(objTestcase As aUTestcase)
        On Error GoTo RunTest_Err
        objTestcase.Assert "Sample assertion 1a", True
        objTestcase.Assert "Sample assertion 1b", True
        Exit Sub
    RunTest_Err:
        objTestcase.Assert "#Error in " & Me.Fixturename, False
        Resume Next
    End Sub

    Quellcode 3

    Charakteristisch für Unit-Testing-Frameworks mit Benutzeroberfläche ist dabei je nach Testergebnis die Anzeige eines roten oder grünen Balkens. Der rote Balken bringt in der Regel eine oder mehrere Meldungen mit sich, die auf den oder die gescheiterten Tests hinweisen.

    Das in der Datenbank enthaltene Autoexec-Makro enthält eine Anweisung, die der VBA-Entwicklungsumgebung ein Menü mit einer Schaltfläche zum Aufrufen des Testrunner-Formulars hinzufügt. So können Sie den Testrunner komfortabel von dort aus aktivieren.

    Neben dem Formular und dem Makro benötigt das Framework einige Module mit der Funktionalität: das Standardmodul aUMenu und die Klassenmodule aUMenuEvents, aUModule, aUTestcase, aUTestsuite und aUTestsuites. Die Modulnamen enthalten das Präfix aU, um die accessUnit-Module leicht von den anderen Modulen unterscheiden zu können.

    Die übrigen Klassen der Datenbank accessUnit.mdb beinhalten die Tests. Sie benötigen auf jeden Fall eine Testsuite.

    Sie enthält die Aufrufe der einzelnen Testcases, die in eigenen Klassen untergebracht sind. Eine solche Testsuite-Klasse sieht etwa wie in Quellcode 1 aus.

    Eine Testsuite enthält nur eine Methode namens Suite. Diese Methode fügt der Testsuite mit der AddTest-Methode eine oder mehrere Testklassen hinzu.

    Die Testsuite dieses Beispiels sorgt für die Ausführung der in dem Klassenmodul clsSampleTest enthaltenen Tests.

    Damit Sie eine Testsuite über den Testrunner aufrufen können, müssen Sie einen Eintrag wie in dem Code aus Quellcode 2 in der Klasse aUTestsuites anlegen.

    Fehlt noch der eigentliche Test. Jeder Test wird in einer Testklasse untergebracht. Eine einfache Testklasse sieht wie in Quellcode 3 aus.

    Einen Test bringt man in je einer Methode unter, deren Methodenname mit “Test” beginnen muss. Ein Test besteht aus einer oder mehreren Assertions (deutsch: Absicherung), die Werte von (Funktions-)Methoden oder Eigenschaften der zu testenden Klasse überprüfen. Eine Assertion hat zwei Parameter: eine aussagekräftige Bezeichnung dessen, was getestet wird, sowie einen Bool”schen Ausdruck als Ergebnis der Assertion.

    Ein oder mehrere Tests, die sich in der gleichen Testklasse befinden und denselben Aspekt einer Klasse testen – etwa eine Methode oder Eigenschaft -, nennt man Testcase.

    Die verschiedenen Tests einer Testklasse erfordern häufig die gleiche Startkonfiguration, und wenn es sich nur um das Instanzieren der zu testenden Klasse handelt. Oft kommen noch weitere Vorbereitungen wie beispielsweise das Anlegen von Testdaten hinzu. Damit man die entsprechenden Anweisungen nicht in jede einzelne Test-Methode einbauen muss, verwenden alle Tests einer Testklasse eine gemeinsame Methode, die alle notwendigen Vorbereitungen enthält. Diese Methode heißt Setup.

    Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

    den kompletten Artikel im PDF-Format mit Beispieldatenbank

    diesen und alle anderen Artikel mit dem Jahresabo

    Schreibe einen Kommentar