Montag, 13. Oktober 2008

entwickler.com Magazine Konferenzen Entwickler Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




April 2006
aus Linux Enterprise Ausgabe: 10.2004
Test First
Test Driven Development: Wie entwickelt sich Software auf Basis von Tests?
von Olga Kolov und Frank Müller

Entwickler schreiben immer hochwertigen Code, zusätzliches Testen kostet nur Zeit und damit Geld. Dies ist leider oftmals die Ansicht von Entwicklern und Projektmanagern in der Softwareentwicklung. Beide sehen Tests nur als Aufwand an, nicht als integralen Bestandteil ihrer Arbeit, leider. Doch seit Kent Beck seine Idee des eXtreme Programmings formulierte, ist ein Wandel eingetreten.


Das eXtreme Programming besteht aus verschiedenen Verfahren und Maßnahmen, die auf eine leichtgewichtige Art und Weise den Erfolg von Softwareprojekten sichern sollen. Eine davon ist das Unit Testing. Dieses verfolgt die Test First-Strategie und setzt auf durch den Entwickler geschriebene Modultests. Klingt ja alles nett, doch was muss man sich darunter vorstellen? Die Idee der Test First-Strategie ist es, einen Test für eine Softwarekomponente - bei der objektorientierten Programmierung in der Regel Klassen - vor der eigentlichen Komponente zu kodieren. Doch wie soll das gehen?

Der Entwickler einer Klasse ist sich in der Regel ja klar darüber, was er mit seiner Klasse erreichen möchte, er sollte es zumindest sein. So entsteht eine Art Protokoll für die Anwendung der Klasse. Und genau dieses Protokoll wird von einem Unit Test geprüft. Die zu entwickelnde und zu testende Klasse wird dabei immer nur so weit ausprogrammiert, wie es notwendig ist, um einen Test zu erfüllen. Nach jedem erfolgreichen Test wird dann der entwickelte Code in kurzen Zyklen weiter verfeinert und erneut getestet, bis dieser die Geschäftslogik erfüllt und in einem optimalen Zustand ist. Neudeutsch spricht man hier von Refactoring. Der Vorteil dieses Verfahrens ist es, stets über einen funktionierenden Code zu verfügen, der leicht automatisiert getestet werden kann. Insbesondere die Bündelung von Tests zu so genannten Testsuiten ist reizvoll. Nach Änderungen in Frameworks können so leicht Fehler durch Seiteneffekte aufgedeckt und wieder behoben werden.

Obwohl zuerst in Smalltalk als SUnit entwickelt, sind die Unit Tests mit durch Java und JUnit so richtig bekannt geworden. Dieses soll auch als Basis für die kommenden Beispiele dienen. Insgesamt spricht man heute jedoch von den xUnit-Applikationen, die jeweils das Framework und die Ablaufumgebung für die Tests bieten. Gerade bei Java und Smalltalk sind diese inzwischen vielfach in die Entwicklungsumgebungen eingebunden und sind so ein wichtiges Werkzeug in der Entwicklung. Bei Smalltalk geht es sogar soweit, dass aus gescheiterten Tests direkt in den Debugger verzweigt wird, der Code an Ort und Stelle verändert und neu kompiliert wird. Anschließend wird genau an der veränderten Stelle neu aufgesetzt - wohlgemerkt, nicht nur an der Abbruchstelle, sondern auch im Ausführungsstapel aufwärts. Aber dies nur am Rande.

Testfälle
Ein einfaches Beispiel soll einen kurzen Einblick in die Idee des Unit Testings geben. Wie bereits angedeutet, wird ein Test zuerst so entwickelt, dass er so gerade durchläuft. Wie soll man das verstehen? Sagen wir, es soll eine Klasse zur Verwaltung von Visitenkarten nach dem vCard-Standard [VCARD] entwickelt werden. Der Constructor nimmt den formatierten Namen entgegen, ein Getter stellt ihn wieder zur Verfügung. Ein erster Test in JUnit schaut also wie folgt aus:

