Testgetriebene Entwicklung mit Access

15.04.2006 von André Minhorst
Die Entwicklung von Access-Datenbanken und Begriffe wie „Testgetriebene Entwicklung“, „Unit-Testing“ oder „Refactoring“ passen nicht unbedingt zusammen – zumindest wenn man der landläufigen Meinung glaubt oder die eine oder andere Google-Suche hinzuzieht. Dabei hat die testgetriebene Entwicklung Vorteile, die nicht nur der objektorientierten Welt vorbehalten sind.

Der übliche Verlauf bei der Entwicklung einer nwendung ist wohl so, dass man einen Teil der nwendung entwickelt – etwa eine Routine, ein ormular oder einen Bericht – und sich dann, enn dieser Teil wie erwartet funktioniert, dem ächsten Teil zuwendet.

Das klappt mehr oder weniger gut, denn bei eitem nicht alle auf den Weg gebrachten Entwicklungen rfahren die Weihen des Praxiseinsatzes. nd diejenigen, die so weit kommen, ringen die Entwickler beim ersten Ruf nach Erweiterungen ur Verzweiflung, weil jede Änderung ich auf unvorhergesehene Weise auf andere nwendungsbereiche auswirkt.

Wie soll die testgetriebene Entwicklung die Anwendung nun vor einem solchen Schicksal schützen?

Kurz gesagt: Indem man dabei für jedes noch o kleine Feature einen automatisierten Test chreibt, der jederzeit per Knopfdruck wiederholt erden und zeigen kann, dass alles wie gewünscht unktioniert. Wichtig ist dabei tatsächlich, ass man sich auf „kleine Einheiten“ – eshalb der Begriff „Unit Testing“ – konzentriert, enn je genauer man die Stelle definiert, an der in Test scheitern könnte, um so einfacher ist das eheben der zugrunde liegenden Funktionalität.

Test-Framework

Die wichtigste Hilfe bei der testgetriebenen Entwicklung ist ein Framework, das den Entwickler beim Schreiben und Durchführen der automatisierten Tests hilft. Das in diesem Beitrag verwendete Testframework heißt accessUnit. Es bietet nicht so viele Features wie die Tools für objektorientierte Sprachen JUnit (für Java) oder NUnit (für die .NET-Sprachen), reicht aber für grundlegende Tests aus. accessUnit ist kostenlos undkommt als DLL mit einigen weiteren Tools. Den Download finden Sie unter www.accessunit.de/ accessVBATools.zip.

Bild 1: Die accessVBATools nach der Installation.

Zur Installation entpacken Sie alle in dem .zip- File enthaltenen Dateien in ein gemeinsames Verzeichnis und rufen die Datei register.bat auf – fertig! Beim nächsten Start der VBA-Entwicklungsumgebungvon Access finden Sie eine neue Menüleiste mit allem, was Sie zur testgetriebenen Entwicklung benötigen (Bild 1). Die gleichen Befehle wie in diesem Menü enthält übrigens auch das Kontextmenü des Codefensters.

Grundlagen

Es gibt eine Unmenge von Argumenten für den Einsatz estgetriebener Entwicklung. Leider bietet dieser Beitrag acht den Rahmen, um erschöpfend darauf einzugehen. enau genommen konzentriert sich der Beitrag auf die Erläuterung des Frameworks accessUnit und der Vorgehensweise beim testgetriebenen Entwickeln. Weitere Informationen finden Sie im Internet: Als Startpunkt bietet sich beispielsweise Wikipedia an, wo Sie als Suchbegriff einfach „Testgetriebene Entwicklung“ eingeben. Schauen Sie außerdem hin und wieder auf www.accessunit.de vorbei, wo Sie zusätzliche Dokumentationen und Updates zu access- Unit finden.

Installieren des accessUnit-Frameworks

Der erste Schritt besteht im „Installieren“ des accessUnit-Frameworks mit dem entsprechenden Menüeintrag. Das Tool legt die zum Framework gehörenden Klassen inklusive einiger Beispieltests und ein Formular in der aktuellen Datenbank an. Der Projektexplorer eines bis dahin leeren Projekts sieht nun wie in Bild 2 aus.

