Wer kennt diesen Ablauf nicht: In einem Workshop mit dem Kunden werden Anforderungen gesammelt; das Team erstellt den (mehr oder minder) perfekten Quellcode, der alle Anforderungen umsetzt, und präsentiert die Lösung stolz dem Kunden, der entgegnet: "Großartiges Ding, aber eigentlich dachten wir ..." Wurzel des Übels ist, dass die natürliche Sprache, mit der Anforderungen formuliert werden, immer Raum für Zweideutigkeiten und versteckte Unschlüssigkeiten lässt, selbst wenn die Formulierungen noch so ausgefuchst sind. Die Diskrepanz zwischen dem, was die einen sagen, die anderen verstehen und die einen meinen, dass die anderen verstanden haben, ist oft sehr groß - dies lässt viele Projekte scheitern. Doch wohin führt der Weg, Anforderungen ein wenig formaler zu dokumentieren?
Was für den Entwickler sein "Boxes and Line"-Diagramm ist, das sind für die Vertreter des Fachbereichs ihre vorzüglich in Excel gestalteten Tabellen. Warum sollte man also nicht den Fachbereich die Kriterien, nach denen er die Umsetzung einer Anforderung wirklich akzeptiert (den so genannten Akzeptanztest) in Tabellenform hinterlegen lassen - also seine Sprache sprechen? Als angenehmer Nebeneffekt könnte dabei auch eine Selbstregulierung der Anforderungsflut aufkommen: Der Fordernde hat nun auch Aufwand, eine Anforderung zu definieren, und muss sich stärker mit dem Innenleben einer Anforderung auseinander setzen bzw. damit, wie sinnvoll (bzw. sinnfrei) eine Anforderung ist.
| Fit 1.1 |
| Kurzbeschreibung: Akzeptanztest-Framework Lizenz: GNU GPL Unterstützte Betriebssysteme: Windows und alle Unix-Derivate mit Java-Unterstützung. Für die .NET-Version werden Windows und das .NET Framework benötigt. Download-URL: fit.c2.com/wiki.cgi?DownloadNow Dokumentations-URL: fit.c2.com/wiki.cgi?FitDocumentation |
Fit: Building the Right Code
Genau an diesem Punkt setzt Fit [1] mit seinem Partner FitNessse [2] an. Fit ist der eigentliche Kern: ein Framework zur Entwicklung von Akzeptanztests. Ausgehend von einer Anforderungsbeschreibung (ruhig auch in Prosa) wird in tabellarischer Form ein Akzeptanztest dazu hinterlegt (Abb. 1). Akzeptanztests sind sowohl Fallnetz als auch Zielbeschreibung eines Entwicklungsprojekts: Nach einem Refactoring oder einer Erweiterung sichern sie die bereits erreichte Funktionalität und zeigen auf, welche zusätzliche Funktionalität der Kunde noch vom System erwartet. Die Kennzahl RTF (Running, Tested Features) gilt als eine der aussagekräftigsten über den Projektfortschritt in einem Softwareprojekt überhaupt.
Die Testtablen samt erläuternder Prosa müssen im HTML-Format oder als Excel-Sheet vorliegen, damit Fit in Aktion treten kann. Fit führt die Testtablen aus und markiert Zellen, die im Testlauf gescheitert sind (Abb. 2).
Als Klebstoff zwischen dem in Tabellen gepressten Akzeptanztest und der eigentlichen Anwendung dienen „Fixtures“ - die Adaptoren zwischen den Testtablen und dem Anwendungscode. Sie interpretieren die Tabellendaten und bestimmen damit den Testlauf gegen die Anwendung (Abb. 1). Fixtures sind somit die Mittler zwischen den von Analysten erzeugten (und evtl. von den Entwicklern in eine geordnete Form gebrachten) tabellarischen Akzeptanztests und dem Anwendungscode.
JUnit: Building the Code Right
Nun stellt sich natürlich die Frage, inwiefern sich Fit und JUnit voneinander abgrenzen. Obwohl sich JUnit mitsamt seinen Satelliten-Frameworks mittlerweile für alle Testebenen berufen fühlt, ist es dennoch im Feld der Unit- und Komponententests zu Hause. Diese Testarten sichern die Robustheit des Codes von innen. Sie sind von Entwicklern für Entwickler geschrieben, um sich mit einem grünen Balken gegen das manchmal drückende schlechte Gewissen abzusichern. Fit zieht, wie schon beschrieben, den Kunden mit in den Testprozess ein und wird damit zum kongenialen Partner von JUnit. Im Gegensatz zu JUnit-Whitebox-Tests ist ein Fit-Test ein Blackbox-Test: Er kennt nur die Außensicht des Systems. Fit-Tests sind von Analysten für Entwickler geschrieben. Zielgruppe an Testlingen sind bei Fit somit Anwendungsfälle und Anwendungsfunktionen - keine Rede von Klassen und Komponenten. Dabei spielt Fit gegenüber JUnit besonders dann seine Vorteile aus, wenn die Testdaten entweder noch nicht klar oder recht umfangreich sind.Hello Fit
Im Praxiseinsatz wählt man bei Fit oft, und zu Recht, ein Top-down-Vorgehen: Zunächst werden Akzeptanztests in Tabellenform entworfen, dann Fixtures dafür entwickelt und diese gegen die Anwendungsschnittstellen implementiert. Dabei kann ein Akzeptanztest aus mehreren unterschiedlichen Tabellen bestehen, die gemeinsam ein Testszenario beschreiben (Listing 5). Die zu den Tabellen gehörigen Fixture-Klassen müssen in Fit dabei immer von der Klasse fit.Fixture erben, um von Fit ausgeführt werden zu können. Da sich Fit ein Framework nennt, bietet es dementsprechend schon einige vorgefertigte und alltagstaugliche Fixture-Implementierungen (Abb. 3):- ColumnFixture (Listing 1 und 2): Überprüfung von Berechnungen und Algorithmen durch zeilenweise Überprüfung von Testdatensätzen (Eingaben samt erwarteter Ausgaben).
- ActionFixture, TimedActionFixture (Listing 3 und 4): Test von Zustandsmaschinen durch Auslösen von Zustandsübergängen (press, enter) und Zustandsüberprüfung (check). Eine TimedActionFixture erweitert die ActionFixture um eine Zeitmessung der einzelnen Aktionen. Die Zeitmessung wird mit in der Ergebnistabelle ausgegeben.
- RowFixture (Listing 5 und 6): Überprüfung von Ausgabelisten. Diese Fixture-Art macht oft nur zusammen mit einer ColumnFixture oder ActionFixture Sinn.
Diese mitgelieferten Fixtures können entweder durch eigene Fixture-Klassen erweitert (im Fall der ColumnFixture und RowFixture) oder direkt genutzt werden (im Fall der ActionFixture und TimedActionFixture). Eine genaue Beschreibung der Konventionen, die bei der Erstellung von Testtablen und Fixtures gelten, ist dem Kasten "Fit-Konventionen" zu entnehmen. Die FitLibrary
[3] enthält weitere sehr interessante vorgefertigte Fixtures (hervorzuheben sind die DoFixture, ImageFixture, SetFixture und SetUpFixture), deren Beschreibung aber den Rahmen dieses Artikels sprengen würde.
Fit-Tabelle - FitNesse-Code|de.qaware.research.fitnesse.StringFormatFixture||in|capitalizeWord()||Hat der alte Hexenmeister sich doch einmal wegbegeben!|Hat Der Alte Hexenmeister Sich Doch Einmal Wegbegeben!||Und nun sollen seine Geister auch nach meinem Willen leben.|Und Nun Sollen Seine Geister Auch Nach Meinem Willen Leben.||Seine Wort' und Werke merkt ich und den Brauch, und mit Geistesstärke tu ich Wunder auch|Seine Wort' Und Werke Merkt Ich Und Den Brauch, Und Mit Geistesstärke Tu Ich Wunder Auch|
ColumnFixture (StringFormat)public class StringFormatFixture extends ColumnFixture {public String in;public String capitalizeWord(){return StringFormat.capitalizeString(in);}}
FitNesse-Code|fit.TimedActionFixture||start|de.qaware.research.fitnesse.CalculatorFixture||enter|calculator|7||press|plus||enter|calculator|5||press|equals||check|calculator|12|
Listing 4
Fixture (Calculator)public class CalculatorFixture extends Fixture {private RealCalculator calculator = new RealCalculator();// buttonpublic void plus() {calculator.plus();}public void equals() {calculator.equals();}// checkpublic int calculator() {return calculator.getValue();}// enterpublic void calculator(int c) {calculator.enterValue(c);}}
FitNesse-Code'''Anforderung: Kunden müssen angelegt werden können.'''Ein Kunde wird repräsentiert durch seinen Namen und seine Adresse (Straße + Nr., Ort und PLZ)|de.qaware.research.fitnesse.CustomerCreateFixture||name|road|town|zip|createCustomer()||Testman|Road to Nowhere 99|Middle of Nowhere|CA 90045|true||Hans Dampf|Alle Gassen 1|München|D-81737|true||Mon Monsieur|Allée les Chèvrefeuilles 5|Une Ville|F-30400|true|'''Anforderung: Alle bereits angelegten Kunden müssen ausgelesen werden können.'''|de.qaware.research.fitnesse.CustomerReadFixture||name|road|town|zip||Testman|Road to Nowhere 99|Middle of Nowhere|CA 90045||Hans Dampf|Alle Gassen 1|München|D-81737||Mon Monsieur|Allée les Chèvrefeuilles 5|Une Ville|F-30400|
Beispiel-Test des CustomerManager (Fixtures)//Erzeugt Kundenpublic class CustomerCreateFixture extends ColumnFixture {public String name;public String road;public String town;public String zip;private CustomerManager cm;public CustomerCreateFixture(){cm = CustomerApplication.getInstance().getCustomerManager();}public boolean createCustomer(){//an Anwendungscode delegierenCustomer cust = cm.createCustomer(name, road, town, zip);return cust != null;}}//Liest alle Kunden auspublic class CustomerReadFixture extends RowFixture {public Object[] query() throws Exception {//an Anwendungscode delegierenCollection customers = CustomerApplication.getInstance().getCustomerManager().getCustomers();Collection result = new ArrayList();//Domänenobjekte in für die Fixture ausgelegten Werttypen transformierenfor (Iterator it = customers.iterator(); it.hasNext();){Customer customer = (Customer)it.next();result.add(new CustomerVO( customer.getName(), customer.getRoad(),customer.getTown(), customer.getZip() ));}return result.toArray();}public Class getTargetClass() {return CustomerVO.class;}public static class CustomerVO {public String name;public String road;public String town;public String zip;public CustomerVO(String name, String road, String town, String zip) {this.name = name;this.road = road;this.town = town;this.zip = zip;}}}
FitNesse Testsuite|FitForFun.TestCalculator|''Testet einen Taschenrechner''||FitForFun.TestStringFormat|''Testet eine Funktion zur Stringformatierung''||FitForFun.TestCustomer|''Testet eine einfache Kundenkomponente''|!path classes!path lib/fitnesse/fitnesse.jar!path lib/fit/fit.jar
Fit-Konventionen
Konventionen der TabellenstrukturEine Testtable muss als erstes Feld immer den voll qualifizierten Klassennamen der Fixture enthalten, die den folgenden Tabelleninhalt interpretieren soll. Die Konvention für den weiteren Aufbau einer Tabelle bestimmt die jeweilige Fixture.
Konventionen der vorgefertigten Fixtures
- ColumnFixture
Tabelle: Eine Spalte der Tabelle entspricht einem bestimmten Eingabe- bzw. Ausgabeparameter. Eine Zeile ist somit ein Testdatensatz, wobei in der ersten Tabellenzeile die Parameter benannt werden müssen. Ausgaben werden von Eingaben dadurch unterschieden, dass nach ihrem Namen "()" angehängt wird. Wird bei einer Zeile erwartet, dass die Eingaben eine Ausnahme verursachen, so muss in der Ausgabespalte das Schlüsselwort error angegeben werden.
Fixture-Implementierung: Die konkrete Fixture muss fit.ColumnFixture erweitern. Für alle in der entsprechenden Tabelle definierten Eingaben muss ein gleich benanntes öffentliches Attribut in der Klasse vorhanden sein (Listing 2: in). Für die Ausgaben muss eine entsprechend benannte öffentliche Methode in der Fixture bereitstehen. - ActionFixture
Tabelle: Die Tabelle benötigt keinen Tabellenkopf (neben der Angabe fit.ActionFixture). Jede Zeile repräsentiert eine Aktion und enthält zwei bis drei Spalten. Die erste Spalte entspricht dabei immer einem der folgenden Kommandos.
- start: Eine Actor-Fixture wird gestartet. Auf sie beziehen sich alle darauf folgenden Anweisungen. Die ActionFixture kann mehrere Fixtures in einer Testtabelle starten.
- enter: Der in der dritten Spalte angegebene Wert wird dem Identifier aus Spalte 2 zugewiesen.
- press: Ein Zustandsübergang wird durch Drücken eines virtuellen Buttons (im Prinzip eine Methode) mit dem Namen aus Spalte 2 ausgelöst.
- check: Es wird geprüft, ob der Identifier aus Spalte 2 den in Spalte 3 angegebene Wert repräsentiert.
Fixture-Implementierung: Wird die ActionFixture verwendet, so muss man nur die Actor-Fixtures implementieren. Hierzu muss man lediglich die Klasse Fixture um einen Satz an Methoden erweitern, die die in der Testtabelle angelegten Aktionen repräsentieren. Für die einzelnen Kommandos gelten dabei unterschiedliche Konventionen: Für das enter-Kommando muss eine entsprechend dem in der Tabelle angegeben Identifier benannte öffentliche Methode mit einem Parameter und dem Rückgabewert void implementiert werden. Für das check-Kommando gilt die umgekehrte Regel: kein Parameter und ein Rückgabewert. Dem press-Kommando genügt eine entsprechend benannte Methode ohne Parameter und Rückgabewert. - RowFixture
Tabelle: Die Tabelle repräsentiert eine erwartete Ergebnisliste. Eine Zeile der Tabelle entspricht somit einem Datensatz in der Ergebnisliste. Der Tabellenkopf besteht aus den Namen der jeweiligen Datensatzattribute. Die Tabelle fordert, dass die Ergebnisliste mindestens die angegebenen Datensätze enthält. Spielt die Ordnung der Datensätze eine Rolle, so kann eine zusätzliche Spalte mit dem Titel order eingeführt werden, in dem die geforderte Position in der Ergebnisliste angegeben werden kann.
Fixture-Implementierung: fit.RowFixture ist die Basisklasse, die diese Fixtures erweitern müssen. Die Basisklasse ist abstrakt und fordert auf, die Methoden void query():Object[] und getTargetClass():Class zu implementieren. Die query()-Funktion soll dabei die zu prüfende Ergebnisliste zurückgeben und getTargetClass() gibt dem Fit-Testrunner die Information, welchen Typ die einzelnen Elemente der Ergebnisliste haben. Von diesem Typ fordert Fit, dass alle Attribute, die in der Tabelle verwendet werden, als öffentliche Felder vorliegen müssen. Aus diesem Grund ist es oft notwendig, eine Zwischenrepräsentation eines Domänenobjekts anzulegen (Listing 6), wie man es schon vom Datenaustausch zwischen der Präsentationsschicht und der Anwendungslogik in Form der Value Objects kennt.
FitNesse, das fitte Frontend
| FitNesse (vom 31. Juli 2005) |
| Kurzbeschreibung: Wiki mit Anbindung an das Fit-Framework. Kollaborative Akzeptanztests. Lizenz: GNU GPL Betriebssysteme: Windows und alle Unix-Derivate mit Java-Unterstützung. Für die .NET-Version werden Windows und das .NET Framework benötigt. Download-URL: www.fitnesse.org/FitNesse.DownLoad Dokumentations-URL: www.fitnesse.org/FitNesse.UserGuide |
Fit allein bietet keine Unterstützung bei der Erstellung der Testtablen. Man ist auf Excel oder einen HTML-Editor angewiesen, wie z.B. Dreamweaver (oder das ganz ordinäre Word). Da der Fit-Erfinder Ward Cunningham gleichzeitig Urheber des Wiki ist, liegt es natürlich nahe, Fit mit einem Wiki zu kombinieren, um die Akzeptanztests unkompliziert per Webbrowser pflegen zu können. Genau dafür dient FitNesse. Fit-Tests werden in FitNesse als Wiki-Seiten hinterlegt und können direkt innerhalb des Wiki ausgeführt werden. Abpictureung 4 zeigt das Zusammenspiel von Fit und FitNesse. Die Installation von FitNesse ist narrensicher: Ein Webserver wird mitgeliefert. Einfach das Archiv entpacken und run.bat starten. Eventuell muss dabei noch der Port, an dem der FitNesse-Webserver lauscht, per -p-Parameter verändert werden. Die Bedienung des Wiki ist, entsprechend der Zielgruppe, intuitiv. Alle möglichen Aktionen sind über die Leiste am linken Seitenrand zugänglich. Per Edit können die aktuelle Seite bearbeitet und ein neuer Akzeptanztest definiert werden. Die dabei zu verwendende Notation kann den Listings 1, 3 und 5 sowie einer allgemeinen Übersicht der Abpictureung 4 entnommen werden. Über Test werden die auf der aktuellen Wiki-Seite definierten Testtablen ausgeführt. Damit das funktioniert, müssen drei Dinge erledigt werden:
- In den Properties die aktuelle Wiki-Seite als Test markieren.
- Auf der Wiki-Seite den Java-Classpath angeben, über den alle auf der Seite verwendeten Fixtures und die Fit- und FitNesse-Klassen zugänglich sind. Dies geschieht über das Wiki-Markup !path. Sind die Pfadangaben relativ, so beziehen sie sich auf das Ausführungsverzeichnis von run.bat.
- Alle "?" hinter Klassennamen dadurch eliminieren, dass ein Inhalt (kann auch eine leere Wiki-Seite sein) hinterlegt wird. FitNesse interpretiert die CamelCase-Notation der Klassen als Wiki-Link und würde ohne eine dahinter liegenden Seite dem Fit-Testrunner den Klassennamen mitsamt dem "?" übergeben. Die Aktion RecentChanges ist recht hilfreich, sich als Entwickler eine Übersicht darüber zu verschaffen, wo die aktuellen Hotspots der Analysten sind und wo Bedarf besteht, ein Fixture anzupassen bzw. zu entwickeln.
| kursiv | ''kursiv'' |
| Fett | '''fett''' |
| See: TestStringFormat (Cross-Reference) | !see TestStringFormat |
| Titel, Überschrift, Unter-Überschrift | !1 Titel, !2 Überschrift, !3 Unter-Überschrift |
| Zentrierter Text | !c Zentrierter Text |
| Notiz / Anmerkung | !note Notiz / Anmerkung |
| LinkZuEinerWikiSeite | The syntax of a wiki word is Camel Case : an alternating pattern of upper and lower case letters. Strictly speaking a wiki word is a string of two more capital letters with lower case letters or numbers between them. |
| Alias-Link auf Wiki-Seite | [[Alias-Link auf Wiki-Seite][LinkZuEinerWikiSeite]] |
| Liste (ungeordnet) Unterliste (geordnet) |
|
|
classpath: c:\directory (Angabe Classpath für einen Test) |
!path c:\directory |
| [Tabelle] | |Alpha| |Beta|gamma|Delta| |1|2|3| |
Fazit
Das Anforderungsmanagement fordert von seinen produzierten Anforderungen, dass sie komplett, präzise und deterministisch sind. All diese Punkte kann man durch den Einsatz von Fit positiv beeinflussen: Die Anforderung werden kompletter, da es nun Aufgabe des Kunden ist, zu allen Anforderungen entsprechende Akzeptanztests zu definieren. Die Anforderungen werden präziser, da sie frühzeitig an Beispielen geklärt werden können. Die Anforderungen werden deterministischer, da Fit-Tests entweder erfolgreich oder nicht erfolgreich sein können. All diese Punkte stärken das Vertrauen des Kunden in die Software. Besonders im Zusammenwirken mit einem Bug-Tracking-System, wie z.B. dem ebenfalls Wiki-basierten Trac [7], kann FitNesse seine Stärke als Fallnetz der SW-Entwicklung ausspielen - Links auf Bugs können direkt neben dem Akzeptanztest platziert werden. Durch die Verfolgung der Tabellen hin zu den Fixtures und hin zum Anwendungscode ist eine Zuordnung einer Anforderung zum Code, der sie umsetzt, einfach möglich.Auch im Zusammenhang mit dem Softwaredesign sind positive Nebeneffekte zu vermelden. Alle Codeteile, die durch Fixtures geprüft werden, müssen entweder eine schmale, dienstorientierte Schnittstelle anbieten oder das entsprechende Fixture erfordert hohen Implementierungsaufwand. Der Leidensdruck ist dann oft so groß, dass man die Fixture-Forderungen schon beim Design der Schnittstellen berücksichtigt. Damit werden implizit lose Kopplung und dienstorientierte Schnittstellen gefördert - an Schichtengrenzen (und dort testet Fit) ein sehr gefragter Architekturstil.
So viel zum Licht. Es können aber auch Schatten ausgemacht werden. So fehlen Plug-ins für Entwicklungsumgebungen, die bei der Entwicklung von Fit-Tests unterstützen und den Testlauf samt ihrer Resultate enger einbinden. Für das Build-Tool Maven ist ein FitNesse-Plug-in verfügbar [8], mit dem die Akzeptanztests in den täglichen Build eingebunden werden können. Für Ant steht ein Fit-Plug-in zum Download bereit [9]. Eine letzte Info für Java-Fremdgänger: Fit (und teilweise auch eine FitNesse-Anbindung) sind auch für .NET, C++, Delphi, Lisp, Objective C, Python, Ruby, Smalltalk und Perl verfügbar.
Josef Adersberger (Josef.Adersberger@QAware.de) ist Leiter des Bereichs Improvement & Innovation bei QAware und Dozent an der Fachhochschule Rosenheim. Sein Hauptinteresse gilt der Software-Qualitätssicherung, der technologieübergreifenden Softwarearchitektur und -produktionsumgebungen.
Links & Literatur
- [1] Fit-Framework: fit.c2.com
- [2] FitNesse: www.fitnesse.org
- [3] FitLibrary User-Guide: fitnesse.org/FitNesse.FitLibraryUserGuide
- [4] FitNesse Markup-Referenz: fitnesse.org/FitNesse.MarkupLanguageReference
- [5] Frank Westphal: Testgetriebene Entwicklung mit JUnit & Fit, dpunkt, 2005
- [6] Rick Mugridge, Ward Cunningham: Fit for Developing Software, Prentice Hall, 2005
- [7] Trac: www.edgewall.com/trac/
- [8] Maven FitNesse Plug-in: fitnesse.org/PluginsPage.MavenPlugIn
- [9] AntFit: www.cmdev.com/antfit/