public class VCardTest extends TestCase {
public void testFormattedName() {
/* create vCard */

VCard vCard = new VCard("Testolf Testermann");

/* test the formatted name /

assertEquals("Testolf Testermann", vCard.getFormattedName());
}
}


Um diesen Test zu kompilieren, ist nun die Definition der VCard notwendig. Puristen implementieren hierbei die erste Version so, dass getFormattedName() eine Konstante zurückliefert, bei einfachen Gettern und Settern ist jedoch auch eine direkte Implementierung kein Problem. Diese schaut dann wie folgt aus:

public class VCard {
private String formattedName;

public VCard(String aFormattedName) {
formattedName = aFormattedName;
}

public String getFormattedName() {
return formattedName;
}
}


Im grafischen TestRunner wird die erfolgreiche Programmierung mit einem grünen Balken belohnt. Erfüllt die Implementierung die Anforderung nicht, so bleibt dieser Balken rot und ein Hinweis auf den fehlgeschlagenen Test wird ausgegeben.


Abb. 1: JUnit in Aktion

Anschließend wird der Code solange verfeinert, bis er aus Entwurfssicht einen optimalen Zustand erreicht hat und dennoch alle Tests besteht. In diesem Fall ist allerdings noch nicht viel Verfeinerung möglich. Darum sei hier als Beispiel noch die Verwaltung von Adressen in vCards genannt. So eine Adresse besteht aus den Attributen type, postOfficeAddress, extendedAddress, street, locality, region, postalCode und country. Ein erster Test sähe also wie folgt aus:

public class VCardAddressTest extends TestCase {
public void testType() {
/* create address */

VCardAddress addr = new VCardAddress(VCardAddress.WORK);

/* test the type /

assertEquals(VCardAddress.WORK, addr.getType());
}
}


Dieser Test funktioniert natürlich nicht ohne die Adresse, die in der ersten Version ohne Verfeinerung wie in Listing 1 definiert wird.

Listing 1

public class VCardAddress {
public final static Integer WORK = new Integer(1);
public final static Integer HOME = new Integer(2);

private Integer type;
private String postOfficeAddress;
private String extendedAddress;
private String street;
private String locality;
private String region;
private String postalCode;
private String country;

public VCardAddress(Integer aType) {
type = aType;
postOfficeAddress = null;
extendedAddress = null;
street = null;
locality = null;
region = null;
postalCode = null;
country = null;
}

public Integer getType() {
return type;
}
}

Noch nicht sehr aufregend, aber eine erste funktionierende Version, die nun in die Klasse VCard integriert werden kann (siehe Listing 2). Und der Test kann - und muss - nun auch angepasst werden (siehe Listing 3).

Listing 2

public class VCard {
private String formattedName;
private HashMap addresses;

public VCard(String aFormattedName) {
formattedName = aFormattedName;
addresses = new HashMap();
}

...

public void addAddress(VCardAddress anAddress) {
addresses.put(anAddress.getType(), anAddress);
}

public VCardAddress getAddress(Integer aType) {
return addresses.get(aType);
}
}


Listing 3

public class VCardTest extends TestCase {
...

public void testAddress() {
/* create vCard, address and add */

VCard vCard = new VCard("Testolf Testermann");
VCardAddress addr = new VCardAddress(VCardAddress.WORK);

vCard.addAddress(addr);

/* test the address /

assertEquals(addr, vCard.getAddress(VCardAddress.WORK));

/* test invalid types */

assertNull(vCard.getAddress(new Integer(4711)));
}
}

Dieser Test schlägt allerdings fehl, da der letzte Test zu einer Ausnahme führt. Der rote Balken im JUnit TestRunner macht dies schnell deutlich. Eine Nachbesserung ist also notwenig:

public class VCard {
...

public VCardAddress getAddress(Integer aType) {
try {
return addresses.get(aType);
} catch (Exception e) {
return null;
}
}
}


So, nun funktioniert es. Dadurch, dass die Unit Tests bereits codiert ist, sind diese Zyklen aus Verfeinerung und Tests sehr schnell. Die heute vielfach anzutreffende Integration von JUnit oder SUnit in die Entwicklungsumgebungen unterstützen dieses Vorgehen. Das einzige, was nun noch fehlt, ist eine TestSuite, die die beiden existierenden Tests - und weitere folgende - zusammenfasst und damit integrierte Tests erleichtert.

public VCardTestSuite extends TestSuite {
public VCardTestSuite () {
super();

addTest(new VCardTest());
addTest(new VCardAddressTest());
}
}