Bild 2: Die Elemente des Testframeworks.

Bis auf die letzten drei Objekte auTestsuites, clsSampleTest und clsTestsuite enthalten alle Elemente lediglich Code, der für die Ausführung der Tests benötigt wird. Nur die genannten Elemente müssen Sie anpassen, wobei Sie zur Klasse auTestsuites gegebenenfalls neue Einträge hinzufügen. Die beiden Klassen clsTestsuite und cls- SampleTest sind Beispieltestklassen, die Sie direkt für Ihre eigenen Tests einsetzen können.

Um den Zusammenhang zwischen den drei Klassenarten zu verstehen, müssen Sie nur Folgendes wissen:

Testsuites

Zur Verdeutlichung schauen Sie sich die Beispielklassen an. Die Klasse auTestsuites listet alle Testsuites wie in Listing 1 auf. In diesem Fall handelt es sich dabei lediglich um die Testsuite- Klasse clsTestsuite. Wenn Sie weitere Testsuites hinzufügen möchten, fügen Sie einfach einen Eintrag nach folgendem Schema zu der Funktion TestsuiteWrapper hinzu:

Option Compare Database
Option Explicit
Public Function TestsuiteWrapper(strTestsuitename As String) As Object
Select Case strTestsuitename
Case "clsTestsuite"
Set TestsuiteWrapper = New clsTestsuite
End Select
End Function

Case "<Name der Testsuite-Klasse>"
Set TestsuiteWrapper = _
New <Name der Testsuite-Klasse>

Im Moment reicht allerdings die vorhandene Testsuite clsTestsuite aus. Sie können neuen Testsuite- Klassen übrigens beliebige Namen geben. Die Beispiel-Testsuite sieht wie in Listing 2 aus.

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

Die einzige Anweisung der Suite-Methode legt die Klasse clsSampleTest als neuen Test fest. Wenn Sie später weitere Testklassen anlegen möchten, fügen Sie einfach weitere Zeilen nach folgendem Schema hinzu (in einer Zeile):

objTestsuite.AddTest New <Name der Testklasse>

Testfixtures

Mit den Testfixtures geht es nun an den Kern der Sache: Die Testklassen enthalten die eigentlichen Tests sowie die Methoden, die vor und nach jedem Test durchgeführt werden sollen.

Listing 3 zeigt, wie die Beispieltestklasse aussieht. Die Klasse besteht aus den folgenden Elementen:

Option Compare Database
Option Explicit
Public Sub Setup()
'put in test preparations:
'Create testdata, objects...
End Sub
Public Sub Teardown()
'put in things like:
'delete testdata, set object variables to nothing etc.
End Sub
Public Property Get Fixturename() As String
'Set Fixturename = Name of this class
Fixturename = "clsSampleTest"
End Property
'Create as many Test Sub procedures as you want. Each of them will be called by
'the Testmaker and for each this class will be initialized and run the Setup method
'before and the TearDown method after calling the assertions of the current test.
'***Important: The sub's name must begin with 'test...'!***
Public Sub Test1(objTestcase As auTestcase)
On Error GoTo RunTest_Err
'Put in Tests like this:
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
Public Sub Test2(objTestcase As auTestcase)
On Error GoTo RunTest_Err
objTestcase.Assert "Sample assertion 2a", True
objTestcase.Assert "Sample assertion 2b", True
Exit Sub
RunTest_Err:
objTestcase.Assert "#Error in " & Me.Fixturename, False
Resume Next
End Sub

Aufbau eines Tests

Ein Test besteht aus einer oder mehreren Assertions. Dabei handelt es sich um einen Ausdruck, der zeigt, ob die Anwendung an der zu testenden Stelle wie gewünscht funktioniert. Jede dieser Assertions enthält zusätzlich einen Text, der beschreibt, was gerade getestet wird. Dieser Text wird vom Testframework ausgegeben, wenn die festgelegte Annahme nicht zutrifft.

Um eine Annahme zu prüfen, legt man in einer Testmethode einen Ausdruck nach folgendem Schema fest (in einer Zeile):