So gerüstet können nun die weiteren Bestandteile der Klassen codiert und weiterhin gleich getestet werden. Der erfolgreiche Durchlauf aller Tests dokumentiert die korrekte Implementierung der Klassen und zusätzlich für den Anwender - aus Entwicklersicht - deren Nutzung. Mit dem weiteren Wachstum des Frameworks wachsen auch die Tests und werden zu Test Suites zusammengefügt. So werden Abhängigkeiten schneller aufgedeckt. Insbesondere, wenn bereits implementierte Klassen verändert werden müssen, sind die Tests gleich anzupassen, um die Korrektheit nachzuweisen. Ändert sich eine Klasse auf nicht vorhergesehene Weise in ihrem Verhalten, fällt dieses bei den Tests des Frameworks schnell auf und lässt sich leicht lokalisieren. Es ist offensichtlich, wie sich dies auf die Arbeit am Code mit größeren Teams sowie die langfristige Wartbarkeit auswirkt. Wenn mehrere Entwickler an den gleichen Quellen arbeiten oder Mitglieder im Team wechseln, erleichtern Unit Tests vielfach besser als eine ausschließliche Dokumentation auf Papier die Zusammenarbeit.

Auf die Verfahren soll an dieser Stelle nicht weiter eingegangen werden. Es finden sich hinreichend Beispiele in der Literatur und im Internet, zum Beispiel unter [FWJUNIT] und [FWTDD].

Einführung
Die Probleme beim Test Driven Development sind vielfach nicht technischer Natur, ohne an dieser Stelle die Schwierigkeiten von Tests in verteilten Umgebungen oder komplexen Systemen verschweigen zu wollen. Doch diese sollen an anderer Stelle noch behandelt werden. Schwieriger ist die Einführung auf der organisatorischen Ebene. Das Management sieht Unit Tests oft als zusätzlichen Aufwand an, viele Entwickler leider auch. Jüngere Teams, die über eine flache Struktur, einen direkten Draht zur Geschäftsleitung und keine festgetretenen Prozesse verfügen, tun sich mit der Nutzung von Unit Tests - und weiteren agilen Methoden - leichter.
Ist in größeren Entwicklungsabteilungen ein eigenes Software Quality Management vorhanden, tut sich ein weiteres Problem auf. Die Entwicklung macht es sich noch leichter und wälzt die Arbeit der Qualitätssicherung auf dieses Team ab. Dabei kann dieses die Arbeit vielfach nicht leisten. Es konzentriert sich auf formale Aspekte wie die Einhaltung von Standards und kann Werkzeuge einsetzen wie Lint oder Metriktools. Gleichzeitig unterstützt es das Entwicklerteam beim Nachweis der Anwenderforderungen oder bei Lasttests.

Was es nicht kann, ist, die Logik eines Moduls zu testen und insbesondere nicht, auf dieser Basis den Entwurfsprozess zu verbessern. Denn das Test Driven Design dient ja nicht nur der Sicherstellung der korrekten Implementierung mit den Unit Tests, sondern es beeinflusst, wie der Name es ausdrückt, auch das Softwaredesign. Das TDD zwingt den Entwickler dazu, sich während der Entwicklung einer Klasse und speziell noch vor der Implementierung Gedanken über die Nutzung der Klasse zu machen. Die kurzen Zyklen von nur wenigen Minuten sorgen dafür, dass nicht erst große Abhängigkeiten entstehen, in denen sich ein Problem dann nur schwer finden lässt. So wird das Design der Software bereits während der Entwicklung durch die Qualität beeinflusst. Das Software Quality Management kann an dieser Stelle die Entwickler nur beraten, schulen, die Durchführung überwachen und bei Problemen technischer Natur helfen, je nach Qualifikation.

Wie jedoch motiviert man Management und Entwickler zum Test Driven Development? Dies ist natürlich von der Position abhängig, in der man sich befindet. Als Team- oder Projektverantwortlicher ist man seinem Management gegenüber vielfach in kaufmännischer Rechtfertigungslage. Alles was Zeit und Geld kostet, ist per se schon einmal schlecht. Dafür verfügt man über die notwendigen organisatorischen Mittel. Als ein fachlich Verantwortlicher für Methoden oder als Software Quality Manager hat man hingegen schon eher Freiheiten, da die Einführung derartiger Maßnahmen zu den Aufgaben der Position gehört. Ist man als Entwickler im Team tätig, so bleibt vielfach nur die Überzeugungsarbeit.