objTestcase.Assert "<Text, der bei falscher Annahme ausgegeben wird>", <Annahme>

Von diesen Annahmen können Sie beliebig viele in eine Testmethode schreiben.

Ein erster Testlauf

Die von den accessVBATools angelegten Klassen ermöglichen das direkte Ausführen eines Testlaufs. Um einen Test zu starten, wechseln Sie zum Access-Fenster und rufen das Formular frmTestrunner auf.

Das Formular enthält ein Kombinationsfeld, mit dem Sie die gewünschte Testsuite auswählen können (derzeit ist nur eine vorhanden), ein Feld namens Status, das Sie gleich kennen lernen werden, ein Feld namens Results, das zur Anzeige der Ergebnisse des Tests dient, sowie drei Schaltflächen: zum Starten und Stoppen der Tests sowie zum Schließen des Formulars (Bild 3).

Bild 3: Das Formular frmTestrunner dient zum Starten eines Tests und zur Ausgabe der Ergebnisse.

Da bereits ein paar Beispieltests angelegt wurden, klicken Sie einfach einmal auf Start, um die Tests auszuführen. Die Anzeige verändert sich wie in Bild 4: Der grüne Balken deutet auf einen erfolgreichen Test hin und der Text im Results- Fenster verdeutlicht dies.

Bild 4: Das Testrunner-Formular zeigt einen grünen Balken für einen erfolgreichen Test an.

Sehen Sie sich nun an, was während dieses Tests passiert ist: Getestet wurden alle in der Klasse clsTestsuite festgelegten Testklassen. In dem Fall handelt es sich um die Testklasse clsSampleTest. Die Klasse clsSampleTest enthält zwei Testmethoden. Für jede dieser Testmethoden passiert nun Folgendes:

Fehlschlagen eines Tests

Die Testmethoden Test1 und Test2 enthalten als Annahme jeweils den Ausdruck True – und da dieser nun einmal True ist, sind alle Annahmen wahr.

Wenn Sie einmal einen Test scheitern lassen möchten, ersetzen Sie einfach in einer der Assertions den Wert True durch einen Ausdruck, der nicht wahr ist – also beispielsweise False, 1=2 oder Ähnliches. Das Ergebnis sieht dann wie in Bild 5 aus – es erscheint ein roter Balken, und die Klasse, der Testnahme und die Bemerkung zu der falschen Annahme werden ausgegeben.

Bild 5: Ein Test ist fehlgeschlagen.

Entwickeln Sie!'

Nach der notwendigen Einführung in den Aufbau und den grundsätzlichen Ablauf von Tests erfahren Sie nun noch, wie Sie mit diesem Rüstzeug entwickeln sollen.

Nehmen Sie einfach einmal an, dass Sie genau wissen, was eine Funktion oder eine Routine tun soll. Eine Funktion beispielsweise soll Werte zurückliefern, eine Prozedur andere Aufgaben erledigen wie beispielsweise Daten in eine Tabelle schreiben, eine Datei löschen oder ein Formularöffnen.

Wenn Sie nun beispielsweise eine Funktion schreiben, soll diese den Eingabewerten entsprechende Ergebnisse liefern. Diese Werte können je nach der Komplexität der Funktion recht mannigfaltig sein. Wie programmieren Sie eine solche Funktion normalerweise? Richtig, Sie versuchen wahrscheinlich, diese direkt so zu programmieren, dass immer die den Eingabewerten entsprechenden Ergebnisse geliefert werden. Nach dem Erstellen der Funktion testen Sie durch, ob sie die gewünschten Ergebnisse liefert.
Doch das klappt manchmal nicht im ersten Anlauf. Sagen wir mal, nur sieben von zehn Aufrufen lieferten das richtige Ergebnis. Also noch mal ran: Funktion verändern, nicht funktionierende Aufrufe testen, funktioniert, fertig! Und, laufen die anderen Aufrufe noch? Was, das haben Sie nicht mehr geprüft – die liefen doch gerade? Na ja, der Kunde wird’s schon melden.

Und wenn die Funktion später einmal geändert werden muss, testet man selbstverständlich auch nur die Ergebnisse der neuen Eingabewerte – oder?

Wenn Sie sich hier nicht wieder erkennen und jede Funktion immer wieder manuell testen, können Sie sich eine Menge Arbeit sparen. Wenn Sie aber sagen, ja, genau, können Sie sich nicht nur eine Menge Arbeit, sondern auch eine Menge Ärger sparen – indem Sie alles dafür tun, fehlerfreien Code zu produzieren.

Der Test treibt

„Testgetrieben“ entwickeln bedeutet, dass man zu allem, was man entwickelt, zunächst einen Test schreibt. Und wenn Sie die Funktion eines Elements ändern möchten, ändern Sie zunächst den zugrunde liegende Test.

Dabei ist es sehr wichtig, dass Sie den Test nach dem Schreiben zunächst ausführen und damit fehlschlagen lassen, denn nur so können Sie sicher sein, dass der Test auch funktioniert. Erst wenn der Testrunner den roten Balken angezeigt hat, implementieren Sie die durch den Test angekündigte Funktionalität und weisen deren Richtigkeit durch einen grünen Balken nach. Außerdem sollten Sie immer den einfachsten Weg zum erfolgreichen Bestehen des anstehenden Tests wählen.

Für das folgende Beispiel verwenden Sie die bereits vorhandene Testmethode Test1. Angenommen, Sie möchten eine Funktion schreiben, die genau die gleiche Aufgabe wie die DLookup- Funktion hat, zum Beispiel weil die DLookup- Funktion mitunter etwas langsam ist und Sie sich einen Ersatz bauen möchten.

Mit der testgetriebenen Entwicklung geht das ganz einfach: Legen Sie fest, wie die Funktion heißen soll (etwa FastDLookup) und beschreiben Sie anhand von Assertions, welche Ergebnisse die Funktion bei welchem Eingabeparameter liefern soll. Dazu benötigen Sie zunächst einmal ein paar Testdaten. Die Testdaten müssen exakt definiert sein, da sich bei Änderungen der Testdaten während des Tests oder zwischen zwei Tests unter Umständen auch die Testergebnisse ändern können. Daher sollten Sie vor jedem Test sicherstellen, dass die gewünschten Testdaten vorhanden sind. Dies geschieht optimalerweise in der Setup-Methode der Testklasse.

In der Teardown-Methode können Sie die Daten und/oder Tabellen wieder löschen beziehungsweise entfernen.

Aus Platzgründen kann dieser Beitrag das Anlegen von Testdaten nicht behandeln, so dass Sie für den ersten Test einfach die Personal-Tabelleder Nordwind-Datenbank verwenden.

Schreiben Sie dann in die Methode Test1, was Sie von der Funktion FastLookup erwarten (die im Übrigen die gleichen Parameter wie das Pendant DLookup enthalten soll):

Die Funktion soll den Wert 1 für die Personal- Nr ausgeben, wenn als Kriterium Nachname = 'Davolio' angegeben wird. Die Assertion sieht wie folgt aus:

objTestcase.Assert "Datensatz nach Nachname
ermitteln", FastLookup("[Personal-Nr]", "Personal",
"Nachname = 'Davolio'") = 1

Löschen Sie die bereits vorhandenen Assertions und auch die zweite Testklasse (Test2), kompilieren Sie das Projekt und – schon weist Sie ein Kompilierfehler darauf hin, dass die Funktion FastLookup gar nicht definiert ist. Natürlich ist sie das nicht! Sie haben, genau wie geplant, erstmal einen Test mit einer Anforderung geschrieben, der erwartungsgemäß gescheitert ist (wenn auch das Scheitern nicht im Testrunner-Formular angezeigt wurde). Also fügen Sie eine Funktion namens FastLookup in ein Standardmodul ein und legen deren Parameter fest:

Public Function FastLookup(strField As String,
strTable As String, strCriteria As String) As Variant
End Function

Start im Testrunner

Nach erfolgreichem Kompiliervorgang starten Sie nun den Test im Testrunner (wenn Sie vergessen zu kompilieren, erscheint eine entsprechende Meldung). Natürlich scheitert auch dieser Test, weil die Funktion FastLookup noch keinen Wert zurückgibt. Passen Sie nun die Funktion so an, dass der Test auf möglichst einfache Art bestanden wird. Dazu lassen Sie einfach die herkömmliche DLookup-Anweisung arbeiten. Ergänzen Sie die FastLookup-Funktion also durch folgendeZeile:'