Sich als Entwickler selbst vom Nutzen zu überzeugen, ist noch relativ einfach. Wichtig ist es hier, mit einem frischen Projekt zu starten. Es ist sehr schwer, nachträglich Tests zu integrieren, was dann auch nicht der Idee des Test Driven Designs entspricht. Maximales Ergebnis wäre der Nachweis der Funktionalität, aber das Design wurde nicht durch die Tests beeinflusst. Startet man hingegen direkt ab der ersten Implementierung mit dem TDD, erliegt man schnell der Begeisterung des unmittelbaren Feedbacks und der leichten und isolierten Testbarkeit der Klassen bzw. Module. Die Neigung, in einem Test gleich einen vertikalen Durchstich durch die Applikation zu prüfen, tritt gar nicht erst auf.

Überzeugungsarbeit
Der nächste Schritt liegt nun darin, im Management für eine Akzeptanz zur Einführung eines Test Driven Developments zu sorgen. Die Begeisterung am grünen Balken aus technologischer Sicht ist nicht hinreichend. Als Entwickler nach TDD-Manier schreiben Sie mehr Quellcode, was mehr Zeit und damit mehr Geld kostet. Doch dieses Vorurteil muss widerlegt werden. Ja, es wird mehr Code produziert, aber der hierdurch erzeugte Aufwand ist in Hinblick auf die Fertigstellung des Projektes - und noch viel mehr in Hinblick auf eine spätere Wartung - sehr gering.

Es ist eine bereits länger existierende Erkenntnis, dass die späte Behebung von Fehlern sehr viel mehr Aufwand erfordert, als wenn sie früh erfolgt. Je eher Fehler aufgedeckt werden, je robuster die Applikation also ist, um so geringer ist der Anteil der Kosten, der für die Korrekturen benötigt wird. Am Beispiel des Hausbaus ist dies leicht nachvollziehbar. Wird ein Fehler in der Elektroverkabelung festgestellt, bevor der Putz auf der Wand ist, ist ein Austausch betroffener Kabel leicht möglich. Ist der Putz aber erst trocken, die Wände tapeziert und gestrichen und man stellt nun das Problem fest, hilft nur eine aufwändige Reparatur oder ein Workaround. Letzterer rächt sich aber sicherlich später erneut.

Die folgenden Aufwandskurven zeigen den Unterschied deutlicher. Die grau unterlegte Fläche stellt die Fertigstellung der Anforderungen während des Projektverlaufes dar. Die durchgehende Kurve ist der Aufwand für die Umsetzung der Anforderungen, die gestrichelte Kurve zeigt den Aufwand für die Fehlersuche und -behebung. Man sieht, wie nach einer initialen Phase der Aufwand für die Entwicklung ansteigt, nach einiger Zeit jedoch wieder abflacht. Der Aufwand für die Fehlersuche nimmt während des gesamten Verlaufs stetig und kann gegen Projektende sogar den Aufwand für die Umsetzung von Anforderungen überschreiten.


Abb. 2: Aufwände in der regulären Entwicklung

Im Test-Driven Development werden in der Anfangszeit weniger Anforderungen umgesetzt, der Aufwand für Entwicklung und Tests ist linear. Erst gegen Ende kann eine Sättigung eintreten, ein ähnlicher Effekt wie bei regulären Projekten, da der ideale Zustand vielfach nicht erreicht wird.


Abb. 3: Aufwände im Test Diven Development

Der schnelle Anstieg der umgesetzten Anforderung mag in der traditionellen Arbeitsweise reizvoll sein. Dies ist jedoch nur auf den ersten Blick korrekt. Eine Addition der Aufwände für Entwicklung und Tests zeigt, dass diese schnell die Summe der Aufwände im Test-Driven Development übersteigen kann. Wird hingegen versucht, den gleichen, typischerweise engen, Kostenrahmen einzuhalten, wird selten an den umgesetzten Anforderungen gespart. Also werden die Tests so weit es geht gekürzt, ein Qualitätsverlust wird riskiert. Eine Geschäftsleitung muss sich fragen, ob sie sich dies erlauben kann.