FastLookup = DLookup(strField, strTable, strCriteria)

Der Test liefert einen grünen Balken, also legen Sie eine weitere Assertion an, die prüft, ob die Funktion für eine Abfrage, die keinen Datensatz zurückliefert, den Wert Null zurückgibt.

objTestcase.Assert "Nicht vorhandener Datensatz", Is-
Null(FastLookup("[Personal-Nr]", "Personal", "1=2"))

Dieser Test ist auf Anhieb erfolgreich: Auch das ist möglich und dient entsprechend als zusätzliche Absicherung.

Es gibt sicher noch eine Menge anderer Tests zum Sicherstellen der Funktionalität – je genauer Sie hier arbeiten, desto besser. Für Demonstrationszwecke sollen diese beiden Annahmen ausreichen; kümmern Sie sich nun um das eigentliche Problem: Es sollte ja ein Ersatz für die DLookup-Funktion gebaut werden, und keine Wrapper-Funktion.

Public Function FastLookup(strField As String, strTable As String, strCriteria As String) As Variant
Dim strSQL As String
On Error Resume Next
strSQL = "SELECT [" & strField & "] FROM [" & strTable & "]"
If Len(strCriteria) > 0 Then
strSQL = strSQL & " WHERE " & strCriteria
End If
FastLookup = DBEngine(0)(0).OpenRecordset(strSQL, dbOpenForwardOnly)(0)
End Function

Also stellen Sie Ihre eigene DLookup-Funktion zusammen, die wie in Listing 4 aussehen könnte. Kompilieren Sie das Projekt und starten Sie erneut den Test. Die erste Assertion ist richtig, aber die Funktion gibt nicht den Wert Null zurück, wenn sie keinen Datensatz findet. Das müssen Sie noch ändern, indem Sie folgenden Dreizeiler am Ende der Funktion einbauen:

If FastLookup = "" Then
FastLookup = Null
End If

Nun läuft der Test ohne Fehler durch – Fertig! Sie können die Funktion nun jederzeit verändern und per Mausklick prüfen, ob sie noch richtigfunktioniert. Wenn Sie die Funktionalität ändern möchten, müssen Sie natürlich vorher die Tests anpassen – und nicht umgekehrt. Was bei diesem kleinen Beispiel nach verhältnismäßig viel Aufwand aussieht, relativiert sich bei umfangreicheren Projekten. Zwar hat man immer ein ähnliches Verhältnis zwischen Testcode und Produktivcode, aber der Aufwand bei späterenÄnderungen oder beim Refactoring des Codes wird wesentlich geringer, zumindest wenn Sie bis dato nach jeder geänderten Zeile Code die komplette Anwendung auf eventuelle Randeffekte geprüft haben.

Mehr zum Thema

Mit diesem Thema ließen sich leicht mehrere Hefte füllen, aber für den Anfang soll diese Einführung reichen. Interessante Themen sind sicher die Erstellung von Testdaten und deren Verwendung – immerhin soll eine Anwendung ja auch während des Betriebs einmal getestet werden, und das am besten nicht mit Produktivdaten.

Auch die Benutzeroberfläche lässt sich mit accessUnit unter die Lupe nehmen. Dazu sind jedoch in manchen Fällen einige Kniffe erforderlich. Auch hier lässt sich mit einer Automatisierung eine Menge Zeit sparen.

Für das Refactoring von Codes sind Tests, wie in diesem Beitrag vorgestellt wurden, Voraussetzung. Wenn Sie die bestehende Funktionalitätdurch Tests ausreichend abgesichert haben, können Sie nach Herzenslust ändern und per Mausklick prüfen, ob und wo sich die Änderungen ausgewirkt haben. Je feinmaschiger Sie dabei das Testnetz aufgezogen haben, desto weniger laufen Sie Gefahr, dass erst der Benutzer auf einen Fehler aufmerksam wird.