Ist Qualität hingegen ein Merkmal der Entwicklung, dann muss die Geschäftsleitung ihren Mitarbeitern auch den Raum hierfür geben. Dies bedeutet, nicht sofort auf frühe umfangreiche Ergebnisse zu drängen, sondern dem Team die Zeit für den Aufbau des Test-Driven Developments - und weiterer Qualitätssicherungsmaßnahmen - zu geben. Hierzu gehören die Ausarbeitung von Vorgaben, die Durchführung von Schulungen und Coachings und der Verzicht auf laufend neue Termine, die dem Team die Arbeit erschweren.

Ideale
Ist das Management erst überzeugt, muss auch das Team mitziehen. Es muss ebenfalls davon überzeugt werden, dass die parallele, intensive Implementierung von Unit Tests in der Summe nicht mehr Zeit kostet. Sie ermöglicht auf Dauer eine gleichmäßigere Auslastung und ein entspannteres Entwickeln. Die stressige Fehlersuche gegen Ende des Projekts wird geringer. Und es muss überzeugt werden, dass sich die Qualität der Software mit dem Test Driven Development verbessert. Wenn nicht schon die gleichmäßigere Entwicklung, dann sollte zumindest diese Qualität ein Ideal der Entwickler ansprechen. Sie muss dem Team als wichtiger Bestandteil der eigenen Arbeit vermittelt werden, den Stolz ansprechen.

Schwächen in einem Projekt ziehen weitere Schwächen nach sich. Dies findet sich unter dem Namen Broken Window Theory in der Literatur wieder. Diese Theorie besagt, dass kleine Schäden, wie zerbrochene Scheiben oder Graffiti, weitere Schäden nach sich ziehen, wenn sie nicht behoben werden. Die Aufmerksamkeit sinkt, die Vorsicht ebenso, Beschädigungen werden billigend in Kauf genommen und ein Abwärtsstrudel tritt ein. In großen Städten wird daher versucht, diese kleinen Schäden frühzeitig zu beheben, um so Verwahrlosung zu vermeiden.
Was bedeutet dies für die Softwareentwicklung? Als Verantwortlicher muss man dem Team vermitteln, bereits kleine Probleme nicht zu akzeptieren. Dies trifft auf viele Bereiche zu, nicht nur auf Unit Tests. Aber mit diesen fängt es für den Entwickler an. Sie helfen ihm, die Qualität seiner Software im Test sicherzustellen und zu dokumentieren.

Dass dies funktioniert, wird leider vielfach auch von den Entwicklern bezweifelt. Also muss neben einer reinen Anordnung auch Überzeugungsarbeit geleistet werden. Dafür bieten sich aus eigener Erfahrung wiederholte Schulungen an, die nicht nur die Grundlagen vermitteln, sondern am realen Code dessen Korrektheit nachweisen oder - was besser ist - Fehler aufdecken. Diese Übungen am lebenden Objekt verdeutlichen den Vorteil, den man mit dem Test-Driven Development gewinnt. Gerade eigene Fehler, die zuvor nicht entdeckt wurden, deren Fehlverhalten unter definierten Laufbedingungen aber nachweisbar ist, erschrecken.

Fazit
Die Idee der Unit Tests und deren Einsatz als Steuerungsmittel für die Entwicklung ist nahezu offensichtlich. Das Test Driven Development funktioniert jedoch nur, wenn es durchgängig von Anfang an und von allen beteiligten Entwicklern eingesetzt wird. Unterstützt die Geschäftsleitung dieses Verfahren, so erhält sie im Gegenzug dafür ein hochwertiges Produkt, allerdings auf Kosten früherer Ergebnisse. Langfristig wird sich dieses jedoch rechnen.

Nun ist der Einsatz des Test Driven Developments auch technisch leider nicht immer so trivial, wie oben in der Einführung. Welche Stolpersteine es gibt und mit welchen Werkzeugen und Erweiterungen diese umgangen werden können, zeigen wir in der nächsten Ausgabe.
Olga Kolov arbeitet als Software-Qualitäts-Beauftragte bei der BOSS AG in Bremen. Frank Müller arbeitet als Systemspezialist bei der THALES NAVAL GmbH in Wilhelmshaven.

Links
[VCARD] www.faqs.org/rfcs/rfc2426.html
[FWJUNIT] www.frankwestphal.de/UnitTestingmitJUnit.html
[FWTDD] www.frankwestphal.de/TestgetriebeneEntwicklung.html


    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.

zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen

Software & Support Verlag GmbH