Schlagwort

testgetriebeneentwicklung

23.6.2012

Mein Buch zum Download

Wie die Zeit vergeht… Testgetriebene Entwicklung mit JUnit und FIT ist nunmehr sieben Jahre alt. Ich habe damals versucht, die TDD-Techniken so zeitlos wie möglich zu beschreiben. Doch natürlich haben sich seitdem eine Reihe neuer Werkzeuge und Ideen hinzugesellt. Im Ruby-Umfeld beispielsweise sind BDD und rSpec mehr oder weniger die Norm. Open Source ohne Unit Tests ist selten, wenn nicht undenkbar geworden. Wie zu erwarten war, sind jedoch nicht alle unsere Ideen kleben geblieben. Aber ein paar schon. Vielleicht die wichtigsten.

Auch sieben Jahre später bin ich mit meinem Text immer noch überraschend einverstanden. Einzig wünschte ich mir, ich hätte den Kontext der Techniken im Buch damals besser herausgestellt. Test-Driven Development sind Best Practices. Best Practices existieren in einem wohl definierten Kontext. Der Untertitel "Wie Software änderbar bleibt" und insbesondere das letzte Buchkapitel "Änderbare Software" geben einen Hinweis, aber der meiner Meinung nach wichtigste Trade-off bleibt, langlebige Software zu entwickeln. In Open Source ist diese Voraussetzung stets gegeben. Im Startup dagegen kann der Kontext ein gänzlich anderer sein. Bei Rivva sind meine Tests zum Beispiel viel mehr risikogetrieben. Und natürlich hätte das Buch für eine andere Programmiersprache auch eine andere Gestalt angenommen. In Ruby entwickle ich fast ausschließlich REPL-getrieben.

Buchcover

Das Buch ist mittlerweile vergriffen und nicht mehr im Druck. Deshalb freue ich mich, dass der dpunkt.verlag mir jetzt sein Einverständnis gegeben hat, das Buch als kostenloses eBook veröffentlichen zu können. Mein besonderer Dank dafür gilt meiner Lektorin Christa Preisendanz.

Download: Testgetriebene Entwicklung mit JUnit & FIT (PDF)

Happy 100th Birthday, Mr. Alan Turing (1912-1954).

17.11.2005

Testgetriebene Entwicklung
mit JUnit und FIT

Buchcover

Frank Westphal
Testgetriebene Entwicklung
mit JUnit & FIT:

Wie Software änderbar bleibt

dpunkt.verlag
346 Seiten
November 2005
ISBN 3-89864-220-8

Bei Amazon ansehen

Beschreibung

Testgetriebene Entwicklung geht von einem fehlschlagenden Test aus. Software wird in kleinen sicheren Schritten entwickelt, die abwechselnd darauf abzielen, eine neue Anforderung zu implementieren (den fehlschlagenden Test also zu erfüllen) und das Design zu verbessern (und dabei weiterhin alle Tests zu bestehen).

  • Wenn frühes und häufiges Testen wichtig ist, warum schreiben wir nicht für jedes neue Feature zuerst einen automatisierten Test? So können wir während der Entwicklung jederzeit unsere Tests ausführen und lernen, ob unser Code wie gewünscht funktioniert.
  • Wenn Design wichtig ist, warum investieren wir dann nicht Tag für Tag darin? So können wir dafür sorgen, dass es möglichst einfach bleibt und nicht mit der Zeit zunehmend degeneriert.
  • Wenn Anforderungsdefinition wichtig ist, warum ermöglichen wir unseren Kunden dann nicht, in einem ausführbaren Anforderungsdokument Testfälle für konkrete Anwendungsbeispiele zu spezifizieren? So können wir dokumentieren, welche Funktionalität tatsächlich gefordert ist, und anschließend verifizieren, ob die Erwartungen des Kunden erfüllt werden.

Das Buch führt mit praktischen Beispielen in die Testgetriebene Entwicklung mit den Open-Source-Werkzeugen JUnit und FIT ein.

Frank Westphal has turned his extensive experience in using and teaching developer testing with JUnit and FIT into a thorough and usable guide for programmers and testers, including the first published guide to JUnit 4. What I was most struck by in reading this book was the combination of philosophical background and detailed, practical advice. – Kent Beck, Three Rivers Institute

Amazon-Rezensionen

ganz aus dem häuschen – Thomas Steinbach

Warum sind wir darauf nicht schon eher gekommen? – H. Mausolf

Grandios !! – Bernd Will

Lockere Lektüre für flotte Erfolge – Gernot Starke

Motivation für die Praxis – Frankmartin Wiethüchter

Seit langem kein Buch mehr so schnell durchgearbeitet! – Ulrich Storck

Rundum gelungen – Jens Uwe Pipka

Übers Entwickeln und Entwerfen - auf testgetriebene Art – Christoph Steindl

Ideal für professionelle Softwareentwickler – Dierk König

Testen als Mittel zum Zweck – Stefan Roock

Buchbesprechungen

Inhaltsverzeichnis

Geleitwort von Johannes Link (PDF)

Kapitel 1: Einleitung (PDF)

  • Was ist Testgetriebene Entwicklung?
  • Warum Testgetriebene Entwicklung?
  • Über dieses Buch
  • Merci beaucoup

Kapitel 2: Testgetriebene Entwicklung, über die Schulter geschaut

  • Eine Programmierepisode
  • Testgetriebenes Programmieren
  • Möglichst einfaches Design
  • Ein wenig Testen, ein wenig Programmieren ...
  • Evolutionäres Design
  • Natürlicher Abschluss einer Programmierepisode
  • Refactoring
  • Abschließende Reflexion
  • Häufige Integration
  • Rückblende
  • "Aus dem Bauch" von Sabine Embacher

Kapitel 3: Unit Tests mit JUnit (PDF)

  • Download und Installation
  • Ein erstes Beispiel
  • Anatomie eines Testfalls
  • Test-First
  • JUnit in Eclipse
  • Das JUnit-Framework von innen
  • »Assert«
  • »AssertionFailedError«
  • »TestCase«
  • Lebenszyklus eines Testfalls
  • »TestSuite«
  • »TestRunner«
  • Zwei Methoden, die das Testen vereinfachen
  • Testen von Exceptions
  • Unerwartete Exceptions
  • "Woran erkennt man, dass etwas testgetrieben entwickelt wurde?" von Johannes Link
  • JUnit 4

Kapitel 4: Testgetriebene Programmierung

  • Die erste Direktive
  • Der Testgetriebene Entwicklungszyklus
  • Die Programmierzüge
  • Beginn einer Testepisode
  • Ein einfacher Testplan
  • Erst ein neuer Test ...
  • ... dann den Test fehlschlagen sehen
  • ... schließlich den Test erfüllen
  • Zusammenspiel von Test- und Programmcode
  • Ausnahmebehandlung
  • Ein unerwarteter Erfolg
  • Ein unerwarteter Fehlschlag
  • "Rückschritt für den Fortschritt" von Tammo Freese
  • Vorprogrammierte Schwierigkeiten
  • "Zwei offene Tests sind einer zu viel" von Tammo Freese
  • Kleine Schritte gehen
  • "Halten Sie Ihre Füße trocken" von Michael Feathers

Kapitel 5: Refactoring

  • Die zweite Direktive
  • Die Refactoringzüge
  • Von übel riechendem Code ...
  • ... über den Refactoringkatalog
  • ... zur Einfachen Form
  • Überlegungen zur Refactoringroute
  • Substitution einer Implementierung
  • Evolution einer Schnittstelle
  • "Coding Standards bewusst verletzen" von Tammo Freese
  • Teilen von Klassen
  • Verschieben von Tests
  • Abstraktion statt Duplikation
  • Die letzte Durchsicht
  • Ist Design tot?
  • "Durch zerbrochene Fenster dringen Gerüche ein" von Dave Thomas & Andy Hunt
  • Richtungswechsel ...
  • ... und der wegweisende Test
  • Fake it ('til you make it)
  • Vom Bekannten zum Unbekannten
  • Retrospektive
  • Tour de Design évolutionnaire
  • Durchbrüche erleben

Kapitel 6: Häufige Integration

  • Die dritte Direktive
  • Die Integrationszüge
  • Änderungen mehrmals täglich zusammenführen ...
  • "Taxi implements Throwable" von Olaf Kock
  • ... das System von Grund auf neu bauen
  • ... und ausliefern
  • Versionsverwaltung (mit CVS oder Subversion)
  • Build-Skript mit Ant
  • Build-Prozess-Tuning
  • Integrationsserver mit CruiseControl
  • Aufbau einer Staging-Umgebung
  • Teamübergreifende Integration
  • Gesund bleiben
  • "Eine Geschichte über die Häufige Integration" von Lasse Koskela

Kapitel 7: Testfälle schreiben, von A bis Z

  • Aufbau von Testfällen
  • Benennung von Testfällen
  • Buchführung auf dem Notizblock
  • Der erste Testfall
  • Der nächste Testfall
  • Erinnerungstests
  • Ergebnisse im Test festschreiben, nicht berechnen
  • Erst die Zusicherung schreiben
  • Features testen, nicht Methoden
  • Finden von Testfällen
  • Generierung von Testdaten
  • Implementierungsunabhängige Tests
  • Kostspielige Setups
  • Lange Assert-Ketten oder mehrere Testfälle?
  • Lerntests
  • Minimale Fixture!
  • "Einfache Tests - einfaches Design" von Dierk König
  • Negativtests
  • Organisation von Testfällen
  • Orthogonale Testfälle
  • Parameterisierbare Testfälle
  • Qualität der Testsuite
  • Refactoring von Testcode
  • Reihenfolgeunabhängigkeit der Tests
  • Selbsterklärende Testfälle
  • String-Parameter von Zusicherungen
  • Szenarientests
  • Testexemplare
  • Testsprachen
  • Umgang mit Defekten
  • "Eine Frage der (Test-)Kultur" von Christian Junghans & Olaf Kock
  • Umgang mit externem Code
  • Was wird getestet? Was nicht?
  • Zufälle und Zeitabhängigkeiten
  • "Der zeitlose Weg des Testens" von Lasse Koskela

Kapitel 8: Isoliertes Testen, durch Stubs und Mocks

  • Verflixte Abhängigkeiten!
  • Was ist die Unit im Unit Test?
  • Mikrointegrationstest versus strikter Unit Test
  • Vertrauenswürdige Komponenten
  • "Vertrauen - und Tests" von Bastiaan Harmsen
  • Austauschbarkeit von Objekten
  • Stub-Objekte
  • Größere Unabhängigkeit
  • Testen durch Indirektion
  • Stub-Variationen
  • Testen von Mittelsmännern
  • Self-Shunt
  • Testen von innen
  • Möglichst frühzeitiger Fehlschlag
  • Erwartungen entwickeln
  • Gebrauchsfertige Erwartungsklassen
  • Testen von Protokollen
  • Mock-Objekte
  • Wann verwende ich welches Testmuster?
  • "Wo Mock-Objekte herkommen" von Tim Mackinnon & Ivan Moore & Steve Freeman
  • Crashtest-Dummies
  • Dynamische Mocks mit EasyMock
  • Stubs via Record/Replay
  • Überspezifizierte Tests
  • Überstrapazierte Mocks
  • Systemgrenzen im Test
  • "Mock-Objekte machen glücklich" von Moritz Petersen

Kapitel 9: Entwicklung mit Mock-Objekten

  • Tell, don't ask
  • Von außen nach innen
  • Wer verifiziert wen?
  • Schnittstellen finden auf natürlichem Weg
  • Komponierte Methoden
  • Vom Mock lernen für die Implementierung
  • Viele schmale Schnittstellen
  • Kleine fokussierte Klassen
  • Tell und Ask unterscheiden
  • nereimmargorP sträwkcüR
  • Schüchterner Code und das Gesetz von Demeter
  • Fassaden und Mediatoren als Abstraktionsebene
  • Rekonstruktion
  • "Meister, ..." von Dierk König

Kapitel 10: Akzeptanztests mit FIT (Framework for Integrated Test)

  • Von einer ausführbaren Spezifikation ...
  • Download Now
  • Schritt für Schritt für Schritt
  • ... zum ausführbaren Anforderungsdokument
  • Die drei Basis-Fixtures
  • »ActionFixture«
  • Richtung peilen, Fortschritt erzielen
  • Fixture wachsen lassen, dann Struktur extrahieren
  • Nichts als Fassade
  • Die Fixture als zusätzlicher Klient
  • Aktion: Neue Aktion
  • »ColumnFixture«
  • Fixture-Interkommunikation
  • Negativbeispiele
  • Transformation: Action -> Column
  • »RowFixture«
  • Einfacher Schlüssel
  • Mehrfacher Schlüssel
  • Abfragemethoden einspannen
  • »Summary«
  • "Warum drei Arten von Fixtures?" von Ward Cunningham
  • »ExampleTests«
  • »AllFiles«
  • Setup- und Teardown-Fixtures
  • Das FIT-Framework von innen
  • »FileRunner«
  • »Parse«
  • »Fixture«
  • Annotationsmöglichkeiten in Dokumenten
  • »TypeAdapter«
  • »ScientificDouble«
  • Domänenspezifische Grammatiken
  • »ArrayAdapter«
  • »PrimitiveFixture«
  • Domänenspezifische Fixtures
  • Anschluss finden
  • Stichproben reiner Geschäftslogik
  • Integrationstests gegen Fassaden und Services
  • "Survival of the FIT Test" von Steffen Künzel & Tammo Freese
  • Oberflächentests
  • Kundenfreundliche Namen
  • FitNesse
  • FitLibrary
  • Akzeptanztesten aus Projektsicht

Kapitel 11: Änderbare Software

  • "Harte Prozesse führen zu harten Produkten" von Dierk König
  • Konstantes Entwicklungstempo
  • "Alten Code testgetrieben weiterentwickeln" von Juan Altmayer Pizzorno & Robert Wenner
  • "Die Latte liegt jetzt höher" von Michael Feathers
  • Kurze Zykluszeiten
  • Neue Geschäftsmodelle
  • "Bug-Trap-Linien" von Michael Hill

Behandelte Werkzeuge

Quellcode

Die Beispiele aus den Kapiteln 2, 3, 4, 5, 6, 7, 8 können Sie sich auch hier herunterladen.

6.1.2002

Testgetriebene Entwicklung

Eingehen auf Veränderung

Wie würden wir programmieren, wenn wir tatsächlich nicht wüssten, wohin unser Kunde die Entwicklung steuern wird? Wie müssten wir programmieren, wenn wir späte Anforderungsänderungen als Chance oder Wettbewerbsvorteil unserer Kunden und nicht als Risiko der Softwareentwicklung auffassen wollten? In dieser Situation würden wir unser Design inkrementell erstellen und in kleinen Schritten anpassen, während neue Anforderungen auf uns zukommen. Unser Designprozess wäre dem organischen Anpassungs- und Wachstumsprozess ganz ähnlich. Design wäre eine ständige Aktivität, die wir Minute um Minute wahrnehmen würden.

Interessanterweise beschreibt dies zu einem großen Teil unsere tagtägliche Situation in der Softwareentwicklung in heutigen schnelllebigen Märkten, wo viele Projekte in der Tat explorativer Natur sind. Ein Design lässt sich in diesen Fällen nicht vorab planen, sondern kann sich erst mit wachsendem Verständnis der Anforderungen entwickeln. Noch mehr ähnelt die Situation jedoch unseren Aufwänden in der Pflege, das heißt Weiterentwicklung, vorhandener Software. Wie würde sich demnach unsere Perspektive ändern, wenn wir davon ausgehen, dass wir schon die Softwareentwicklung so betreiben können wie Wartungsprojekte? Was müssten wir leisten, damit wir inkrementell und unbegrenzt lange immer neue Funktionen in unsere Programme integrieren können?

Um diesen Gedankensprung in die Realität zu retten, benötigen wir eine qualitätsbewusste Alternative zum vorab geplanten Design. Wir suchen nach einer Strategie, mit der wir verhindern, dass die Codequalität mit wachsender Programmgröße in Mitleidenschaft gezogen wird. Eine vielversprechende Wiederentdeckung ist die testgetriebene Entwicklung, oft auch testgetriebene Programmierung oder gar testgetriebenes Design genannt, wenn auch andere Autoren mit dem Namen zum Teil bestimmte Schwerpunkte der Technik ansprechen. Im Englischen spricht man von Test-Driven Development oder Test-First-Design.

Just-in-time-Design

Testgetriebenes Programmieren ist eine Just-in-time-Designtechnik, um auf Just-in-time-Anforderungen einzugehen. Wir schreiben dabei Unit Tests, noch bevor wir den zu testenden Programmcode schreiben. Idealerweise wird jede funktionale Programmänderung zuvor durch das Schreiben eines weiteren Tests motiviert. Wir entwerfen diesen Test so, dass er zunächst fehlschlägt, weil das Programm die gewünschte Funktionalität noch nicht besitzt. Erst anschließend schreiben wir den Code, der den Test zum Laufen bringt. Auf diese Weise wird die gesamte Programmentwicklung inkrementell durch das unmittelbare Feedback konkreter Tests angetrieben.

Sie fragen sich vielleicht, was wir denn testen sollen, wenn wir noch überhaupt keinen Code geschrieben haben? Doch diese Frage lässt sich umdrehen. Woher wissen wir denn, was wir programmieren sollen, wenn wir noch nicht wissen, was denn überhaupt erforderlich ist? Zuerst die Tests zu schreiben, ist eine Möglichkeit, um herauszufinden, was wir programmieren müssen und was nicht, und wie wir auch sicherstellen können, dass wir tatsächlich programmieren werden, was wir programmieren wollten. Analysieren Sie dazu, wie die erforderliche Klasse funktionieren und sich verhalten sollte. Dokumentieren Sie dann Ihr Verständnis in einem ausführbaren Unit Test. Stellen Sie sich dabei vor, der zu testende Code wäre einfach schon realisiert. Entwerfen Sie die Tests aus der Verwendungsperspektive, so, wie Sie sich die Schnittstelle der zu testenden Klasse wünschen, und verdrängen Sie die Gedanken an die Implementierung für einen kurzen Moment.

Wir sprechen von einer Designtechnik, weil das frühe Testen eine ganze Reihe positiver Auswirkungen auf das resultierende Design hat. Ein Vorteil dieser Technik wird hier schon unmittelbar deutlich. Wenn Sie Ihre Tests zuerst schreiben, wird Ihr Code auch testbar sein. Den Test haben Sie ja schließlich gerade schon geschrieben. Code dagegen, der nicht unter Gesichtspunkten der einfachen Testbarkeit entworfen wurde, läßt sich auch nachträglich meist nur noch schwer testen. Tatsächlich handelt es sich beim Test-First-Ansatz noch stärker um eine Designstrategie als eine Teststrategie. Ich werde zum Ende des Artikels noch auf die Langzeiteffekte dieser Technik zurückkommen.

Testgetriebenes Design ist der linke Fuß unserer evolutionären Designstrategie. Der rechte Fuß ist das Refactoring. Um den inkrementellen Designansatz zu gewährleisten, ist unbedingt eine höchstsäuberlich strukturierte Codebasis erforderlich. Wir müssen sicherstellen, dass wir uns durch kurzfristig getroffene Entwurfsentscheidungen nicht zunehmend in die Ecke malen. Um die innere Qualität des Programms dabei nicht zu opfern, sondern über sehr weite Zeiträume aufrechtzuerhalten, müssen wir das Design durch fortlaufende überarbeitung in Stand halten. Wir verbessern die Struktur des Code durch unzählige kleine Refactorings und führen nach jedem Schritt alle gesammelten Tests aus, um sicherzugehen, dass wir nicht ungewollt das Verhalten des Programms verändert haben. Miteinander kombiniert ermöglichen testgetriebenes Programmieren und anschließendes Refactoring, dass wir entwickeln können, was wir brauchen, wenn wir es brauchen.

Iteratives Testen und Programmieren

Die testgetriebene Entwicklung zwingt uns in einen stark iterativen Prozess, in dem jeder Schritt die Möglichkeit zum Lernen bietet. Jeder Test, den wir schreiben und erfüllen, kann uns konkretes Feedback darüber liefern, welchen Test wir als nächstes schreiben könnten oder sollten. Je kleiner wir dabei unsere Programmieretappen wählen, desto schneller können wir lernen, wohin uns der Weg führt.

Der Zyklus des Testens und Programmierens schaut so aus:

  1. Wir entwerfen einen Test, der zunächst fehlschlagen sollte.
  2. Wir schreiben gerade soviel Code, dass der Test tatsächlich fehlschlägt.
  3. Wir schreiben gerade soviel Code, dass tatsächlich alle Tests durchlaufen.

Diesen Prozess wiederholen wir, solange uns weitere Tests einfallen, die unter Umständen fehlschlagen könnten, bis der Code schließlich seine durch die Tests spezifizierten Anforderungen erfüllt. Anschließend refaktorisieren wir den Code in die einfachste Form, oft jedoch schon früher, um "Platz" für weiteren Code zu schaffen und neue Tests dadurch einfacher erfüllen zu können. Dazu später mehr.

Machen wir mal ein kleines Beispiel.

Angenommen, wir möchten uns die Namen der Kunden eines Videoladens merken. Um diese Funktionalität zu erreichen, schreiben wir im ersten Schritt einen neuen Test. Dieser Test schlägt zunächst fehl, weil sich ein Customer bisher noch keinen Namen merken kann. Die Klasse besitzt weder einen geeigneten Konstruktor, noch eine passende Accessor-Funktion, um den Namen wieder abzufragen. Um die nötigen änderungen einzufügen, definieren wir den folgenden Testfall.


public class CustomerTest...
  public void testCustomerName() {
    Customer customer = new Customer("Bent Keck");
    assertEquals("Bent Keck", customer.getName());
  }
}

Damit dieser Testfall überhaupt durchkompiliert, besteht der zweite Schritt darin, wenigstens die minimalen Schnittstellenrümpfe in unsere Customer Klasse einzufügen. Das überraschende ist jetzt, dass wir die Klasse nicht sofort ausimplementieren, sondern den Testfall erst einmal mutwillig fehlschlagen lassen. Warum wir die Klasse leer lassen, wird Ihnen klarer werden, wenn Sie auf die Farbübergänge unseres JUnit-Balkens Acht geben. Noch ist er schön grün...


public class Customer...
  public Customer(String name) {
  }

  public String getName() {
    return null;
  }
}

Unser Code sollte jetzt fehlerfrei durchkompilieren. Um den zweiten Schritt abzuschließen, machen wir den Test. Unser Testfall sollte fehlschlagen und so tut er es dann auch. Der abgedruckte rote Balken signalisiert, wo immer er auftaucht, dass wir die Tests zu diesem Zeitpunkt ausführen mit dem protokollierten Fehler als Resultat.


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<Bent Keck> but was:<null>
at CustomerTest.testCustomerName(CustomerTest.java:14)

Im dritten Schritt schreiben wir jetzt endlich den Code, der unseren Testfall erfüllt. Das überraschende hier wiederum ist, dass wir nur gerade soviel Programmcode schreiben, wie unbedingt nötig ist, um alle unsere gesammelten Testfälle zu erfüllen. Der grüne Balken drückt aus, dass der abgedruckte Code unsere Tests passiert.


JUnit-IkonGrüner JUnit-Balken

public class Customer...
  public String getName() {
    return "Bent Keck";
  }
}

Grüner Balken! Fertig! Nach nur 10 Sekunden, ist das nicht was?

Rollen Sie jetzt bitte nicht mit den Augen, wir gehen ja noch ein Stückchen weiter. Als Nächstes extrahieren wir den Namen in ein eigenes Feld. Moderne IDEs unterstützen dieses Refactoring auf Knopfdruck.


JUnit-IkonGrüner JUnit-Balken

public class Customer...
  private String name = "Bent Keck";

  public String getName() {
    return name;
  }
}

Wieder ein grüner Balken! Alle Tests laufen! Was machen wir als Nächstes?

Hmm, den Namen des Kunden im Konstruktor im Feld merken?


JUnit-IkonGrüner JUnit-Balken

public class Customer...
  private String name;

  public Customer(String name) {
    this.name = name;
  }
}

Erneut bleibt der Balken grün. Sind wir fertig?

Ich würde sagen, unsere kleine Aufgabe ist abgeschlossen, bis zu dem Zeitpunkt, dass wir einen weiteren Test schreiben, der den JUnit-Balken wieder rot färbt.

Was haben wir erreicht? Wir haben eine kleine neue Funktion in unser bestehendes Programm integriert. Wir haben zunächst die einfachste Lösung programmiert, die unseren Test besteht, und haben dann die Struktur des Code umgeformt, nachdem die Tests schon liefen. Wir sind dabei in ganz kleinen Schritten vorgegangen und haben nach jedem Schritt die Tests ausgeführt, um sicherzugehen, dass wir keine bestehende Programmfunktionalität beeinträchtigen. Bemerkenswert ist dabei, dass wir uns solange sicher wiegen können, solange wir genau wissen, aufgrund welcher Zeilen Code sich der Testbalken von grün auf rot oder zurück von rot auf grün verfärbt. Es ist dieser stetige Wechsel der beiden Farben, der uns minütlich über unseren letzten Schritt informiert.

Tipp
Arbeiten Sie von einem grünen Testbalken aus immer zunächst auf einen roten Balken hin. Arbeiten Sie dann vom roten Balken aus auf dem kürzesten Weg zurück auf den grünen JUnit-Balken. Beim grünen Balken angekommen nehmen Sie die Möglichkeit zum Refactoring wahr.

Mit diesem Vorgehen verzahnen wir Analyse, Design, Implementierung und Test zeitnah in einem iterativ-inkrementellen Prozess. Wir analysieren zuerst, was genau eigentlich von uns verlangt wird und was wir dafür zu tun haben. In einem ausführbaren Test spezifizieren wir, was wir dazu benötigen und was wir als Erfolgskriterium betrachten. Dann implementieren wir die neue Funktion. Abschließend testen wir, ob wir tatsächlich erreicht haben, was wir ursprünglich vor Augen hatten. Sie können sich sicher vorstellen, dass Sie viel stressfreier arbeiten, wenn jede kleinste Programmänderung, und mag sie noch so mutig gewesen sein, jederzeit binnen Sekunden durch einen automatischen Test auf erwünschte Wirksamkeit und unerwünschte Nebeneffekte geprüft werden kann.

Wir verfolgen mit dieser Arbeitsweise die Idee, das konkrete Feedback, das uns nur geschriebener Code liefern kann, unmittelbar als Basis für unseren nächsten Programmierzug verwenden zu können. Erfolgreich angewendet mündet der Fluss testgetriebenen Programmierens in eine Serie kleiner Erfolge positiven Feedbacks. Sie testen ein wenig, lassen sich von einem fehlschlagenden Test demonstrieren, wo änderungen notwendig sind, programmieren dort ein wenig, testen erneut und feiern einen kleinen Erfolg. High Five und dann von vorne.

Der Trick hier ist, eine Idee des kleinstmöglichen Inkrementschritts zu haben und diesen möglichst schnell und möglichst einfach in einer kurzen Programmieriteration in ausführbaren Code zu verwandeln. Das Prinzip dabei ist, sich auf dem Weg durch unmittelbares Feedback abzusichern.

Eine Testepisode

Ich möchte das Prinzip des unmittelbaren Feedbacks an einer kleinen Geschichte verdeutlichen. Starten wir dazu mit einer neuen Testepisode. Unsere Aufgabe sei es, eine neue Preiskategorieklasse für Filme mit regulärem Preis zu entwickeln. Nennen wir sie RegularPrice.

Wenn ich mitgezählt hätte, wie viele Male ich das Klassengerüst eines JUnit-Testfalls schon eingetippt habe, wäre es mir vermutlich früher langweilig geworden. Eines Tages wurde es mir dann wirklich leid und die Klassengenerierung automatisiert. Gute Erfahrungen konnte ich mit einer Klassenschablone wie der folgenden machen. Die meisten Entwicklungsumgebungen bieten die Codegenerierung derlei Klassengerüste für verschiedenste Zwecke an.


import junit.framework.*;

public class RegularPriceTest extends TestCase {

  public RegularPriceTest(String name) {
    super(name);
  }
    
  public void testFirst() {
    fail("write a test first");
  }
}

Der Clou an dieser leeren Klasse sind zwei Dinge. Zum einen erkennt JUnit eine Klasse erst als Testfallklasse an, wenn sie mindestens eine test... Methode enthält. Zum anderen hätte ich vielleicht auch mal mitzählen sollen, wie oft ich eine neue Testklasse angelegt und vergessen habe, diese in die passende Testsuite einzuhängen.

Gerade wenn Sie im Team entwickeln, bemerken Sie den Fortschritt in der Anzahl von Testfällen in Ihrem Projekt nicht mehr so einfach. Sie finden sich dann unter Umständen in der Situation wieder, dass Sie testen, was das Zeug hält, bis Ihnen irgendwann schlagartig bewusst wird, dass der Testbalken grün bleibt, was immer Sie auch für einen Unsinn hineintippen.

Nachdem ich mich auf diese Weise mehrmals zum Dummkopf gemacht hatte, dachte ich darüber nach, mir das Feedbackprinzip zunutze zu machen. Schließlich kam mir die Einsicht, jede neue Testfallklasse mittels eines roten Balkens einzuläuten.

Und so erwarte ich heute die Erinnerungsstütze durch den roten Balken und zwinge mich damit dazu, die neuen Tests in meine aktive Suite einzubinden.


public class AllTests...
  public static Test suite() {
    TestSuite suite = new TestSuite();
    suite.addTestSuite(CustomerTest.class);
    suite.addTestSuite(EuroTest.class);
    suite.addTestSuite(MovieTest.class);
    suite.addTestSuite(RegularPriceTest.class);
    return suite;
  }
}

Ein kurzer Test bestätigt unser Handeln.


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError: write a test first
at RegularPriceTest.testFirst(RegularPriceTest.java:10)

Eine Lösung, die das Problem von der anderen Seite packt, ist eine AllTests Testsuiteklasse, die automatisch alle Testfälle ausführt, die sich im Klassenpfad finden lassen. Sie finden eine solche Klasse neben vielen anderen JUnit-Erweiterungen in der Dateiablage der JUnit-Yahoogroup unter http://groups.yahoo.com/group/junit/files/.

Kommen wir jetzt auf unser Testproblem zurück...

1. Wir entwerfen einen Test, der zunächst fehlschlagen sollte

Eine bewährte Idee ist, dass wir uns zu Beginn einer Programmierepisode zunächst die notwendigen Testfälle überlegen und sie auf einer Karteikarte notieren. Das ist Analyse.

In der Regel werden Sie jedoch nicht alle Anforderungen im Kopf haben, sondern jemanden zu Rate ziehen müssen, der sich mit der Fachlichkeit besser auskennt. Schnappen Sie sich dazu einfach einen Kunden und verhandeln Sie mit ihm die genauen Anforderungen Ihrer Aufgabe. Wenn Sie sich noch näher beraten müssen, können Sie zu einer Wandtafel wandern und das Problem dort im kleineren oder grösseren Kreis analysieren und dafür Lösungen entwerfen.

Unsere Aufgabe ist eine neue Preiskategorie. Filme kosten regulär die ersten drei entliehenen Tage insgesamt 1.50 Euro. Für jeden weiteren Ausleihtag kommen weitere 1.50 Euro hinzu. Wir malen kurz ein Bild und benennen unsere ersten Testfälle.

Preiskurve

Da erschöpfendes Testen unmöglich ist, ist eine nützliche Strategie, entlang von äquivalenzklassen und ihren Grenzbedingungen zu testen. äquivalente Tests testen in der Regel das gleiche Verhalten. Das heißt, dass für einen gegebenen Programmierfehler entweder alle Tests innerhalb einer äquivalenzklasse gemeinsam fehlschlagen oder alle Tests gemeinsam durchlaufen. Sehr häufig können wir durch diese Betrachtung die interessanten Tests finden und viele Fälle bereits ausklammern, die wir uns unter Umständen sparen können, weil sie uns keine neuen Informationen liefern.

Unsinnige Eingaben wie Null und negative Zahlen, erfahren wir auf Rückfrage von unserem Kunden, liegen außerhalb unseres aktuellen Problems. Die entsprechenden Vorbedingungen für robuste Eingaben wird demnach eine andere Klasse treffen.

Die interessanten Testfälle liegen für uns bei einem, drei, vier und fünf Ausleihtagen. Sie selbst mögen manchmal ganz andere Fälle testen wollen, als Ihr Programmierpartner vorschlägt. Das ist kein Problem, solange die Tests das Design antreiben.

In vielen Fällen werden wir unsere initial gewählte Liste von Testfällen ohnehin während der Entwicklung anpassen. Neue Testfälle mögen hinzukommen und geplante fallen wieder weg. Während der Entwicklung zu lernen ist ein gutes Zeichen. Und tatsächlich ist es sogar ganz interessant, eine andere Route einzuschlagen, als wir hier gehen werden.

Für den Einstieg picken wir uns einen Testfall aus unserer Liste heraus, der ein möglichst kleines Inkrement für unsere bestehende Codebasis darstellt. Manchmal kann der erste Test gegen eine neue Klasse auf die Erzeugung der Instanz selbst gerichtet sein sowie dem Sicherstellen eines definierten Grundzustandes. Dieser Testfall entfällt in diesem Beispiel jedoch, weil ein RegularPrice Objekt nur Verhalten und keinen Zustand hat. Nehmen wir deshalb beispielsweise den Testfall für den ersten Ausleihtag.

Sie erinnern sich, wir wollen uns der gewünschten Funktionalität schrittweise annähern und die Feedbackschleife währenddessen möglichst häufig schließen. Wir wollen die Entwicklung eines evolutionären Designs antreiben, indem wir den Testfall so schreiben, dass er zunächst einmal fehlschlägt. Aus Sicht der hier und jetzt zu treffenden Entscheidungen kann dieser Schritt manchmal eine ernste Herausforderung darstellen.

Ferner entwerfen wir den Test so, wie wir uns die Verwendung der zu testenden Klasse wirklich wünschen. Das ist Design.

Wir müssen uns erst fragen, was die Klasse überhaupt tun soll, und uns anschließend überlegen, wie sie es denn tun soll. Den Blickwinkel auf diese Weise abzulenken ist deshalb praktisch, weil die Schnittstelle einer Klasse wichtiger ist als ihre Implementierung. Ignorieren Sie deshalb stets noch für einen Moment alle Implementierungsdetails, die sich schon in Ihrem Kopf zu Antwort melden, und schreiben Sie die Klassenschnittstelle einfach erst einmal so hin, wie sie im gewünschten Zusammenhang ideal verwendbar wäre. Ron Jeffries nennt diesen Gedankengang sprechend "Programming by Intention".

Tipp
Versetzen Sie sich geistig in den Programmierer, der Ihre Klasse verwendet, und entwerfen Sie die Klasse so, wie Sie sie intuitiv verwenden wollen würden.

Legen wir los! Die Ausleihgebühr beträgt für einen Tag 1.50 Euro. Wir benennen die generierte Testfallmethode passend um und schreiben mal hin, was wir vorhaben.


public class RegularPriceTest...
  public void testChargingOneDayRental() {
    RegularPrice price = new RegularPrice();
    assertEquals(new Euro(1.50), price.getCharge(1));
  }
}

Durch diese zwei Zeilen Testcode haben wir bereits einige Entwurfsentscheidungen getroffen.

  • Unsere neue Klasse trägt den Namen RegularPrice.
  • Exemplare der Klasse erzeugen wir durch den Default-Konstruktor.
  • Die Ausleihgebühr berechnen wir mit Hilfe einer Instanzmethode getCharge, die dazu die Anzahl von Ausleihtagen vom Typ int benötigt.
  • Als Resultat der Operation erwarten wir ein Objekt vom Typ Euro zurück.

Wir benutzen hier die Euro Klasse aus dem vorherigen Artikel "Unit Testing mit JUnit". Falls Sie JUnit noch nicht kennen, finden Sie dort auch eine Einführung dazu.

2. Wir schreiben gerade soviel Code, dass der Test tatsächlich fehlschlägt

Wir haben soeben eine Testfallmethode geschrieben und versuchen zu kompilieren. In dem Fall, dass wir durch den Test eine neue Klasse oder eine neue Methode motivieren, erwarten wir, dass die übersetzung der Testklasse, grob gesagt, schief geht, obwohl es auch äußerst interessant wäre falls nicht.

Typischerweise wollen wir auf diesem Weg in kleinen Schritten Fehlermeldungen der Art provozieren, dass eine Klasse oder Methode, die wir in unserem Test bereits benutzen, in der geforderten Form noch gar nicht existiert. Wir prüfen dadurch, ob unsere neue Klasse oder unsere neue Methode nicht mit einer bestehenden Klasse oder Methode im System kollidiert. Die neue Klasse oder Methode existiert schließlich noch nicht. Durch Polymorphie können aber durch Vererbung sehr schwer identifizierbare Seiteneffekte auftreten und fast nichts ist schlimmer, als sich darin zu täuschen, welches Stückchen Code ausgeführt wird. Wenn Sie etwas mehr Glück haben, hat ein Kollege die Klasse schon implementiert.

In statisch typisierten Programmiersprachen wie Java macht der Compiler also den ersten Test.


Class RegularPrice not found.

Die Klasse wird nicht gefunden, so dass wir gezwungen sind, eine leere Hülse anzulegen.


public class RegularPrice {
}

Dann versuchen wir erneut zu kompilieren und bekommen wieder einen Fehler.


Method getCharge(int) not found in class RegularPrice.

Typisch für diesen Entwicklungsstil ist, dass wir uns von übersetzungsfehler zu übersetzungsfehler hangeln. Wir schreiben dabei nur gerade soviel Code, dass sich die Testklasse überhaupt übersetzen lässt. Unser Ziel sind dabei kleine schnelle Feedbackschleifen. Sehr interaktive Entwicklungsumgebungen und inkrementelle Compiler unterstützen diese extrem iterative Arbeitsweise sehr gut.

Tipp
Lassen Sie sich bei der Fehlerbehebung ruhig von der Entwicklungsumgebung führen. Beheben Sie nur gerade, was der aktuelle übersetzungsfehler von Ihnen verlangt.

Abschließend definieren wir die benötigte Methode.


public class RegularPrice {
  public Euro getCharge(int daysRented) {
    return null;
  }
}

Da unser Test als Rückgabewert ein Euro Objekt erwartet, liefern wir vorerst einfach null zurück. Dieser Schritt mag zwar seltsam erscheinen, ist aber notwendig, um den Test zunächst fehlschlagen zu sehen.

Wir rekompilieren. Unser Compiler ist zufrieden. Dann sind wir es auch. Schlägt unser neuer Test aber auch tatsächlich fehl?

Ein neuer Test, und zwar nur ein fehlschlagender, sollte Anlass sein zum Schreiben neuer Programmfunktionalität. Normalerweise stellen wir mit jedem neuen Test neue Behauptungen auf, an denen wir unsere bestehende Codebasis elegant scheitern lassen. Der einzige Weg, um den Test zu erfüllen, ist somit, das Programm zu ändern und die tatsächlich geforderte Funktionalität zu implementieren.

Wir wollen dabei immer vom letzten grünen Balken ausgehend auf einen roten Balken hinarbeiten. Indem wir den Test zunächst fehlschlagen sehen, testen wir den Test selbst. Wenn der Test fehlschlägt, war der Test tatsächlich erfolgreich. Ein solcher Test gibt uns Vertrauen, dass wir einen nützlichen Test geschrieben haben. Läuft der Test aber unvermutet durch, haben sich unsere Annahmen als fehlerhaft entpuppt. Entweder haben wir dann nicht den Test geschrieben, den wir zum Antreiben unseres weiteren Designs schreiben wollten, oder unser Code implementiert die neue Funktion wirklich bereits. Beides stellt für uns nützliches Feedback dar.

Machen wir also den Test für den Test.


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<1.5> but was:<null>
at RegularPriceTest.testChargingOneDayRental
  (RegularPriceTest.java:11)

Es ist zu einfach, diesen kleinen Zwischenschritt aus falscher Bequemlichkeit auszulassen. Ich bekomme oft die Frage gestellt, ob ich wirklich so "umständlich und offensichtlich langsam" arbeite. Dabei kann uns der rote Balken und seine Fehlermeldung(en) immer sagen, was der nächste Programmierzug ist. Und das Prinzip ist so einfach. Arbeiten Sie nur vom roten auf den grünen Balken hin.

3. Wir schreiben gerade soviel Code, dass tatsächlich alle Tests durchlaufen

Von vielen verschiedenen Wegen, neue Funktionalität in das Programm zu implementieren, schlagen wir immer den Weg ein, der für uns am einfachsten erscheint. Wir programmieren zu jedem Zeitpunkt wirklich nur, was zur Erfüllung des aktuellen Testfalls absolut notwendig ist, und kein wenig mehr.

In einigen Fällen kann die einfachste Lösung sein, aus einer Methode einen festkodierten Wert zurückzuliefern. Unfassbar, aber wahr.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  public Euro getCharge(int daysRented) {
    return new Euro(1.50);
  }
}

Sowas kann natürlich nur eine übergangslösung sein bis zu dem Zeitpunkt, da uns ein weiterer Test beweist, dass die aktuelle Lösung wirklich zu einfach ist. In dem Fall sind wir dann gezwungen, für die fehlende Funktionalität weiteren Code zu schreiben. Auf diesem Weg treiben die Tests schrittweise die Entwicklung fortgeschrittener Logik an, die nicht mehr allein mit festkodierten Werten auskommt.

Eine Warnung sei hier ausgesprochen. Wenn Sie mehr Code schreiben, als Ihr Testfall in Wirklichkeit verlangt, schreiben Sie immer zu wenige Tests. Sie schreiben dann Code, der unter Umständen von keinem Test gesichert wird. Die Codeabdeckung Ihrer Tests wird in diesem Fall geringer ausfallen und das Sicherheitsnetz, das durch die feingranularen Unit Tests für das Refactoring entsteht, wird dadurch grobmaschiger. Versuchen Sie jedoch trotzdem unbedingt herauszufinden, mit wie wenigen Tests Sie in Ihrer Realität auskommen könnten. Der ultimative Test für Ihre Testsuite ist dabei, dass ein grüner Balken Ihnen Vertrauen in Ihre Programmänderung schenken soll. Ein grüner Balken soll Sie praktisch Glauben machen, das Programm enthielte keine Fehler. Sobald Ihnen jedoch welche durchs Netz gehen, sollten Sie unbedingt weitere Tests schreiben. Das Beste, was Sie tun können, ist experimentieren, messen und reflektieren. Erwarten Sie jedoch nicht, dass das, was nicht getestet ist, auch funktioniert.

Während Sie so programmieren und über der Implementation brüten, werden Ihnen oft weitere interessante Grenzfälle auffallen, die Sie ebenfalls unbedingt noch testen wollen. Denken Sie in Momenten wie diesen an die änderungsfreundliche Karteikarte, auf der Sie schon die anderen Testfälle notiert haben. In vielen solcher Fälle werden Sie ohnehin erst einmal Rücksprache mit Ihrem Kunden halten müssen, was eigentlich passieren soll, wenn der und der Fall eintritt.

Wir sind am Ende einer kurzen Testetappe angekommen. Jetzt erfolgt der wirkliche Test, ob wir erreicht haben, was wir erhofft hatten, zu erreichen. Die Tests sagen uns, wann wir fertig sind: wenn alle Tests laufen.

Wenn der Testbalken wie jetzt grün wird, wissen wir, dass es der gerade geschriebene Code war, der den Unterschied gemacht hat. Es ist diese Art der Entwicklung, die uns ein kurzes Glücksgefühl schenken kann, das Ihnen vorenthalten bleibt, solange Sie ohne diese kurzen, konkreten Feedbackzyklen programmieren. Der kleine Erfolg gibt uns zu Recht einen kurzen Moment zum Durchatmen und Freimachen ("Okay, geschafft!") und bietet einen natürlichen Abschluss für den gegenwärtigen Gedankengang ("Was ist der nächste Testfall?"). Die Tests reduzieren so Ihren Stress.

Wenn der Testbalken dagegen rot bleibt, wissen wir, dass es ebenfalls der gerade geschriebene Code gewesen sein muss, der den Fehler provoziert hat. Unterscheiden müssen wir dabei, ob nur der zuletzt geschriebene Test betroffen ist oder gar ältere Tests wieder zerbrochen sind, die schon einmal liefen. Da wir immer nur ein paar Zeilen Code und selten eine ganze Methode am Stück schreiben, lohnt es in den meisten Fällen jedoch kaum, dafür einen Debugger zu öffnen. Es sei denn, Sie sind Smalltalker. Dort findet ein beträchtlicher Teil der testgetriebenen Programmierung tatsächlich nur im Debugger statt. Es ist deshalb vorstellbar, dass Java-Umgebungen eines Tages ähnlichen Luxus bieten werden.

Hier schließt sich dann auch der Kreislauf der testgetriebenen Entwicklung.

Tests und Programmcode im Wechselspiel

Wenn wir dem Prozess aufmerksam folgen, fällt uns ein enges Wechselspiel zwischen den Tests und dem Programmcode auf.

  • Der fehlschlagende Test entscheidet, welchen Code wir als nächstes schreiben, um die Entwicklung der Programmlogik voranzutreiben.
  • Wir entscheiden anhand des bisher geschriebenen Code, welchen Test wir als nächstes schreiben, um die Entwicklung des Designs weiter anzutreiben.

Der zweite Punkt erfordert im Vergleich wesentlich mehr Kreativität, weil Tests oft schwieriger zu formulieren als zu erfüllen sind.

Aufgrund der gegenseitigen Rückwirkung zwischen Tests und Programmcode ist es nicht ratsam, mehrere fehlschlagende Testfälle gleichzeitig im Rennen zu haben oder gar erst alle möglichen Testfälle zu schreiben und sie anschließend Stück für Stück zu implementieren. Stellen wir während der Implementierung nämlich fest, dass unser Design in eine ganz andere Richtung gehen möchte, müssten wir alle unsere geschriebenen Tests noch mal ändern.

Manchmal können wir gerade erst während der Programmierung erkennen, welchen Test wir eigentlich hätten schreiben müssen. Oft werden dann getroffene Entwurfsentscheidungen invalidiert und in vielen Fällen ändert sich die Klassenschnittstelle noch einmal. Aus diesem Grund ist es angebracht, nur einen Test auf einmal zu schreiben und sofort zu implementieren. Nur so lernen wir schrittweise, welche Tests uns wirklich weiterbringen. Die Buchführung darüber, welche Tests wir noch schreiben wollen, können wir auf einer einfachen Karteikarte vornehmen.

Tipp
Schreiben Sie einen Testfall zurzeit und bringen Sie ihn zum Laufen, bevor Sie die Arbeit am nächsten Testfall beginnen. Wenn Sie mehrere Testfälle schreiben müssen, sammeln Sie sie auf einer Karteikarte.

Ausnahmebehandlung

Der erste Durchlauf sollte Ihnen zeigen, wie sich die testgetriebene Entwicklung anfühlen sollte. Gewissermaßen habe ich ein Idealbild skizziert, dessen Realität häufig von Ausnahmen begleitet sein wird. Genau genommen handelt es sich ausdrücklich um Ausnahmen in meiner Beschreibung, jedoch nicht im Prozess selbst. Während der Programmierung werden diese "Bad Day" Szenarien ebenso häufig auftreten wie das zuvor beschriebene "Good Day" Szenario.

Die drei häufigsten Ausnahmefälle treten während der Schritte 2 und 3 auf.

  • Der Test läuft, obwohl er eigentlich fehlschlagen sollte.
  • Der Test schlägt fehl, obwohl er eigentlich laufen sollte.
  • Die Implementierung des Tests erfordert neue Methoden und damit weitere Tests.

2a. Der Test läuft, obwohl er eigentlich fehlschlagen sollte

Die natürliche Triebfeder des testgetriebenen Designs ist, dass wir Tests schreiben, die neue Funktionalität einfordern und unseren bestehenden Code auf intelligente Weise zerbrechen. Solche Tests schlagen fehl, bis wir den Code wieder in Ordnung gebracht und um die neue Funktion erweitert haben.

Manchmal lässt sich das Design jedoch nicht so elegant durch neue Tests erzwingen. Manchmal schreiben wir einfach Tests, die schon auf Anhieb laufen. Wieso das?

Hier sehen Sie ein Beispiel. Unser nächster Test ist die Ausleihgebühr für drei Tage.

Für den zweiten Test benötigen wir erneut ein price Objekt, weshalb es günstig erscheint, es zunächst in die Test-Fixture herauszuziehen.


JUnit-IkonGrüner JUnit-Balken

public class RegularPriceTest...
    
  private RegularPrice price;
    
  protected void setUp() {
    price = new RegularPrice();
  }
    
  public void testChargingOneDayRental() {
    assertEquals(new Euro(1.50), price.getCharge(1));
  }
}

Durch dieses Refactoring können wir gleich viel einfacher den zweiten Test hinschreiben und müssen nicht unnötig Code kopieren. Vorher lassen wir jedoch zur Sicherheit die Tests durchlaufen.

Dann schreiben wir den neuen Test für drei Ausleihtage.


JUnit-IkonGrüner JUnit-Balken

public class RegularPriceTest...
  public void testChargingThreeDayRental() {
    assertEquals(new Euro(1.50), price.getCharge(3));
  }
}

Keine überraschung, unser neuer Test läuft auf Anhieb, weil er keine neuen Forderungen stellt.

In diesem Fall hätten wir zwar nur eine andere Testreihenfolge einschlagen müssen, doch nicht immer lässt sich so leicht der Testfall finden, mit dem wir wie gewünscht den roten Testbalken erzwingen.

Tests dieser Art sind für die testgetriebene Entwicklung zunächst einmal wertlos, weil sie uns mit dem Design nicht weiterbringen. Diese Tests erzählen uns jedoch eine Geschichte über unseren Testprozess selbst, aus der wir für die Zukunft lernen können. Aber wie sind wir hier überhaupt hingekommen?

Ein grüner Balken könnte doch allerhöchstens bedeuten, dass entweder unser Code die neue Funktion schon realisiert oder unser Test bereits einen Fehler enthält. Unter Umständen sind ja wirklich die Behauptungen fraglich, die wir im letzten Test aufgestellt haben. Die Wahrscheinlichkeit dafür ist zwar meist relativ gering, wenn wir uns aber vor Augen halten, dass sich der Fehler nur in den Code eingeschlichen haben kann, den wir gerade getippt haben, geht eine genaue Nachprüfung schnell vonstatten.

Wann immer ich dem grünen Balken jedoch nicht ganz und gar traue, streue ich oft extra kleine Fehler in Tests oder Code ein. Die Geschichte vom nicht ausgeführten Test kennen Sie ja bereits.

Manchmal kann es auch ratsam sein, den getippten Unfug der vergangenen wenigen Minuten einfach auf den Haufen zu werfen und in kleineren Schritten mit häufigerem Testen nochmals von vorne zu starten. Ehrlich. Fassen Sie Mut und gehen Sie zum letzten grünen oder auch roten Balken zurück, mit dem die Welt noch in Ordnung war. Probieren Sie's wenigstens einmal aus. Sie könnten sich damit unter Umständen viel Zeit und Mühe ersparen.

Der andere Grund für einen unerwarteten grünen Balken ist, dass die vermeintlich neue Funktion wirklich schon implementiert und getestet ist. Dieser Umstand spricht dann auch tatsächlich über unseren Testprozess und bietet eine gute Gelegenheit zum Reflektieren.

  • Haben wir für einen der vergangenen Testfälle geringfügig mehr Code geschrieben, als unbedingt notwendig gewesen wäre?
  • Ist dieser Fall schon anderswo mitgetestet? Vielleicht nur indirekt? Wie stark unterscheiden sich die Tests? Und können wir sie geeignet zusammenfassen?
  • Duplizieren wir gerade Testcode und wissen es vielleicht nicht?
  • Haben die Tests, die wir schreiben, zuwenig "Kick" dafür, dass sie interessante Entwurfsentscheidungen aufwerfen? Können wir solche Tests zukünftig ans Ende der Episode stellen? Ist es dann überhaupt noch notwendig, sie zu schreiben?

In diesem Fall hatten wir einfach nur Pech mit der Wahl unseres Inkrements. Falls es ein nächstes Mal gibt, sollten wir vorher zum Beispiel vier Ausleihtage testen.

Ernsthaft problematisch wird es, wenn es schwieriger ist, den Testcode korrekt zu schreiben als den Programmcode. Ihre Tests sollten deswegen keine eigene Logik enthalten, sonst müssten Sie für sie selbst wiederum Tests schreiben. Die ganze Arbeitsweise beruht auf der stillen Annahme, dass die Tests einfacher zu schreiben sind als der zu testende Code. Sobald sich dieses Verhältnis bei Ihnen permanent umdreht, gibt es nichts, als einfachere Tests zu schreiben. Da Tests und Code sehr stark miteinander verzahnt sind, werden Sie dabei jedoch immer auch Einsicht in ein einfacheres und besseres Design finden.

3a. Der Test schlägt fehl, obwohl er eigentlich laufen sollte

Die Eleganz des testgetriebenen Programmieransatzes liegt in seinen schnellen und konkreten Feedbackzyklen. Zwischen dem Zeitpunkt einer getroffenen Entwurfsentscheidung und dem bewiesenen Erfolg oder Misserfolg dieser Idee liegen nur wenige Minuten und immer nur ein paar Zeilen Code. Wenn wir in sehr kleinen Schritten vorgehen und uns stets am einfachsten Design orientieren, vergehen zwischen zwei grünen Testbalken gerade einmal 1-3 Minuten.

Manchmal jedoch sind unsere Inkremente zu groß gewählt und wir verzetteln uns einfach. Manchmal machen wir an sich vermeidbare Programmierfehler. Manchmal schreiben wir Code, der gar nicht funktioniert wie erwartet.

Hier ist ein Beispiel, wie es sich ganz tatsächlich zugetragen hat. Wir testen die Ausleihe von vier Tagen.


public class RegularPriceTest...
  public void testChargingFourDayRental() {
    assertEquals(new Euro(3.00), price.getCharge(4));
  }
}

Wir prüfen noch, ob der Test auch wirklich fehlschlägt...


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<3.0> but was:<1.5>
at RegularPriceTest.testChargingFourDayRental
  (RegularPriceTest.java:24)

Dann implementieren wir, was uns als erste Lösung in den Kopf kommt...


public class RegularPrice {
  public Euro getCharge(int daysRented) {
    Euro result = new Euro(1.50);
    if (daysRented == 4) {
      result.add(new Euro(1.50));
    }
    return result;
  }
}

Doch unser Test sagt "Stoppt mal!"


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<3.0> but was:<1.5>
at RegularPriceTest.testChargingFourDayRental
  (RegularPriceTest.java:24)

Oops, wir haben wohl vergessen, zu rekompilieren. Wir kompilieren also neu...aber ohne Resultat.

Wie sind wir hierher gekommen? Die Regel ist, dass der Fehler nur in dem Code stecken kann, den wir gerade getippt haben. Sehen Sie den Fehler?

Wir können uns aus dem Geschehen gerade keinen Reim machen und verwerfen deshalb unsere letzte änderung. Zeitlich verlieren wir durch den Rollback ungefähr eine Minute. Wer weiß, wieviele unnötige Minuten wir uns dadurch tatsächlich ersparen...


public class RegularPrice {
  public Euro getCharge(int daysRented) {
    return new Euro(1.50);
  }
}

Wenn wir nur gerade unseren aktuellen Testfall erfüllen wollen, wäre folgende Lösung eigentlich noch viel einfacher gewesen, müssen wir uns schließlich eingestehen.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  public Euro getCharge(int daysRented) {
    if (daysRented <= 3) return new Euro(1.50);

    return new Euro(3.00);
  }
}

Okay, dieser Code belohnt unseren Test mit grüner Farbe. Trotzdem sind wir mit der Lösung noch nicht zufrieden. In unserem ersten Versuch wollten wir bereits ausdrücken, dass der Preis aus einem fixen und einem variablen Anteil besteht. Machen wir das mal.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  public Euro getCharge(int daysRented) {
    if (daysRented <= 3) return new Euro(1.50);

    return new Euro(1.50).add(new Euro(1.50));
  }
}

Brrr... Bei diesem Anblick läuft uns ein kleiner Schauer über den Rücken. Zum einen: Warum tauchen hier drei new Euro(1.50) Schnipsel auf? Zum anderen: Was bedeuten diese drei identischen Schnipsel? Oder: Sind sie überhaupt identisch?

Aber: Moment, Moment, Moment. Wieso funktioniert das add Konstrukt plötzlich an dieser Stelle, nicht aber so vor einer Minute in unserem ersten Versuch?

Lassen Sie uns jetzt doch noch einmal der Sache auf den Grund gehen. Die Tests lügen nicht. Ein Blick auf den zugehörigen Testfall zeigt ein Beispiel zur korrekten Verwendung der Euro Klasse. Schlagen Sie bitte im vorherigen Artikel "Unit Testing mit JUnit" nach, wenn Sie die Testfallklasse als Ganzes lesen möchten.


public class EuroTest...
  private Euro two;

  protected void setUp() {
    two = new Euro(2.00);
  }

  public void testAdding() {
    Euro three = two.add(new Euro(1.00));
    assertEquals("sum", 3.00, three.getAmount(), 0.001);
    assertEquals("two", 2.00, two.getAmount(), 0.001);
  }
}

Okay, Objekte der Klasse Euro sind also nicht veränderbar. Stattdessen wird ein neues Objekt mit dem neuen Wert zurückgegeben. Wir beschließen, dass plus ein besserer Name für die add Operation wäre und dass eine entsprechende Umbenennung fällig ist, sobald wir mit den Tests abgeschlossen haben.

Mit einem grünen Balken im Rücken machen wir uns endlich ans Refactoring. Zur Erinnerung: Die unzähligen "ein Euro fuffzig" Objekte störten uns. Guter Code braucht gute Namen. Ein Refactoring sollte immer durchgeführt werden, wenn wir etwas gelernt haben und wir dieses Wissen Bestandteil des Programms selbst machen möchten.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  static final Euro BASE_PRICE = new Euro(1.50);

  public Euro getCharge(int daysRented) {
    if (daysRented <= 3) return BASE_PRICE;

    return BASE_PRICE.add(new Euro(1.50));
  }
}

Ein kurzer Testlauf und weiter geht's mit den anderen "150 Cents".


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  static final Euro BASE_PRICE = new Euro(1.50);
  static final Euro PRICE_PER_DAY = new Euro(1.50);

  public Euro getCharge(int daysRented) {
    if (daysRented <= 3) return BASE_PRICE;

    return BASE_PRICE.add(PRICE_PER_DAY);
  }
}

Ebenso gehen wir mit der Anzahl Tage um, die der reduzierte Preis gelten soll.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice {
  static final Euro BASE_PRICE = new Euro(1.50);
  static final Euro PRICE_PER_DAY = new Euro(1.50);
  static final int DAYS_DISCOUNTED = 3;

  public Euro getCharge(int daysRented) {
    if (daysRented <= DAYS_DISCOUNTED) return BASE_PRICE;

    return BASE_PRICE.add(PRICE_PER_DAY);
  }
}

Durch den letzten Testfall und anschließendes Refactoring hat unser Code erste Form angenommen. Der Punkt aber war, dass es immer lohnt, kleinere Schritte zu wählen. Fehler der Art dieses Beispiels sind leider menschlich. Deshalb ist auch mein einziger Rat, den ich hier geben kann, in ganz kleinen Schritte vorzugehen, gescheit mit einem Partner zu programmieren und aus den Fehlern der Vergangenheit für die Zukunft zu lernen.

Wenn Sie jedoch ins Stocken geraten und viele Minuten allein zur Fehlersuche benötigen, dann schreiben Sie entweder zu wenige, zu große oder die falsche Art von Tests. Vielleicht machen Ihnen aber auch unerwünschte Seiteneffekte zu schaffen, dann sollten die Unit Tests relativ nahe der Fehlerursache Alarm schlagen und sofort den Fehler finden lassen. Wenn sehr viele Tests gleichzeitig zerbrechen, kann dies häufig dafür sprechen, dass Ihr Design unter Umständen nicht ausreichend entkoppelt ist.

3b. Die Implementierung des Tests erfordert neue Methoden und damit weitere Tests

Seine schnellen Feedbackzyklen erfährt der Testansatz dadurch, dass jeder Test zunächst auf sehr einfache Weise erfüllt wird und keine unnötige Komplexität entsteht. Sinnvolle Generalisierungen und Abstraktionen bilden sich erst nach Richtungsgabe weiterer Tests heraus. Test für Test entwickeln wir so, was unser Design für heute wirklich benötigt.

Manchmal können wir einen Test jedoch beim besten Willen nicht einfach mal so schnell erfüllen. Manchmal wäre zunächst ein vereinfachendes Refactoring angebracht. Manchmal entdecken wir, dass uns zur intuitiven Implementierung eigentlich noch Methoden oder Objekte fehlen. Manchmal ist der Schritt, den unser Testfall nimmt, auch einfach nur zu groß und wir bohren uns immer weiter in die Tiefe, bis wir schließlich das Tageslicht verlieren und kapitulieren müssen.

Auch hierzu betrachten wir ein konkretes Beispiel. Unser letzter Testfall testet die Fortschreibung der Progressionsrampe für die Ausleihe von mehr als vier Tagen.


public class RegularPriceTest...
  public void testChargingFiveDayRental() {
    assertEquals(new Euro(4.50), price.getCharge(5));
  }
}

Erneut lassen wir uns von unserer Testsuite führen.


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<4.5> but was:<3.0>
at RegularPriceTest.testChargingFiveDayRental
  (RegularPriceTest.java:28)

Für diesen Testfall müssen wir für alle Zeiträume von mehr als drei Tagen den tagesabhängigen Euro-Betrag mit den Tagen multiplizieren, die der Kunde drüberliegt.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice...
  public Euro getCharge(int daysRented) {
    if (daysRented <= DAYS_DISCOUNTED) return BASE_PRICE;

    int additionalDays = daysRented - DAYS_DISCOUNTED;
    return BASE_PRICE.add(new Euro(PRICE_PER_DAY.getAmount()
                                   * additionalDays));
  }
}

Whoa, diese Lösung ist hübsch hässlich. Um den Test möglichst einfach zu erfüllen, haben wir unser Euro Objekt PRICE_PER_DAY erst wieder in den primitiven Typ double wandeln müssen, um es dort mit der Anzahl von Tagen zu multiplizieren, um dann sofort wieder ein Euro Objekt zu bauen. Die Berechnung des Resultats lässt sich kaum noch darüber aus, was eigentlich passiert.

Wann immer ein Programm so aus der Form gerät wie jetzt gerade, will es uns etwas mitteilen. Hier sieht es danach aus, als fehle der Euro Klasse die Möglichkeit, Geldbeträge mit ganzen Zahlen zu multiplizieren. Wir müssen dieses Verständnis darüber, wie der Code strukturiert werden will, in das Programm selbst zurückführen.

Definitiv richtig ist jedoch die Entscheidung, erst die einfache, aber hässliche Lösung hinzuschreiben, damit den Test zu erfüllen und dann zu refaktorisieren. Nachdem der Test grün ist, haben wir eine viel bessere Ausgangsbasis für eine funktionserhaltende Verbesserung des Designs. Wir refaktorisieren nur mit einem grünen Balken.

Die Disziplin dafür aufzubringen und durchzuhalten, zu verschieben, was Sie schon vor Ihrem geistigen Auge sehen, wenn auch nur für wenige Momente, ist ein Schlüsselaspekt dieses Prozesses. Bleiben Sie immer in der Nähe des grünen Lichts und versuchen Sie nicht, zuviele Bälle gleichzeitig in der Luft zu jonglieren.

Tipp
Arbeiten Sie nicht an verschiedenen Enden zur gleichen Zeit. Versuchen Sie Probleme nacheinander zu lösen, nicht gleichzeitig. Brechen Sie die Probleme stattdessen kleiner und holen Sie sich öfters Feedback ein.

Unser letzter Test läuft und damit haben wir jetzt die Verantwortung, den Code so zu verbessern, dass er unsere Intention besser für andere Programmierer kommuniziert.

Was wir eigentlich ausdrücken wollen ist, dass die Euro Klasse die Verantwortlichkeit übernimmt, einen Geldbetrag mit einer ganzen Zahl zu multiplizieren. Zeit also, einen weiteren Test für die Klasse zu schreiben. Wir verwenden erneut das "zwei Euro" Objekt aus der Fixture.


public class EuroTest...
  private Euro two;

  protected void setUp() {
    two = new Euro(2.00);
  }

  public void testMultiplying() {
    assertEquals(new Euro(14.00), two.times(7));
  }
}

Den weiteren Testprozess sollten Sie soweit schon kennengelernt haben. Wir kompilieren, nur um zu sehen, ob in der Zwischenzeit nicht schon jemand aus unserem Team eine times Methode benötigt hat. Das scheint heute jedoch nicht der Fall zu sein. Vielleicht haben wir morgen mehr Glück...


Method times(int) not found in class Euro.

Solange sich der Code nicht fehlerfrei übersetzen lässt, können wir nicht die Tests ausführen, und solange sich die Tests nicht ausführen lassen, können wir nicht herausfinden, was unser nächster Programmierzug sein will. Ein leerer Methodenrumpf muss her.


public class Euro...
  public Euro times(int factor) {
    return null;
  }
}

Sofort beschwert sich JUnit über die leere Implementierung.


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  expected:<14.0> but was:<null>
at EuroTest.testMultiplying(EuroTest.java:34)

Die Lösung dazu ist einfach.


JUnit-IkonGrüner JUnit-Balken

public class Euro...
  public Euro times(int factor) {
    return new Euro(cents * factor);
  }
}

Damit können wir die neue times Methode in unserer Preisberechnung mitbenutzen, wodurch sich der Code ganz erheblich vereinfacht.


JUnit-IkonGrüner JUnit-Balken

public class RegularPrice...
  public Euro getCharge(int daysRented) {
    if (daysRented <= DAYS_DISCOUNTED) return BASE_PRICE;

    int additionalDays = daysRented - DAYS_DISCOUNTED;
    return BASE_PRICE.add(PRICE_PER_DAY.times(additionalDays));
  }
}

Meine Erfahrung sagt, dass es immer möglich ist, wie in diesem Beispiel zunächst die einfache, aber dreckige Lösung einzuschlagen und hinterher sofort zu refaktorisieren.

Wenn wir den aktuellen Testfall jedoch tatsächlich nur mit einer neuen Methode oder einer neuen Klasse möglichst einfach zum Laufen zu kriegen meinen, dann müssen wir uns an den langsameren iterativen Abstieg machen. Dazu fangen wir eine neue Testfallmethode an und implementieren die neue Methode oder die neue Klasse auf die gewohnte testgetriebene Art und Weise. Das hätten wir in diesem Beispiel sogar auch machen können. Wir müssen dabei nur Acht geben, dass wir nicht länger tauchen gehen, als uns der Sauerstoff reicht. Ständiges Feedback erhalten wir bei der Bottom-up-Implementierung nur, wenn wir häufig genug auftauchen und die Top-level-Tests ausführen können.

Wenn wir zur Erfüllung des Testfalls zum Beispiel sofort die times Methode aufgerufen und mitentwickelt hätten, wäre diese Methode schlussendlich nur indirekt durch einen Testfall der RegularPriceTest Klasse mitgetestet. Wenn wir gleichzeitig einen neuen Testfall testMultiplying geöffnet hätten, würden wir uns mit zwei fehlschlagenden Testfällen gleichzeitig rumschlagen. Das wäre Murks. Offene Tests, die wegen "inkompatibler Umstellungen" auskommentiert wurden, sich für eine Zeit lang nicht übersetzen lassen oder aus anderen Gründen fehlschlagen, deuten in diesem Prozess oft auf eine ungeschickte Zerlegung der Programmieraufgabe in Teilaufgaben und Testfälle. Ich werde in meinem nächsten Artikel noch mehr auf diese Problematik eingehen.

Berührungspunkt zum einfachen Design

Das Feedbackprinzip erfordert, dass der Weg zur Implementierung des aktuellen Testfalls extrem kurz gehalten wird. Ansonsten zeigt die Erfahrung, dass sich manch einer beim Top-down-Entwurf tiefer und tiefer in die Innereien des Systems bohrt. Der schlimmste Fall wäre, dass unser aktueller Testfall eine Methode benötigt, die noch eine weitere Methode benötigt, die wiederum zusätzliche Methoden benötigt und so weiter. Das Ergebnis eines solchen Top-down-Abstiegs wäre während der gesamten Bottom-up-Implementierung ungewiss, bis alle unsere Tests wieder laufen.

Es ist diese Problematik, die es rechtfertigt, die Dinge beim einfachen Namen zu nennen und wörtlich die einfachste Lösung zu suchen, die möglicherweise funktionieren könnte. Nehmen Sie an, dass Sie momentan nicht mehr benötigen, als Ihr Testfall verlangt. Wenn Ihr Testfall schon zuviel auf einmal erfordert, zerlegen Sie ihn einfach in mehrere kleinere oder verwerfen Sie ihn und schreiben Sie den einfachsten Testfall, der Ihnen einfällt.

Der nötige Vereinfachungsschritt, um herauszufinden, wie wir den Code in kleinen Babyschritten und in möglichst einfacher Weise ins Leben testen können, kann extrem herausfordernd sein. Den Test dann hinterher zu erfüllen, ist dagegen oft der leichtere Schritt von beiden.

Berührungspunkte zum Refactoring

Neue Tests stets nur auf die einfache, hässliche Art zum Leben zu bringen, würde zwangsläufig zu einem Design-Fiasko führen und die Weiterentwicklung des Programms letztlich zum Kollaps bringen. Um die Struktur des Programms aber aufrechtzuerhalten und fortlaufend zu verbessern, ist ein regelmäßiges Refactoring im Prozess der testgetriebenen Entwicklung absolut unerlässlich.

Wann ein Refactoring notwendig wird, müssen Sie noch selbst entscheiden, doch es gibt zum Glück nur zwei Momente, zu denen sich ein Refactoring aufdrängen kann.

  • Um einen Test möglichst einfach zu erfüllen, können wir unseren Code vorher in Form refaktorisieren.
  • Unmittelbar nachdem die Tests laufen, müssen wir unseren Code in die einfachste Form refaktorisieren.

Ein vorausschauendes Refactoring ist dabei als Kürteil anzusehen und ein ausgelassenes Refactoring ist immer durch ein nachgezogenes kompensierbar, wenn auch die Kosten dafür mit der Zeit steigen. Ein abschließendes Refactoring dagegen gehört zum Pflichtteil und tritt dadurch mit Abstand häufiger auf. Kent Beck hat deshalb das Mantra "Make it run, make it right, make it fast." geprägt.

Das bedeutet, dass wir zunächst unsere Tests erfüllen, obwohl wir genau wissen, dass es insgesamt vielleicht noch einfachere Lösungen gibt und dass ein geschicktes Design weit mehr erfordert, als nur einen grünen Testbalken hervorzuprogrammieren. Sobald die Tests jedoch laufen, haben wir wenigstens die Versicherung, dass wir die Nuss überhaupt knacken können, dass wir den richtigen Weg eingeschlagen haben und dass wir mit den Tests als Sicherheit unbekümmert mit verschiedenen Refactoringzügen experimentieren könnten. Programmierung und Refaktorisierung sollten sich dabei überhaupt nicht an geläufigen Vorstellungen von laufzeitoptimiertem Programmcode orientieren. Performance-Fragen werden stattdessen später durch den Einsatz eines Profilers beantwortet. Das Programm selbst soll uns informieren, wo es an Geschwindigkeit mangelt. Immerhin erlaubt überhaupt erst gut faktorisierter Code ein effektives Performance-Tuning an den benötigten Stellen. Außerdem können wir so anschließend mit dem Werkzeug messen, was die Optimierung wirklich gebracht hat, außer schlechter lesbarem Code, und können so unnötige Tricksereien wieder rückgängig machen.

Eine wichtige Frage ist, wann der richtige Zeitpunkt zum Refactoring gekommen ist. Wieviel Unrat dürfen wir anhäufen, bevor wir hinter unserer Testsuite aufzuräumen beginnen? Die wirkliche Frage, die sich dahinter jedoch versteckt, lautet anders. Wann haben wir genug gelernt, um das Refactoring zielgerichtet durchführen zu können?

Zu früh zu refaktorisieren, kann bedeuten, das Design aufgrund von Spekulationen in die falsche Richtung zu drängen. Zu spät zu refaktorisieren, kann dagegen darin resultieren, erst nach vielen, unter Umständen auch größeren Refactorings die Richtungsgabe zu erkennen, in die der Code gelenkt werden will. Im Unglücksfall kann jedes Refactoring jedoch immer durch ein inverses rückgängig gemacht werden. Starten Sie mit dem Refactoring deshalb besser früher denn später.

Tipp
Refaktorisieren Sie, wenn möglich, sofort, wenn Sie die Einsicht in ein insgesamt einfacheres Design gefunden haben, nicht jedoch inmitten der Programmierung.

Wann immer Ihnen während der Implementierung eine Möglichkeit zur Vereinfachung des Designs auffällt, notieren Sie die Idee, bevor sie verloren geht. Kümmern Sie sich später darum, wenn der JUnit-Balken wieder auf grün ist. Vermischen Sie jedoch nicht Programmierung und Refaktorisierung, sonst besteht die Gefahr, dass Sie sich fürchterlich vertüdern.

Testen als Design- und Analysetechnik

Ward Cunningham behauptet, dass testgetriebene Programmierung keine Testtechnik ist. Was meint er damit? Er drückt damit aus, dass dieser Programmieransatz vielmehr Design- und Analysetechnik als Testtechnik ausmacht. Sind die Tests demnach nur ein willkommenes Geschenk des Prozesses?

Tatsächlich zwingt uns die Arbeitsweise viel stärker in die Analyse des geforderten Programmverhaltens und den Entwurf einer geeigneten Klassenschnittstelle, als vorzeitig schon über Implementierungsdetails zu entscheiden.

  • Was ist unsere Aufgabe?
  • Wie passen die Anforderungen ins Gesamtbild?
  • Welche Systemteile werden wir berühren und anpassen müssen?
  • Wo liegen die interessanten Grenzfälle?
  • Welche Klassen und Methoden benötigen wir?
  • Wie soll die Klasse benutzt werden?
  • Wie testen wir das?

Der Prozess setzt voraus, dass wir schon genau wissen, was wir programmieren müssen. Die Idee dahinter ist, dass wir unsere Anforderungen systematisch und sehr zeitnah zur Programmierung analysieren und auf sehr niedrigem Abstraktionsniveau in einem ausführbaren Test formulieren müssen, bevor wir losprogrammieren können. Die Unit Tests stellen eine gute Möglichkeit dafür dar, konkret und unmissverständlich zu definieren, was unser Programm leisten soll. Haben wir dieses Verständnis erst einmal in einem Test festgehalten, ist die Chance auch groß, dass unser Code das Richtige tun wird. Manchmal stoßen wir bei dem Versuch, die Anforderungen klar und deutlich in einem Test festzunageln, auf Widersprüche, Missverständnisse und Fragen, die uns helfen, das richtige Programm zu entwickeln. Deshalb ist es fast unumgänglich, dass Sie Ihren Kunden intensiv in die Entwicklung einbinden. Sind die Ansprechpartner auf Kundenseite ständig direkt verfügbar, können Ihre Fragen während der Entwicklung sofort geklärt werden.

Mit den Tests gewinnen wir eine ausführbare Spezifikation, mit der wir fortan automatisch unseren Programmcode exerzieren können. Wir gewinnen außerdem eine umfassende Dokumentation darüber, was unser Code wie leistet. Es sind die Tests, die wir uns als Erstes anschauen, wenn wir verstehen wollen, wie sich der Code verhält. Die Testsuite kann uns anhand konkreter Anwendungsbeispiele demonstrieren, wie eine Klasse oder Methode verwendet werden soll. Da Spezifikation und Dokumentation direkt in der Programmiersprache selbst formuliert sind, sorgt der Compiler in statisch typisierten Sprachen dafür, dass wir nicht vergessen, unsere Tests anzupassen, wenn sich getestete Klassenschnittstellen geändert haben. Spezifikation und Dokumentation haben so eher die Chance, aktuell zur Codebasis zu bleiben.

Testgetrieben zu programmieren impliziert, dass die Tests schon Klassen verwenden, bevor wir überhaupt den Code für die Klasse geschrieben haben. Wir versetzen uns mit den Tests regulär in die Rolle eines frühen Verwenders unserer Klasse. Es ist exakt diese frische Perspektive, die uns lehren kann, wie der Code eigentlich geschrieben und entworfen werden soll. Schließlich können wir oft erst durch die Benutzung einer Schnittstelle herausfinden, wie wir die Klassenverwendung gestalten hätten sollen. Nicht selten schlägt ein testgetriebenes Design auf diesem Weg eine vollkommen andere Richtung ein, als im voraus erahnt werden könnte.

Wenn wir Unit Tests schreiben, müssen wir die Implementation an ihrer Schnittstelle isolieren. über Implementierungsdetails entscheiden wir zu diesem Zeitpunkt noch überhaupt nicht. Weil Klassen öfters verwendet als geschrieben werden, sollten Klassenschnittstellen ohnehin immer aus der Perspektive bequemer Benutzbarkeit entworfen werden, nicht zur möglichst bequemen Programmierung. Außerdem ist die Wahrscheinlichkeit hoch, dass sich die Implementierung der Klasse noch einmal ändert. Oder zweimal...

Unit Testing erfordert, dass wir genau wissen, was eigentlich die "Unit" ist, die wir testen wollen. Dazu müssen wir das Programm in kleine, unabhängig voneinander testbare Einheiten isolieren. Diese Entkopplung wiederum erfordert ebenso Schnittstellen in unserem Code. Gerade dadurch, dass wir den Code schon in unseren Tests verwenden, kann uns der Unit Test oft zeigen, wie der Code strukturiert werden will, damit er möglichst einfach testbar ist. Das Resultat davon ist häufig ein Design, das in Richtung vieler kleiner Klassen strebt und untereinander durch schmale Schnittstellen entkoppelt ist, so dass über den Unit Test hinweg andere Systemteile zu Testzwecken substituiert werden können. Allein schon indem Sie darauf Wert legen, welche Konstruktoren Sie schreiben, können Sie oft die Testumgebung für ein Objekt minimieren.

Im Gegensatz dazu ist es häufig schlecht bis gar nicht möglich, eine bestehende Codebasis nachträglich noch auf effektive Weise zu testen, wenn diese nicht ursprünglich mit dem Anspruch auf Testbarkeit entworfen wurde. Oft sind die gegenseitigen Abhängigkeiten in solch einem Code, der nicht testgetrieben entwickelt wurde, so groß, dass es praktisch fast unmöglich ist, diese Module in Isolation zu testen. In einer solchen Umgebung degenerieren die Unit Tests meist zu Mikrointegrationstests, da häufig ein größerer Anwendungskontext für die Testumgebung aufgebaut und verwaltet werden muss. Die Anzahl der zu schreibenden Testfälle explodiert häufig aufgrund der kombinatorischen Vielfaltigkeit, in der die Module zusammenhängen. Hier hilft es eben nur, die einzelnen Module und Klassen besser zu entkoppeln. Dann jedoch laufen wir in ein bekanntes Henne-Ei-Dilemma. Ohne eine umfassende Testsuite sollten wir nicht refaktorisieren. Ohne Refactoring können wir keine einfachen Tests schreiben.

Viele unserer heutigen Testprobleme entstehen wirklich nur dadurch, dass der Code, mit dem wir arbeiten müssen und zu dem unser Code Schnittstellen bildet, selbst nicht testgetrieben entwickelt wurde.

Danksagungen

In der Reihenfolge, in der wertvolle Anmerkungen bei mir eintrudelten: Tammo Freese (der kritischste Leser, den ich vermutlich kenne, hat mich überzeugt, den Test-First-Zyklus auf drei Schritte runterzubrechen und mir als Sparringspartner für viele gemeinsame Programmierepisoden gedient), Martin Müller-Rohde, Hans Wegener, Dierk König ("offene Tests" als Test-Smell war der entscheidende Hinweis, der mich dazu bewegte, das Beispiel 3b komplett umzuschreiben), Stefan Roock, Antonín Andert, Bastiaan Harmsen (mit einem unbestechlichen "Handle" für die menschlichen Faktoren), Johannes Link, Olaf Kock, Stefan Schmiedl (kam mit der grossen Refaktorplanierraupe und schrieb meinen Kram gleich so um, dass er es besser verstehen konnte, was ich, wie sein Kumpel Armin Roehrl, schamlos ausgenutzt hab), Martin Lippert, Michael Schürig, Manfred Lange, Etienne Studer und Ilja Preuß.

Weiterführende Literatur

29.4.2005

Frühes und
häufiges Testen

OBJEKTspektrum 3/2005

von Frank Westphal und Johannes Link
erschienen im OBJEKTspektrum 3/2005

"From Stoplight to Spotlight": Vormals zu einer Phase im Entwicklungsprozess erklärt und oft genug als Schlusslicht des Projekts betrachtet, spielt sich das Testen heute als Aktivität ab und geht bei testgetriebener Entwicklung sogar allem anderen mit rotgrünem Scheinwerferlicht voraus.

In der Softwarebranche breitet sich eine Kluft aus. Auf der einen Seite sehen wir Entwicklerteams, die ihre Software schnell und zuverlässig ändern können. Sie schreiben automatisierte Testfälle für alles, was womöglich schief gehen könnte, und halten ihren Code durch ständiges Refactoring sauber und flexibel. Auf der anderen Seite sehen wir Teams, deren Entwicklungsgeschwindigkeit mit fortschreitender Projektdauer immer weiter sinkt. Sie testen ihr System überwiegend manuell und ihr Code von gestern steht den Anforderungen von heute nicht selten im Weg. Während erstere ihren Kunden dabei helfen, neue Geschäftsideen möglichst schnell in hoher Qualität umzusetzen, stehen letztere unter einem immer höheren Druck und der Drohung, durch "billige" Teams in Offshoring-Ländern ersetzt zu werden.

Entwickler, die das Testen lieben gelernt haben?

Eingeläutet wurde der Wandel beim Testen vor nunmehr sieben Jahren durch Extreme Programming (XP) und JUnit. Testen war auf einmal cool. JUnits grüner Balken machte süchtig, sorgte er doch für ein lang vermisstes Gefühl: das Vertrauen, dass die produzierte Software tatsächlich wie gewünscht funktionierte. Doch schnellen Testerfolgen folgte erste Ernüchterung. Bestimmte Ecken ließen sich nicht so einfach automatisiert testen, so z. B. grafische Benutzungsschnittstellen, Enterprise Java Beans (EJBs), Datenbanken, Threads und Legacy Code. Hier zeigte sich dann auch, wer wirklich testgetrieben entwickelte und wer nicht. Denn Testprobleme weisen zumeist auf eine Design- oder Prozessschwäche hin.

Heute gelten diese Herausforderungen als gemeistert. Was sich programmieren lässt, lässt sich auch testen. Der Weg zu dieser Erkenntnis führt über einige Hürden; erfolgreiches Testen verlangt eben mehr als die bloße Beherrschung des Test-Frameworks. Insbesondere zeigt sich meist recht schnell, dass zeitlich nachgeordnetes Testen wesentlich ineffektiver ist als das Schreiben der Tests vor der Implementierung: Zum einen lässt sich Testbarkeit im Nachhinein nur mit Mühe erzwingen, zum anderen können vorab geschriebene Tests das Design lenken. Gerade dieser Vorteil testgetriebener Entwicklung ist jedoch – auch in Projekten mit hoher Testkultur – in vielen Köpfen noch nicht gefestigt.

Wohin geht die testgetriebene Entwicklung?

Nachdem sich die meisten Projekte bisher nur auf Unit-Tests gestützt haben, entdecken viele den Nutzen von System- und Akzeptanztests. Wenn diese Tests aus der Feder des Kunden bzw. der Fachabteilung kommen, kann so eine wichtige Kommunikationslücke zwischen den Domänenexperten, die die Anforderungen kennen, und den Entwicklern, die diese realisieren, geschlossen werden. Hat sich JUnit als De-facto-Standard für Entwicklertests etabliert, so schickt sich derzeit Ward Cunninghams Framework for Integrated Test (FIT) an, das kundenfreundlichste Akzeptanztest-Framework zu werden. Der angezeigte Trend geht hier eindeutig in Richtung des ausführbaren Anforderungsdokuments. Man verbindet effektiv die Anforderung mit ihrem Abnahmetest. Geht die Formulierung dieser Tests der eigentlichen Entwicklung voraus, so besteht durchaus die Möglichkeit, das traditionelle Anforderungsmanagement auf den Kopf zu stellen. Dabei verändert sich die Rolle des Testers. Anstatt den Entwicklern wegen tausender Kleinigkeiten auf die Finger zu klopfen, kann er nun mit dem Kunden dessen Anforderungen in testbare und eindeutige Beispiele überführen. Auch auf das Projektmanagement wirken sich die feature-basierten Akzeptanztests positiv aus, stellt diese Testart doch ein untrügliches Mittel dar, um den tatsächlichen Projektfortschritt messen und damit "managen" zu können.

Wie gut ein Prozess wirklich "sitzt", zeigt sich, wenn der Stresspegel zunimmt. Noch sehen wir häufig, dass Tests weggelassen werden, wenn die Zeit knapp wird. Nur wer seine Reflexe soweit verändert hat, dass er trotz Zeitdruck weiterhin an seinen guten Gewohnheiten festhält, hat sich überzeugt: Testen spart Zeit und Geld. Zum einen führen intensive Unit-Tests zu deutlich reduzierten Fehlerraten. Von einigen XP-Teams wird mittlerweile berichtet, dass ihre Software praktisch fehlerfrei sei. So hätten die Anwender, nachdem sich die Software länger als ein Jahr in Produktion befände, noch keinerlei Fehler entdeckt. Zum anderen ermöglichen erst automatisierte Tests eine schnelle, evolutionäre Anpassung des Designs an neue Anforderungen. Die Testautomatisierung erweist sich damit als eine Kosten sparende und Effizienz steigernde Maßnahme, sobald die initiale Lernkurve überwunden ist. Wie nicht zuletzt durch Toyota im Automobilbau demonstriert: Produktivität und Qualität sind keine widersprüchlichen Ziele.

Die Grundannahme, dass Software nun mal "buggy" ist, wird sich sicher noch eine Zeit lang halten, jedoch mehr und mehr entkräftet werden. Wer effektiv testet, wird seine Software jederzeit ausliefern können, ohne dass die Qualität einknickt. Die Zykluszeiten von einer neuen Anforderung zum produktiven Code werden zukünftig drastisch kürzer werden und dadurch neue Geschäftsmodelle ermöglichen. Entwickelt wird in Zukunft nur noch geschäftswertorientiert: Die Anforderung mit der größten Wertschöpfung kommt zuerst. Inkrementelle Entwicklung wird zum Synonym für inkrementelle Finanzierung. Möglich wird dieser Wandel unter anderem durch automatisierte Tests.

9.2.2005

Tonabnehmer #1: Johannes Link -
Softwaretests mit JUnit

Johannes Link

Zum Auftakt des Tonabnehmers, meiner neuen Audiokolumne zur agilen Softwareentwicklung, sprach ich mit Johannes Link über sein neuestes Buch Softwaretests mit JUnit.

Das Telefoninterview können Sie als MP3 herunterladen oder direkt als Podcast empfangen. (Was ist Podcasting?)

Aus dem Interview erfahren Sie alles Wissenswerte rundum die Testgetriebene Softwareentwicklung. Auf meine Frage, welche Herausforderungen auf uns zukommen, antwortete Johannes: "Das Entscheidende, was wir in den letzten Jahren erlebt haben, ist ein großer Kostendruck auf die Softwareindustrie. [...] Wir müssen herausstellen, dass Testgetriebene Entwicklung nur zu einem kleinen Teil mit Testen und Programmierung zu tun hat, sondern große Auswirkungen auf den Prozess und das Management hat. Denn im Prozess und im Management liegen eben die größten Optimierungspotenziale der Softwareentwicklung. Und dann können wir, wenn wir das rüberbringen, auch vielen, die völlig auf Outsourcing und Offshoring schwören, den Wind aus den Segeln nehmen."

"Wenn ich als Kunde mir einen Dienstleister aussuche, würde ich einen auswählen, der von sich aus automatisierte Testfälle erstellt und die Anforderungen mit mir zusammen in automatisierten Akzeptanztests festhalten will."

26.8.2001

Extreme Programming

Extreme Programming (XP) ist ein agiler Softwareentwicklungsprozess für kleine Teams. Der Prozess ermöglicht, langlebige Software zu erstellen und während der Entwicklung auf vage und sich rasch ändernde Anforderungen zu reagieren. XP-Projekte schaffen ab Tag eins Geschäftswert für den Kunden und lassen sich fortlaufend und außergewöhnlich stark durch den Kunden steuern.

Im Kern beruht XP auf den Werten Kommunikation, Einfachheit, Feedback, Mut, Lernen, Qualität und Respekt. XP erfordert Disziplin und Prinzipientreue. Die beschriebenen XP-Techniken sind jedoch wirklich nur die Startlinie. Das Ziel ist es, den Entwicklungsprozess an die örtlichen Begebenheiten anzupassen und fortlaufend zu verbessern.

Techniken für ein XP-Team

Ein XP-Team besteht aus zwei bis etwa zwölf Programmierern, einem Kunden oder mehreren direkten Anprechpartnern auf Kundenseite und dem Management. Ferner erfordert der Prozess die Rollen des Trainers und Verfolgers. Der Trainer bespricht mit dem Team die diszipliniert einzuhaltenden Techniken und erinnert das Team, wenn es die selbstgewählten Regeln verletzt. Der Verfolger nimmt regelmässig den aktuellen Status und die geleisteten Programmieraufwände auf, um so zuverlässige Geschichtsdaten über das Projekt zu erhalten. Zu beachten ist, daß der Kunde in der Regel weder den Geldgeber noch den wirklichen Endanwender darstellt.

Offene Arbeitsumgebung

Das Team arbeitet zusammen in einem größeren Raum oder eng aneinander grenzenden Räumen. Typischerweise ist der "Kriegsraum" mit Wandtafeln und unzähligen Flipcharts ausgestattet. Die Arbeitstische stehen meist dicht beieinander im Kreis mit den Monitoren nach außen gerichtet und sind so gestaltet, daß zwei Programmierer zusammen bequem an einem Computer arbeiten können.

Kurze Iterationen

Die Entwicklung erfolgt in Perioden von ein bis drei Wochen. Am Ende jeder Iteration steht ein funktionsfähiges, getestetes System mit neuer, für den Kunden wertvoller Funktionalität.

Gemeinsame Sprache

Das Team entwickelt in seiner Arbeit ein gemeinsames Vokabular, um über die Arbeitsweisen und das zu erstellende System diskutieren zu können. Die Kommunikation im Team erfolgt stets offen und ehrlich.

Retrospektiven

Jede Iteration endet damit, in einem Rückblick über die eigenen Arbeitsweisen kritisch zu reflektieren und im Team zu diskutieren, was gut lief und was in Zukunft anders angegangen werden muß. Typischerweise werden aus den Dingen, die während dieser Team-Reviews zur Oberfläche kommen, Regeln generiert, vom Team akzeptiert, auf Poster geschrieben und im Projektraum zur Erinnerung an die Wand geheftet. Ein- oder zweimal jährlich macht das Team für zwei Tage einen gemeinsamen Ausflug, um in einem Offsite-Meeting formal vor- und zurückzublicken.

Tägliches Standup-Meeting

Der Tag beginnt mit einem Meeting, das im Stehen gehalten wird, damit es kurz und lebendig bleibt. Jedes Teammitglied berichtet reihum, an welcher Aufgabe er gestern gearbeitet hat und was er heute machen wird. Probleme werden genannt aber nicht gelöst. Die meisten Teams treffen sich vor der Wandtafel ihrer Iterationsplanung.

Techniken für die Kunden

Benutzergeschichten

Die Kunden halten ihre Anforderungen in Form einfacher Geschichten auf gewöhnlichen Karteikarten fest. Jeder geschriebenen Story-Karte kommt das Versprechen nach, den genauen Funktionsumfang zum rechten Zeitpunkt im Dialog mit den Programmierern zu verfeinern und zu verhandeln.

Iterationsplanung

Jede Iteration beginnt mit einem Planungsmeeting, in dem das Kundenteam seine Geschichten erzählt und mit dem Programmierteam diskutiert. Die Programmierer schätzen den Aufwand grob ab, den sie zur Entwicklung jeder einzelnen Geschichte benötigen werden. Die Kunden wählen in Abhängigkeit der Aufwandsschätzungen den Kartenumfang für die Iteration aus, der ihren Geschäftsgegenwert maximieren würde. Die Programmierer zerlegen die geplanten Geschichten am Flipchart in technische Aufgaben, übernehmen Verantwortung für einzelne Aufgaben und schätzen deren Aufwände vergleichend zu früher erledigten Aufgaben. Aufgrund der genaueren Schätzung der kleinen Aufgaben verpflichten sich die Programmierer auf genau soviele Geschichten, wie sie in der vorhergehenden Iteration entwickeln konnten. Diese Planungsspiele schaffen eine sichere Umgebung, in welcher geschäftliche und technische Verantwortung zuverlässig voneinander getrennt werden.

Anforderungsdefinition im Dialog

Das für die anstehenden Programmieraufgaben nötige Verständnis der Anforderungen wird fortlaufend in der Konversation mit den Kunden geprüft und vertieft. In kurzen Designsessions wird unter Umständen auf eine der Wandtafeln ein wenig UML gemalt oder es werden Szenarien mit Hilfe von CRC-Karten durchgespielt. Während der gesamten Entwicklung dienen die Kunden als direkte Ansprechpartner zur Bewältigung fachlicher Fragen. Die verbleibende Zeit verbringen die Kunden mit dem Schreiben und Ergründen neuer Benutzergeschichten und Akzeptanztests.

Akzeptanztests

Die Kunden spezifizieren während der Iteration funktionale Abnahmekriterien. Typischerweise entwickeln die Programmierer ein kleines Werkzeug, um diese Tests zu kodieren und automatisch auszuführen. Spätestens zum Ende der Iteration müssen die Tests erfüllt sein, um die gewünschte Funktion des Systems zu sichern.

Kurze Releasezyklen

Nach ein bis drei Monaten wird das System an die wirklichen Endanwender ausgeliefert, damit das Kundenteam wichtiges Feedback für die Weiterentwicklung erhält.

Techniken für die Entwicklung

Programmieren in Paaren

Die Programmierer arbeiten stets zu zweit am Code und diskutieren während der Entwicklung intensiv über Entwurfsalternativen. Sie wechseln sich minütlich an der Tastatur ab und rotieren stündlich ihre Programmierpartner. Das Ergebnis ist eine höhere Codequalität, grössere Produktivität und bessere Wissensverbreitung.

Gemeinsame Verantwortlichkeit

Der gesamte Code gehört dem Team. Jedes Paar soll jede Möglichkeit zur Codeverbesserung jederzeit wahrnehmen. Das ist kein Recht sondern eine Pflicht.

Erst Testen

Gewöhnlich wird jede Zeile Code durch einen Testfall motiviert, der zunächst fehlschlägt. Die Unit Tests werden gesammelt, gepflegt und nach jedem Kompilieren ausgeführt.

Design für heute

Jeder Testfall wird auf die einfachst denkbare Weise erfüllt. Es wird keine unnötig komplexe Funktionalität programmiert, die momentan nicht gefordert ist.

Refactoring

Das Design des Systems wird fortlaufend in kleinen, funktionserhaltenden Schritten verbessert. Finden zwei Programmierer Codeteile, die schwer verständlich sind oder unnötig kompliziert erscheinen, verbessern und vereinfachen sie den Code. Sie tun dies in disziplinierter Weise und führen nach jedem Schritt die Unit Tests aus, um keine bestehende Funktion zu zerstören.

Fortlaufende Integration

Das System wird mehrmals täglich durch einen automatisierten Build-Prozess neu gebaut. Der entwickelte Code wird in kleinen Inkrementen und spätestens am Ende des Tages in die Versionsverwaltung eingecheckt und ins bestehende System integriert. Die Unit Tests müssen zur erfolgreichen Integration zu 100% laufen.

Techniken für das Management

Akzeptierte Verantwortung

Das Management schreibt einem XP-Team niemals vor, was es zu tun hat. Stattdessen zeigt der Manager lediglich Probleme auf und läßt die Kunden und Programmierer selbst entscheiden, was zu tun gilt. Dies ist eine große, neue Herausforderung für das Management.

Information durch Metriken

Eine der Hauptaufgaben des Managements ist es, dem Team den Spiegel vorzuhalten und zu zeigen, wo es steht. Dazu gehört unter anderem das Erstellen einfacher Metriken, die den Fortschritt des Teams oder zu lösende Probleme aufzeigen. Es gehört auch dazu, den Teammitgliedern regelmässig in die Augen zu schauen und herauszufinden, wo Hilfe von Nöten ist.

Ausdauerndes Tempo

Softwareprojekte gleichen mehr einem Marathon als einem Sprint. Viele Teams werden immer langsamer bei dem Versuch, schneller zu entwickeln. Überstunden sind keine Lösung für zuviel Arbeit. Wenn Refactorings und Akzeptanztests aufgeschoben werden, muß der Manager dem Team stärker den Rücken freihalten. Wenn Teammitglieder müde und zerschlagen sind, muß der Manager sie nach Hause schicken.

Links

Bücher

Mailinglisten

User's Groups

24.6.2001

Unit Tests mit JUnit

Automatisierte Unit Tests in Java

JUnit ist ein kleines, mächtiges Java-Framework zum Schreiben und Ausführen automatischer Unit Tests. Da die Tests direkt in Java programmiert werden, ist das Testen mit JUnit so einfach wie das Kompilieren. Die Testfälle sind selbstüberprüfend und damit wiederholbar.

Unit Testing ist der Test von Programmeinheiten in Isolation von anderen im Zusammenhang eines Programms benötigten, mitwirkenden Programmeinheiten. Die Größe der unabhängig getesteten Einheit kann dabei von einzelnen Methoden über Klassen bis hin zu Komponenten reichen.

JUnit-Logo
Download JUnit 3.8.1

Download und Installation

JUnit ist als Open Source Software unter der IBM Public License veröffentlicht. Die aktuelle Version (momentan JUnit 3.8) können Sie von SourceForge beziehen: http://sourceforge.net/projects/junit/. Entsprechende Frameworks sind für nahezu alle gängigen Programmiersprachen frei erhältlich: http://www.xprogramming.com/software.htm.

Das JUnit-Framework kommt in einem JAR-Archiv namens junit.jar verpackt. Die vollständige Distribution besteht gegenwärtig aus einem ZIP-Archiv, in dem neben junit.jar auch dessen Quelltexte, dessen Tests, einige Beispiele, die JavaDoc-Dokumentation, die FAQ, ein Kochbuch und zwei sehr lesenswerte Artikel aus dem amerikanischen Java Report beiliegen. Machen Sie sich mal ruhig mit den Beigaben vertraut. Es lohnt sich.

Zur Installation entpacken Sie bitte das ZIP-Archiv und übernehmen Sie junit.jar in Ihren CLASSPATH. Fertig!

Wie Sie JUnit mit praktisch jeder gängigen Java-Entwicklungsumgebung verwenden, können Sie unter http://www.junit.org/IDEs.htm nachlesen. Für den Anfang reicht es jedoch vollkommen aus, wenn Ihr Compiler sich nicht darüber beschwert, die JUnit-Klassen in seinem Klassenpfad zu vermissen. Den Test dafür werden wir in zwei Minuten machen.

Ein erstes Beispiel

Wir wollen eine Klasse Euro ins Leben testen, die Euro-Beträge akurater repräsentieren kann als der bloße Java-Typ double, den wir bisher (im Artikel "XP über die Schulter geschaut") verwendet haben. Anhand dieses kleinen Beispiels können Sie den prinzipiellen Aufbau eines Testfalls kennenlernen und Ihren ersten kleinen Erfolg mit JUnit feiern. In der JUnit-Dokumentation werden Sie ein ähnliches Beispiel finden. Ich habe mich aus verschiedenen Gründen dafür entschieden, auf vertrautem Terrain loszuwandern. Michael Wein schrieb mir, unbedingt darauf hinzuweisen, daß weder die Money Klasse der JUnit-Distribution noch die in diesem Beispiel entwickelte Klasse den Robustheitsanforderungen der Finanzwirtschaft genügt. Wenn's drauf ankommt, benutzen Sie besser java.lang.BigDecimal oder ähnliches.

Die Klasse Euro stellt Wertobjekte für geldliche Beträge dar. Das heißt, das Objekt wird eindeutig durch seinen Wert beschrieben. Sie können ein Wertobjekt nicht verändern. Wenn Sie ein Wertobjekt manipulieren, erhalten Sie ein anderes Objekt mit dem neuen Wert zurück.

Einen Test für eine Klasse zu schreiben bietet immer auch eine gute Gelegenheit, über ihre öffentliche Schnittstelle nachzudenken. Was also erwarten wir von unserer Klasse? Nun, zunächst möchten wir sicherlich ein Euro Objekt instanzieren können, indem wir dem Konstruktor einen Geldbetrag übergeben. Wenn wir ein Euro Objekt zu einem anderen hinzuaddieren, möchten wir, daß unsere Klasse mit einem neuen Euro Objekt antwortet, das die Summe der beiden Beträge enthält. Euro-Beträge unterliegen dabei einer besonderen Auflösung in ihrer numerischen Repräsentation. Zum Beispiel erwarten wir, daß auf den Cent genau gerundet wird und daß 100 Cents einen Euro ergeben. Aber fangen wir mit der einfachen Datenhaltung an. Hier sehen Sie den ersten Test:


import junit.framework.*;

public class EuroTest extends TestCase {

  public EuroTest(String name) {
    super(name);
  }

  public void testAmount() {
    Euro two = new Euro(2.00);
    assertTrue(2.00 == two.getAmount());
  }

  public static void main(String[] args) {
    junit.swingui.TestRunner.run(EuroTest.class);
  }
}

JUnit ist wirklich einfach zu verwenden. Es wird nicht schwerer.

Anatomie eines Testfalls

Sie erkennen, daß wir unsere Tests getrennt von der Klasse Euro in einer Klasse namens EuroTest definieren. Um unsere Testklasse in JUnit einzubinden, leiten wir sie von dessen Framework-Basisklasse junit.framework.TestCase ab.

Jede Testklasse erhält einen Konstruktor für den Namen des auszuführenden Testfalls. Das Test-Framework instanziert für jeden Testfall ein neues Exemplar dieser Klasse, wie wir später bei der Betrachtung des Lebenszyklus eines Testfalls sehen werden.

Unser erster Testfall verbirgt sich hinter der Methode testAmount. JUnit erkennt diese Methode dadurch als Testfall, daß sie der Konvention des Signaturmusters public void test...() folgt. Testfallmethoden dieses Musters kann das Framework via Java Reflection zu einer Suite von Tests zusammenfassen und voneinander isoliert ausführen.

In diesem Testfall erzeugen wir uns zunächst ein Objekt mit dem Wert "zwei Euro". Der eigentliche Test erfolgt mit dem Aufruf der assertTrue Methode, die unsere Testklasse aus ihrer Oberklasse erbt. Das assertTrue Statement formuliert eine Annahme, die JUnit automatisch für uns verifizieren wird.

Die assertTrue Methode dient dazu, eine Bedingung zu testen. Als Argument akzeptiert sie einen boolschen Wert bzw. einen Ausdruck, der einen solchen liefert. Der Test ist erfolgreich, wenn die Bedingung erfüllt ist, d.h. der Ausdruck zu true ausgewertet werden konnte. Ist die Bedingung nicht erfüllt, d.h. false, protokolliert JUnit einen Testfehler. In diesem Beispiel testen wir, daß unser Objekt two als Ergebnis der getAmount Operation die erwarteten "zwei Euro" antwortet.

Durch Aufruf der main Methode können wir unseren ersten JUnit-Test ausführen. Der junit.swingui.TestRunner stellt eine grafische Oberfläche auf Basis von Java Swing dar, um Unit Tests kontrolliert ablaufen zu lassen. Mit der JUnit-Oberfläche werden wir uns noch zu einem späteren Zeitpunkt beschäftigen. Momentan läßt sich unsere Testklasse noch nicht übersetzen. Sie beschwert sich noch darüber, keine Klasse Euro zu kennen.

Erst testen, dann programmieren

Im nächsten Artikel werden wir der Programmiertechnik Testgetriebene Entwicklung begegnen. Dort werden Sie sehen, wie wir ein Programm inkrementell in kleinen Schritten entwickeln können, indem wir grundsätzlich immer zuerst einen Test schreiben, bevor wir die Klasse weiter schreiben, die diesen Test dann erfüllt. Natürlich können Sie mit JUnit aber auch Tests für schon bestehenden Code schreiben, obwohl Klassen oft schlecht testbar sind, wenn sie nicht von vornherein mit Testbarkeit im Hinterköpfchen entworfen wurden.

Tipp
Schreiben Sie Tests, bevor Sie den Code schreiben, der diese Tests erfüllen soll, damit Sie sicherstellen, daß Ihr Code einfach zu testen ist.

Den Prozess der testgetriebenen Programmierung werden Sie später noch detailliert kennenlernen. Zu diesem Zeitpunkt wollen wir uns allein auf das Testen mit JUnit konzentrieren. Lassen Sie uns deshalb direkt zur Implementation der Klasse Euro übergehen. Was müssen wir hinschreiben, um den Test zu erfüllen?


JUnit-IkonGrüner JUnit-Balken

public class Euro {
  private double amount;

  public Euro(double amount) {
    this.amount = amount;
  }

  public double getAmount() {
    return this.amount;
  }
}

Wenn Sie den Code kompilieren und EuroTest ausführen (entweder aus Ihrer IDE heraus oder via java EuroTest auf der Kommandozeile), sollte ein neues Fensterchen mit JUnit's grafischer Oberfläche und darin ein freundlicher, hellgrüner Balken erscheinen. Erfolg! Der Test läuft.

JUnit-Runner mit grünem Balken

Klicken Sie ruhig noch einmal auf den Run Knopf. Dieser Knopf macht ganz klar süchtig. Sie werden sich später ganz sicher dabei ertappen, die Tests zwei- oder dreimal nacheinander auszuführen nur wegen des zusätzlichen Vertrauens, das Ihnen der grüne Balken schenkt. Es ist wirklich ein unbeschreiblich großartiges Gefühl, wenn hunderte von Tests ablaufen und sich der Fortschrittbalken dabei von links nach rechts mit Grün füllt. So ein Gefühl gibt Mut, die "Softness" von Software auf's Neue zu entdecken.

Das JUnit-Framework

Im zweiten Teil des Artikels möchte ich Ihnen den Aufbau von JUnit detaillierter vorstellen. Ich könnte Ihnen empfehlen, sich den Code von JUnit genau anzusehen. Wenn Sie so sind wie ich, dann lernen Sie ein Framework am besten, indem Sie dessen Code studieren. JUnit ist ein gutes Beispiel für ein kleines, fokussiertes Framework mit einer hohen Dichte säuberlich verwendeter Entwurfsmuster. JUnit ist vor allem deshalb ein gutes Beispiel, weil es inklusive seines eigenen Testcode kommt. Versprechen Sie sich selbst bitte, daß Sie den Code lesen werden. Wenigstens irgendwann...

Tipp
Eines guten Tages sollten Sie sich die Zeit nehmen und den junit.framework Code studieren und um Ihre eigenen Anforderungen erweitern.

Ich werde im folgenden die JUnit-Klassen vorstellen, mit denen Sie am meisten in Berührung kommen werden. Wenn Sie keine Mühe gescheut haben und sich tatsächlich angeschaut haben, was Kent Beck und Erich Gamma ursprünglich auf einem Flug zur OOPSLA-Konferenz zusammen programmiert haben, wissen Sie schon, wovon ich sprechen werde. Was Sie wahrscheinlich jedoch noch nicht wissen ist, wie die einzelnen Puzzlestücke zum Ganzen zusammengelegt werden. Dazu wenden wir uns hin und wieder unserer Euro Klasse zu.

"Assert"

Wie testen wir mit JUnit?

JUnit erlaubt uns, Werte und Bedingungen zu testen, die jeweils erfüllt sein müssen, damit der Test okay ist. Die Klasse Assert definiert dazu eine Menge von assert Methoden, die unsere Testklassen aus JUnit erben und mit denen wir in unseren Testfällen eine Reihe unterschiedlicher Behauptungen über den zu testenden Code aufstellen können:

assertTrue(boolean condition) verifiziert, ob eine Bedingung wahr ist.
Beispiele:

assertTrue(theJungleBook.isChildrensMovie());
assertTrue(40 == xpProgrammer.workingHours() * days);
assertEquals(Object expected, Object actual) verifiziert, ob zwei Objekte gleich sind. Der Vergleich der Objekte erfolgt in JUnit über die equals Methode.
Beispiele:

assertEquals("foobar", "foo" + "bar");
assertEquals(new Euro(2), Movie.getCharge(1));

Der Vorteil dieser und der folgenden assertEquals Varianten gegenüber dem Test mit assertTrue liegt darin, daß JUnit Ihnen nützliche zusätzliche Informationen bieten kann, wenn der Test tatsächlich fehlschlägt. JUnit benutzt in diesem Fall die toString Repräsentation Ihres Objekts, um den erwarteten Wert auszugeben.

assertEquals(int expected, int actual) verifiziert, ob zwei ganze Zahlen gleich sind. Der Vergleich erfolgt für die primitiven Java-Typen über den == Operator.
Beispiele:

assertEquals(9, customer.getFrequentRenterPoints());
assertEquals(40, xpProgrammer.workingHours() * days);
assertEquals(double expected, double actual, double delta) verifiziert, ob zwei Fließkommazahlen gleich sind. Da Fließkommazahlen nicht mit unendlicher Genauigkeit verglichen werden können, wird zusätzlich eine Toleranz erwartet.
Beispiele:

assertEquals(3.1415, Math.pi(), 1e-4);
assertNull(Object object) verifiziert, ob eine Objektreferenz null ist.
Beispiele:

assertNull(hashMap.get(key));
assertNotNull(Object object) verifiziert, ob eine Objektreferenz nicht null ist.
Beispiele:

assertNotNull(httpRequest.getParameter("action"));
assertSame(Object expected, Object actual) verifiziert, ob zwei Referenzen auf das gleiche Objekt verweisen.
Beispiele:

assertSame(bar, hashMap.put("foo", bar).get("foo"));

Die assertEquals Methode ist neben den oben aufgeführten Argumenttypen auch für die primitiven Datentypen float, long, boolean, byte, char und short überladen. Der Phantasie beim Testen sind also keine Grenzen gesetzt.

"AssertionFailedError"

Was passiert, wenn ein Test fehlschlägt?

Die im Testcode durch assert Anweisungen kodierten Behauptungen werden von der Klasse Assert automatisch verifiziert. Im Fehlerfall bricht JUnit den laufenden Testfall sofort mit dem Fehler AssertionFailedError ab.

Um den Test des centweisen Rundens nachzuholen, könnten wir zum Beispiel folgenden Testfall schreiben:


public class EuroTest...
  public void testRounding() {
    Euro roundedTwo = new Euro(1.995);
    assertTrue(2.00 == roundedTwo.getAmount());
  }
}

Der Test läuft natürlich nicht, weil unsere Klasse noch kein Konzept für das Runden hat. Der Fortschrittbalken verfärbt sich rot.

JUnit-Runner mit rotem Balken

Die Oberfläche teilt uns mit, daß während der Ausführung unseres neuen Testfalls ein Fehler (Failure) aufgetreten ist, der im unteren Textfenster zur Rückmeldung protokolliert wird:


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError
at EuroTest.testRounding(EuroTest.java:16)

"Der Testfall testRounding schlug fehl", sagt uns JUnit. "Werfen Sie einen Blick auf Zeile 16 der Klasse EuroTest."

Wenn Ihnen diese Fehlermeldung nicht ausreichen sollte, bietet Ihnen JUnit für alle assert Methoden an, einen Erklärungstext zu tippen, der im Fehlerfall mitprotokolliert wird. Ich kann Ihnen nur raten, diese Variante zur Dokumentation der Tests auszunutzen, ganz besonders bei langen Ketten von assert Anweisungen.


assertTrue("amount not rounded", 2.00 == roundedTwo.getAmount());
Tipp
Für die Tests gilt der gleiche Qualitätsanspruch wie für den übrigen Code: selbstdokumentierend, kein duplizierter Code und möglichst einfach.

Der Unterschied liegt allein im ausdrucksstärkeren Begleittext, den JUnit im Fehlerfall zusätzlich ausgibt statt nur dem Namen des fehlgeschlagenen Testfalls und der Zeilennummer des aufgetretenen Fehlers:


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  amount not rounded
at EuroTest.testRounding(EuroTest.java:16)

Das Beste wäre in diesem Fall natürlich gewesen, wir hätten gleich die bequemere assertEquals Variante verwendet:


assertEquals("rounded amount",
             2.00, roundedTwo.getAmount(), 0.001);

In diesem Fall kann JUnit den erwarteten gegen den tatsächlichen Wert testen und unser Begleittext darf etwas kürzer ausfallen:


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  rounded amount expected:<2.0> but was:<1.995>
at EuroTest.testRounding(EuroTest.java:16)

Zum Vergleich der intern verwendeten Fließkomma-Repräsentation tolerieren wir in diesem Beispiel ein Delta von 0.001. Das bedeutet, daß die verglichenen Werte sich nicht um einen Betrag unterscheiden dürfen, der grösser ist als ein Tausendstel Euro.

Mittlerweile finde ich den Anblick des roten Fortschrittbalkens aber schon etwas unbefriedigend, Sie nicht auch? Vom Testen infiziert zu sein bedeutet, nicht eher locker zu lassen, bis die Tests wieder auf grün sind. Die Frage ist: Wollen Sie den Konstruktor selbst anpassen oder wollen Sie sehen, was ich hingetippt hätte? Voilà!


JUnit-IkonGrüner JUnit-Balken

public class Euro {
  private long cents;
    
  public Euro(double euro) {
    cents = Math.round(euro * 100.0);
  }
    
  public double getAmount() {
    return cents / 100.0;
  }
}

"TestCase"

Wie gruppieren wir Testfälle um eine gemeinsame Menge von Testobjekten?

Ein Testfall sieht in der Regel so aus, daß eine bestimmte Konfiguration von Objekten aufgebaut wird, gegen die der Test läuft. Diese Menge von Testobjekten wird auch als Test-Fixture bezeichnet. Pro Testfallmethode wird meist nur eine bestimmte Operation und oft sogar nur eine bestimmte Situation im Verhalten der Fixture getestet.

Tipp
Testen Sie pro Testfallmethode nicht zuviel, nur eine ganz bestimmte Funktion und dabei immer nur eine interessante Randbedingung zurzeit.

Schauen wir uns dazu ein Beispiel an:


public class EuroTest...
  public void testAdding() {
    Euro two = new Euro(2.00);
    Euro three = two.add(new Euro(1.00));
    assertEquals("sum", 3.00, three.getAmount(), 0.001);
    assertEquals("two", 2.00, two.getAmount(), 0.001);
  }
}

In diesem Testfall erzeugen wir uns ein Euro Objekt und addieren es zu einem anderen. Das Resultat der Addition soll ein neues Euro Objekt sein, dessen Wert die Summe von "einem Euro" und "zwei Euro", also "drei Euro" beträgt. Unser ursprüngliches "zwei Euro" Objekt soll durch die Addition nicht verändert werden. Wenn wir diesen Test erfüllen sollten, würden wir vielleicht folgendes programmieren:


JUnit-IkonGrüner JUnit-Balken

public class Euro...
  private Euro(long cents) {
    this.cents = cents;
  }
    
  public Euro add(Euro other) {
    return new Euro(this.cents + other.cents);
  }
}

Der JUnit-Balken ist wieder zurück auf grün, aber was passiert eigentlich, wenn wir ein Euro Objekt mit negativem Betrag definieren würden und damit die add Operation aufriefen? Wäre damit noch die Intention des Addierens ausgedrückt oder sollte es dann besser subtract heissen? Verschieben wir die Antwort auf diese Frage für ein paar Momente. Damit wir die Idee aber nicht verlieren, notieren wir auf einer Karteikarte "Wie geht add mit negativen Beträgen um?"

Tipp
Halten Sie Ideen sofort fest, verlieren Sie sie nicht wieder.

Wir waren dabei stehengeblieben, eine geeignete Test-Fixture herauszuziehen. Lassen Sie uns dazu noch einmal einen kurzen Blick auf unsere bisherige EuroTest Klasse werfen:


import junit.framework.*;

public class EuroTest extends TestCase {

  public EuroTest(String name) {
    super(name);
  }

  public void testAmount() {
    Euro two = new Euro(2.00);
    assertTrue(2.00 == two.getAmount());
  }

  public void testRounding() {
    Euro roundedTwo = new Euro(1.995);
    assertEquals("rounded amount",
                 2.00, roundedTwo.getAmount(), 0.001);
  }

  public void testAdding() {
    Euro two = new Euro(2.00);
    Euro three = two.add(new Euro(1.00));
    assertEquals("sum", 3.00, three.getAmount(), 0.001);
    assertEquals("two", 2.00, two.getAmount(), 0.001);
  }

  public static void main(String[] args) {
    junit.swingui.TestRunner.run(EuroTest.class);
  }
}

Da sich der Code zum Aufbau einer bestimmten Testumgebung häufig wiederholt, sollten Sie alle Testfälle, die gegen die gleiche Menge von Testobjekten laufen, unter dem Dach einer Testklasse zusammenfassen. Mit dem Code, der in den Testfällen jeweils das "zwei Euro" Objekt kreiert, haben wir duplizierten Code dieser Art, den wir nun in die Fixture herausziehen wollen. Der Gewinn wird aus diesem ersten Beispiel nicht unmittelbar deutlich werden. Später werden wir Testklassen mit komplexeren Testumgebungen begegnen, in denen die Testfälle durch diesen Schritt stark vereinfacht werden können.

Allgemein gilt, daß alle Testfälle einer Testklasse von der gemeinsamen Fixture Gebrauch machen sollten. Hat eine Testfallmethode keine Verwendung für die Fixture-Objekte, so ist dies meist ein guter Indiz dafür, daß die Methode auf eine andere Testklasse verschoben werden will. Generell sollten Testklassen um die Fixture organisiert werden, nicht um die getestete Klasse. Somit kann es durchaus vorkommen, daß zu einer Klasse mehrere korrespondierende Testklassen existieren, von denen jede ihre individuelle Test-Fixture besitzt.

Tipp
Organisieren Sie Testklassen um eine gemeinsame Fixture von Testobjekten, nicht um die getestete Klasse.

Schauen wir uns unseren Testcode unter diesen Gesichtspunkten an. Im ersten und dritten Testfall verwenden wir das "zwei Euro" Objekt. Wir könnten damit anfangen, dieses Objekt in die Test-Fixture zu übernehmen. Der zweite Testfall macht eine Ausnahme. Lassen Sie uns auch hier später entscheiden, wie wir damit umgehen möchten. Wir nehmen also eine weitere Karteikarte, notieren einfach "EuroTest testRounding() refaktorisieren" und setzen uns auf die Karte drauf.

Damit fehlerhafte Testfälle nicht andere Testfälle beeinflussen können, wird die Test-Fixture für jeden Testfall neu initialisiert. Dazu müssen Sie die Objekte der Test-Fixture zu Instanzvariablen Ihrer TestCase Unterklasse erklären. Sie haben dann die Möglichkeit, zwei Einschubmethoden des Frameworks wie folgt zu überschreiben: In der Methode setUp initialisieren Sie diese Instanzvariablen, um so eine definierte Testumgebung zu schaffen. In der Methode tearDown geben Sie wertvolle Testressourcen wie zum Beispiel Datenbank- oder Netzwerkverbindungen wieder frei, die Sie zuvor in setUp in Anspruch genommen haben. JUnit behandelt Ihre Test-Fixture dann wie folgt: Die setUp Methode wird gerufen, bevor ein Testfall ausgeführt wird. Die tearDown Methode wird gerufen, nachdem ein Testfall ausgeführt wurde. Hier sehen Sie unsere Testklasse mit extrahierter Test-Fixture:


JUnit-IkonGrüner JUnit-Balken

import junit.framework.*;

public class EuroTest extends TestCase {

  private Euro two;

  public EuroTest(String name) {
    super(name);
  }

  protected void setUp() {
    two = new Euro(2.00);
  }

  protected void tearDown() {
  }

  public void testAmount() {
    assertTrue(2.00 == two.getAmount());
  }

  public void testRounding() {
    Euro roundedTwo = new Euro(1.995);
    assertEquals("rounded amount",
                 2.00, roundedTwo.getAmount(), 0.001);
  }

  public void testAdding() {
    Euro three = two.add(new Euro(1.00));
    assertEquals("sum", 3.00, three.getAmount(), 0.001);
    assertEquals("two", 2.00, two.getAmount(), 0.001);
  }

  public static void main(String[] args) {
    junit.swingui.TestRunner.run(EuroTest.class);
  }
}

Bitte beachten Sie, daß Sie Fixture-Variablen in der setUp Phase initialisieren, nicht im Deklarationsteil noch im Konstruktor der Testfallklasse. Bitte beachten Sie ferner, daß wir die tearDown Methode in diesem Fall nicht hätten definieren müssen, da wir keine Testressourcen freizugeben haben. Ich habe sie hier lediglich aus Gründen der Vollständigkeit stehen gelassen und damit Sie ihr schon einmal begegnet sind.

Lebenszyklus eines Testfalls

Was passiert, wenn JUnit die Tests dieser Klasse ausführt?

  1. Das Test-Framework durchsucht die Testklasse mit Hilfe des Reflection API nach öffentlichen Methoden, die mit test beginnen und weder Parameter noch Rückgabewert besitzen.
  2. JUnit sammelt diese Testfallmethoden in einer Testsuite und führt sie voneinander isoliert aus. Die Reihenfolge, in der Testfallmethoden vom Framework gerufen werden, ist dabei prinzipiell undefiniert.
  3. Damit zwischen einzelnen Testläufen keine Seiteneffekte entstehen, erzeugt JUnit für jeden Testfall ein neues Exemplar der Testklasse und damit eine frische Test-Fixture.
  4. Der Lebenszyklus dieses Exemplars ist so gestaltet, daß vor der Ausführung eines Testfalls jeweils die setUp Methode aufgerufen wird, sofern diese in der Unterklasse redefiniert wurde.
  5. Anschliessend wird eine der test... Methoden ausgeführt.
  6. Nach der Ausführung des Testfalls ruft das Framework die tearDown Methode, falls diese redefiniert wurde, und überläßt das EuroTest Objekt dann der Speicherbereinigung.
  7. Dieser Zyklus wird vereinfacht erklärt ab Schritt 3 solange wiederholt, bis alle Testfälle jeweils einmal ausgeführt wurden.

Wenn wir die Sequenz mit einem Profiler aufzeichnen würden, in der JUnit unsere Testklasse benutzt, ergibt sich hier beispielsweise folgende Aufrufreihenfolge:

  1. new EuroTest("testAdding")
  2. setUp()
  3. testAdding()
  4. tearDown()
  5. new EuroTest("testAmount")
  6. setUp()
  7. testAmount()
  8. tearDown()
  9. new EuroTest("testRounding")
  10. setUp()
  11. testRounding()
  12. tearDown()
Tipp
Zum effektiven Testen müssen Testfälle isoliert voneinander ausführbar sein. Treffen Sie deshalb keine Annahmen über die Reihenfolge, in der Testfälle ausgeführt werden. Führen Sie voneinander abhängige Tests stattdessen gemeinsam in einem Testfall aus.

Wir sind jetzt an einer Stelle angelangt, an der es lohnen könnte, das grössere Bild zu betrachten. In UML läßt sich der Sachverhalt in etwa wie folgt darstellen:

TestCase-Klassenhierarchie
Assert
testet Werte und Bedingungen
TestCase
isoliert eine Reihe von Testfällen um eine gemeinsame Fixture von Testobjekten
EuroTest
testet das Verhalten unserer entwickelten Euro Klasse

(Mit dem Wechsel von der D-Mark zum Euro wurde auch unsere Klasse umgetauft. Das Diagramm zeigt noch den alten Namen, sorry.)

"TestSuite"

Wie führen wir eine Reihe von Tests zusammen aus?

Unser Ziel ist es, den gesamten Testprozess so weit zu automatisieren, daß wir den Test ohne manuellen Eingriff wiederholbar durchführen können. Wichtig ist schließlich, die Unit Tests möglichst häufig auszuführen, idealerweise nach jedem Kompilieren . Nur so erhalten wir unmittelbares Feedback darüber, wann unser Code zu funktionieren beginnt und wann er zu funktionieren aufhört. Wenn wir also immer nur winzig kleine Bissen vom Problemkuchen nehmen, werden wir uns nie länger als 1-3 Minuten mit Programmieren aufhalten, ohne grünes Licht zum Weiterprogrammieren einzuholen. Eher selten werden wir dabei nur einzelne Tests ausführen wollen. Meistens ist es schlau, alle gesammelten Unit Tests in einem Testlauf auszuführen, um ungewollten Seiteneffekten frühzeitig zu begegnen.

Tipp
Führen Sie möglichst nach jedem erfolgreichen Kompiliervorgang alle gesammelten Unit Tests aus.

Mit JUnit können wir beliebig viele Tests in einer Testsuite zusammenfassen und gemeinsam ausführen. Dazu verlangt JUnit von uns, daß wir in einer statischen suite Methode definieren, welche Tests zusammen ausgeführt werden sollen. Eine Suite von Tests wird dabei durch ein TestSuite Objekt definiert, dem wir beliebig viele Tests und selbst andere Testsuiten hinzufügen können. Auf welcher Klasse Sie diese suite Methode definieren ist nebensächlich. In den meisten Fällen werden Sie jedoch spezielle Klassen allein dafür definieren wollen, um solche Testsuiten zu repräsentieren. Nehmen wir beispielsweise alle bisher bestehenden Testfälle (auch jene aus dem "XP über die Schulter geschaut" Artikel), würde unsere Testsuiteklasse folgende Gestalt annehmen:


import junit.framework.*;

public class AllTests {
  public static Test suite() {
    TestSuite suite = new TestSuite();
    suite.addTestSuite(CustomerTest.class);
    suite.addTestSuite(EuroTest.class);
    suite.addTestSuite(MovieTest.class);
    return suite;
  }
}

Alles, was Sie in JUnit zu tun haben, um eine Testsuite zu definieren, ist ein TestSuite Exemplar zu bilden und mittels der addTestSuite Methode verschiedene Testfallklassen hinzuzufügen. Jede Testfallklasse definiert implizit eine eigene suite Methode, in der alle Testfallmethoden eingebunden werden, die in der betreffenden Klasse definiert wurden. JUnit erledigt diesen Teil für Sie automatisch mittels Reflection.

Sie werden vielleicht über den Typ des Rückgabewerts der suite Methode verwundert sein. Warum Test und nicht TestSuite? Nun, hier begegnen wir erstmalig JUnit's Test Interface, das sowohl von TestCase als auch von TestSuite implementiert wird. Dieses Entwurfsmuster ist auch als Kompositum (engl. Composite) bekannt. Es erlaubt genau, daß wir beliebig viele TestCase und TestSuite Objekte zu einer umfassenden Testsuite-Hierarchie kombinieren können. In UML ausgedrückt:

TestSuite-Composite-Pattern
Test
abstrahiert von Testfällen und Testsuiten
TestSuite
führt eine Reihe von Tests zusammen aus

Momentan liegen alle unsere Klassen noch im Default-Package. Der Regelfall ist natürlich, daß unsere Klassen in verschiedenen Paketen stecken. Wir werden die Klassen später auf geeignete Klassenpakete verteilen. In einem solchen Fall müssen wir das Klassenpaket beim Bündeln der Testsuite entweder mitspezifizieren oder importieren:


suite.addTestSuite(common.Contract.class);

In vielen Fällen ist es praktisch, pro Package eine Testsuiteklasse zu definieren. Schon seit JUnit 1.0 ist es geläufig, Testsuiteklassen mit dem Namen AllTests zu benennen. Nach diesem Muster können Sie Hierarchien von Hierarchien von Testsuiten bilden:


suite.addTest(database.AllTests.suite());

Bitte beachten Sie, daß wir die main Methode von der EuroTest Testfallklasse in die AllTests Testsuiteklasse verschieben sollten, da sie hier besser aufgehoben ist:


public class AllTests...
  public static void main(String[] args) {
    junit.swingui.TestRunner.run(AllTests.class);
  }
}

Wenn Sie die Testsuite aller Unit Tests ausführen möchten, tippen Sie bitte java AllTests oder führen die Klasse innerhalb Ihrer Java IDE aus. In jedem Fall sollte sich die bekannte JUnit-Oberfläche auftun.

Abschließend...

Im dritten und letzten Teil dieses Artikels wollen wir auf die beiden Karteikarten zurückkommen, die wir im Zuge unserer Programmierepisode zurückgelassen haben.

Erinnern Sie sich noch an die Karte "EuroTest testRounding() refaktorisieren"? Wir hatten uns vorgenommen, uns den Testfall testRounding noch einmal vorzuknöpfen, weil dieser Testfall vom üblichen Testmuster der Test-Fixture abzuweichen schien. Bedeutet das nun wirklich, wir müssten extra für diesen Testfall eine neue Testfallklasse bilden? Nein, wir können guten Gebrauch der Test-Fixture machen, wenn wir unseren Testfall geeignet umschreiben:


public class EuroTest...
  public void testRounding() {
    Euro roundedTwo = new Euro(1.995);
    assertEquals("rounded amount", two, roundedTwo);
  }
}

Für ein richtiges Wertobjekt gehört es sich ohnehin, daß wir zwei Objekte mittels der equals Methode auf Gleichheit testen können. Lassen Sie uns diese Intention deshalb auch in unserem Code ausdrücken:


public class EuroTest...
  public void testAdding() {
    Euro three = two.add(new Euro(1.00));
    assertEquals("sum", new Euro(3.00), three);
    assertEquals("two", new Euro(2.00), two);
  }
}

Aber warten Sie! Unsere Tests fangen an, sich zu beschweren:


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  sum expected:<Euro@8b456289> but was:<Euro@8b496289>
at EuroTest.testAdding(EuroTest.java:30)

junit.framework.AssertionFailedError:
  rounded amount expected:<Euro@18d18634> but was:<Euro@19318634>
at EuroTest.testRounding(EuroTest.java:24)

Das ist gut, daß uns JUnit sagt, daß was nicht stimmt! Aber was geht hier vor? Die Ausgabe der Objektreferenzen, die Java standardmässig als Resultat der toString Methode liefert, hilft uns wenig weiter. Überschreiben wir also mal das Standardverhalten mit einer ausdrucksstärkeren String-Repräsentation:


public class Euro...
  public String toString() {
    return "" + getAmount();
  }
}

Hey, was ist das? Programmieren, ohne einen Test dafür zu haben? Tja, nun, diese Methode ist so einfach, daß ein Test daran meines Erachtens verschwendet wäre.

Tipp
Beobachten Sie den Return-on-Investment, wenn Sie Tests schreiben. Schreiben Sie nur Tests der Art, die ihren Aufwand wert sind.

Testen wir jetzt, erhalten wir folgende Rückmeldungen:


JUnit-IkonRoter JUnit-Balken

junit.framework.AssertionFailedError:
  sum expected:<3.0> but was:<3.0>
at EuroTest.testAdding(EuroTest.java:30)

junit.framework.AssertionFailedError:
  rounded amount expected:<2.0> but was:<2.0>
at EuroTest.testRounding(EuroTest.java:24)

Hmmm, seltsam, aber doch kristallklar: Die assertEquals Anweisung in Zeile 30 unseres Tests greift intern auf die equals Methode zurück und diese vergleicht zwei Objekte standardgemäß anhand ihrer Referenzen. Damit unser Euro Objekt als wirkliches Wertobjekt und nicht als Referenzobjekt behandelt wird, müssen wir die equals Methode geeignet überschreiben. Aber zunächst definieren wir für das gewünschte Verhalten einen weiteren Test:


public class EuroTest...
  public void testEquality() {
    assertEquals(two, two);
    assertEquals(two, new Euro(2.00));
    assertEquals(new Euro(2.00), two);

    assertTrue(!two.equals(new Euro(0)));
    assertTrue(!two.equals(null));
    assertTrue(!two.equals(new Object()));
  }
}

Die Implementation der equals Methode sieht in etwa wie folgt aus:


JUnit-IkonGrüner JUnit-Balken

public class Euro...
  public boolean equals(Object o) {
    if (o == null || !o.getClass().equals(this.getClass())) {
      return false;
    }

    Euro other = (Euro) o;
    return this.cents == other.cents;
  }
}

Nochmal nachdenken: Der Java-Standard erwartet von uns, daß wir mit der equals Methode auch gleichzeitig die hashCode Methode redefinieren:


public class EuroTest...
  public void testHashCode() {
    assertTrue(two.hashCode() == two.hashCode());
    assertTrue(two.hashCode() == new Euro(2.00).hashCode());
  }
}

Also:


JUnit-IkonGrüner JUnit-Balken

public class Euro...
  public int hashCode() {
    return (int) cents;
  }
}

Puhhh... Sie bemerken, daß wir hier zusätzlichen Code schreiben, nur um unsere Klassen zu testen. Das ist interessanterweise nicht mal schlimm. Im Gegenteil, wir werden später sehen, wie das testgetriebene Programmieren unsere Klassen dahin lenkt, daß sie testbarer werden.

Tipp
Spendieren Sie Ihren Klassen ruhig zusätzliche öffentliche Methoden, wenn sie dadurch leichter getestet werden können.

Wow, wir haben zu diesem Zeitpunkt zehn Tests(!) Damit ist die erste Schallmauer durchbrochen. Sicherlich können wir immer mehr Tests schreiben... Wichtig ist, im Auge zu behalten, welche Tests ihren Aufwand wert waren und welche nicht. Die Milchmädchenrechnung geht so:

Tipp
Schreiben Sie nur Tests für solchen Code, der unter Umständen fehlschlagen könnte.

Testen von Exceptions

Wie testen wir, ob eine Exception folgerichtig geworfen wird?

Die zweite Karte fragt, "Wie geht add mit negativen Beträgen um?" Nun, sicherlich ist es schon nicht sinnvoll, negative double Werte in den Konstruktor der Klasse geben zu dürfen. Wir sollten deshalb die Vorbedingung des Konstruktors spezifizieren. Normalerweise würde ich nicht empfehlen, Tests für Vorbedingungen zu schreiben, doch möchte ich an dieser Stelle eine Ausnahme machen, weil Sie so mal sehen, wie Sie mit JUnit Exceptions testen können.


public class EuroTest...
  public void testNegativeAmount() {
    try {
      final double NEGATIVE_AMOUNT = -2.00;
      new Euro(NEGATIVE_AMOUNT);
      fail("Should have raised an IllegalArgumentException");
    } catch (IllegalArgumentException expected) {
    }
  }
}

Das JUnit-Muster, um Exceptions zu testen, ist denkbar einfach, nachdem es einmal klar geworden ist. Wir probieren, ein Euro Exemplar mit negativem Wert zu bilden. Was wir im Fall einer solchen Verletzung der Vorbedingung erwarten würden, wäre eine IllegalArgumentException zu werfen. Wenn der Konstruktor also diese Ausnahme auslöst, kommt der catch Block zum Tragen. Der Variablenname der Exception drückt es schon aus, wir erwarten eine IllegalArgumentException. Wir fangen die erwartete Exception und alles ist gut. Wenn der Konstruktor die Ausnahme dagegen nicht auslöst, wird die fail Anweisung ausgeführt und der spezifizierte Fehlertext von JUnit in einem AssertionFailedError Fehlerobjekt protokolliert. Der fail Methode sind wir bisher noch nicht begegnet. Sie wird von der Assert Klasse realisiert. Sie können sie wann immer verwenden, wenn Sie einen Testfall mit einem Fehler abbrechen möchten.

Abschliessend sehen Sie den angepassten Konstruktor:


JUnit-IkonGrüner JUnit-Balken

public class Euro...
  public Euro(double euro) {
    if (euro < 0.0) {
      throw new IllegalArgumentException("Negative amount");
    }

    cents = Math.round(euro * 100.0);
  }
}

Schenken Sie Ihrem Testcode unter allen Umständen die gleiche Aufmerksamkeit wie dem Programmcode, der in Produktion gehen soll. Es ist keineswegs unüblich, daß Sie mehr Testcode schreiben werden wie Produktionscode. Unit Tests zu schreiben ist allem voran eine Investition in die Zukunft ihrer Software, die sie durch regelmässiges wie sorgfältiges Refactoring unbedingt aufrechterhalten sollten.

Tipp
Refaktorisieren Sie Ihren Testcode genauso regelmässig und sorgfältig wie ihren übrigen Code.

Nächstes Tutorial in dieser Serie: Testgetriebene Entwicklung

Danke für Eure Verbesserungsvorschläge

Tammo Freese, Johannes Link, Karsten Menne, Martin Müller-Rohde, Stefan Roock, Andreas Schoolmann und Frankmartin Wiethüchter

Weiterführende Informationen

19.2.2001

XP über die Schulter geschaut

Ein erster Einblick

Wie sieht ein Tag mit Extreme Programming aus? Nun, Sie werden wie gewohnt programmieren. Obwohl, nicht ganz. Sie programmieren mit einem Partner. Sie arbeiten zu zweit an einer kleinen überschaubaren Aufgabe. Die Zerlegung in Aufgaben haben Sie kurz zuvor im Team in einem kurzen Design-Meeting mit dem Kunden diskutiert und geplant. Sie haben die Verantwortlichkeit für eine Reihe von Aufgaben akzeptiert und suchen sich für deren kurze Dauer jeweils einen Programmierpartner.

Bevor Sie gemeinsam zu programmieren beginnen, analysieren Sie und Ihr Partner zunächst, was Ihr Auftraggeber überhaupt an Anforderungen stellt. Dazu möchten Sie unter Umständen direkt mit Ihrem Kunden sprechen. Er arbeitet wenige Meter von Ihnen beiden entfernt im gleichen Büro, damit Fragen des Entwicklungsteams unmittelbar diskutiert werden können.

Jetzt wissen Sie beide, was verlangt wird, und Sie können endlich losprogrammieren. Einen Augenblick. Bevor Sie losprogrammieren, überlegen Sie und Ihr Partner sich zunächst ein Design dafür, wie die Anforderungen überhaupt umgesetzt werden sollen. Dazu schreiben Sie eine Reihe von Testfällen. Anschliessend schreiben Sie den Code, der diese Tests erfüllt. Dabei streben Sie stets die einfachste Lösung an. Testfall für Testfall erhalten Sie grünes Licht und gehen weiter. Die Aufgabe gilt dann als erledigt, wenn Ihnen beiden keine weiteren Tests mehr einfallen, die sich für die aktuellen Anforderungen sinnvoll schreiben liessen.

Wenn alle Ihre Tests erfüllt sind, verlangt XP von Ihnen, den geschriebenen Code noch möglichst zu vereinfachen und sein Design zu verbessern. Der Code darf unter anderem keine duplizierte Logik enthalten und muß jede Intention ausdrücken, die für das Verständnis des Programms notwendig erscheint. Nachdem Sie den Code in Form gebracht haben, lassen Sie Ihre Arbeit durch die Suite aller im Team gesammelten Tests kontrollieren und integrieren schließlich Ihren geschriebenen Code mit den Änderungen Ihrer Teamkollegen. Anschliessend besorgen Sie sich zunächst einmal frischen Kaffee und helfen eventuell direkt Ihrem Partner bei seiner nächsten Programmieraufgabe.

Ein XP-Tag geht schnell vorüber, da Sie den ganzen Tag lang intensiv mit Ihren Kollegen programmiert haben. Das bedeutet nicht etwa, daß Sie Ihrem Partner bei der Arbeit über die Schulter schauen. Im Gegenteil. Im Paar zu programmieren bedeutet aufmerksam in die Programmierepisode involviert zu sein. Sie unterhalten sich über den Code, den sie gemeinsam schreiben. Regelmässig übergeben Sie Ihrem Partner die Tastatur und lassen ihn "fahren". Am Ende eines solchen Tages sind Sie erschöpft und, glauben Sie mir, Sie gehen pünktlich nach Hause, weil Sie acht Stunden lang fokussiert gearbeitet haben und die geschriebenen Tests Ihnen Vertrauen in die geleistete Arbeit geben.

Zugegeben, der Name "Extreme Programming" läßt einen extrem einseitigen Ansatz der Softwareentwicklung vermuten. Auf den zweiten Blick jedoch widmet sich XP sehr viel intensiver der Analyse, dem Design und dem Test als schwergewichtige Methoden. XP bringt uns zurück zu den Wurzeln des Handwerks guter Programmierung und der Fragestellung, was wirklich zählt, wenn wir hochqualitative Software erstellen wollen.

Ich möchte Sie dazu einladen, das Handwerk von XP und das Lebensgefühl eines leichtgewichtigen Prozesses kennenzulernen. Begleiten Sie mich auf den folgenden Seiten durch ein XP-Projekt und schauen Sie dem Team bei der täglichen Arbeit über die Schulter und in die Karten.

Programmieren in Paaren

Die Regel lautet: Wer um Hilfe bittet, dem wird Hilfe geboten. Tatsächlich wird keine Zeile Produktionscode geschrieben, ohne daß zwei Paar Augen auf den Bildschirm gerichtet sind. Das bedeutet, Sie programmieren zu zweit an einem Rechnern mit einer Tastatur und einer Maus. Sie sitzen nebeneinander, führen ein intensives Gespräch über den entstehenden Code und wechseln sich regelmässig an der Tastatur ab. Sie dürfen dabei sogar Spaß haben, denn Programmieren soll Spaß bringen. Sie wechseln häufiger Ihre Programmierpartner und am Ende der Woche haben Sie idealerweise mal mit jedem Ihrer Kollegen zusammengearbeitet, damit sich das aufgebaute Wissen über das gesamte Team verbreitet.

Felix und Ulrich, Dienstag, 1.Iteration, 11.03 Uhr

Ulrich: Was ist unsere Aufgabe?

Felix: Wir müssen die Miete für ausgeliehene DVDs berechnen.

Ulrich: Wie berechnen wir das?

Felix: Der Preis ist abhängig davon, wie lange eine DVD ausgeliehen wird.

Ulrich: Und wie?

Felix: Laß mich mal sehen... Auf unserer Karte steht, reguläre Filme kosten für zwei Tage 2 Euro. Ab dem dritten Ausleihtag dann 1.5 Euro pro Tag.

Ulrich: Okay, wie machen wir das also?

Felix: Mmm, ich schlage vor, wir merken uns zunächst mal, welche Filme ein Kunde ausleiht und berechnen später die Miete anhand der Ausleihtage.

Ulrich: Warum berechnen wir die Kosten nicht auf der Stelle?

Felix: Das ist später vielleicht notwendig, aber momentan ist das keine Anforderung.

Ulrich: Schön, wie soll unsere erste Klasse heissen?

Felix: Spannende Frage... Die Verantwortlichkeiten klingen fast danach, als würde sich die Klasse selbst Customer nennen wollen.

Ulrich: Fangen wir damit mal an.

Felix: Stop mal! Laß uns gleich einen Test dafür schreiben...

Ulrich: Gut, dazu muß unsere Testklasse von TestCase aus dem Test-Framework ableiten.

Felix: Und dann bekommt sie noch einen Konstruktor für den Namen des Testfalls.


public class CustomerTest extends junit.framework.TestCase {
  public CustomerTest(String name) {
    super(name);
  }
}

Testgetriebenes Programmieren

Die anzustrebende Geisteshaltung muß sein, daß eine Funktion solange nicht existiert, bis sie einen automatisierten Test besitzt. Tatsächlich entsteht kein Produktionscode, bevor es nicht einen entsprechenden Testfall gibt, der fehlschlägt. Das bedeutet, Sie schreiben einen Test noch bevor Sie den Code schreiben, der diesen Test erfüllt. Sie erstellen inkrementell eine umfassende Suite von Unit Tests, die das gesamte Programm in isolierten Einheiten auf die erwartete Funktion hin überprüft. Sie verwenden ein Test-Framework, das Ihnen dabei hilft, automatische Tests zu schreiben und auszuführen. Sie sammeln und pflegen diese Tests, damit Sie nach jeder Änderung sicherstellen können, daß die Testsuite zu 100% läuft.

Ulrich: Was ist der erste Testfall?

Felix: Das einfachste wäre, wir fangen mit dem Ausleihen eines Films an.

Ulrich: Und wie?

Felix: Es ist so, daß wir für jede Methode, die funktionieren soll, Zusicherungen schreiben wollen, um die Funktion abzuprüfen.

Ulrich: Dafür ist die assertTrue Methode gedacht, die wir aus TestCase erben.

Felix: Genau. Als Parameter erwartet sie eine Bedingung, die true ergeben muß, damit der Testfall als erfüllt gilt. Den Rest erledigt das Framework.

Ulrich: Und wohin schreiben wir nun den Testcode?

Felix: Am besten schreiben wir dafür eine Testfallmethode testRentingOneMovie, die dann die Mietkosten für den Film testet. Das Framework findet automatisch alle Methoden, die mit test beginnen, und führt sie aus.

Ulrich: Gut, schreiben wir mal auf, was wir bis jetzt wissen. Wir benötigen zunächst mal ein Customer Exemplar. Und dann muß ich so tun, als gäbe es einfach alle Methoden schon, die ich mir wünsche.

Felix: Richtig. Wir leihen eine DVD für einen Tag aus und es soll 2 Euro kosten.

Ulrich: Das ist einfach.


public class CustomerTest...
  public void testRentingOneMovie() {
    Customer customer = new Customer();
    customer.rentMovie(1);
    assertTrue(customer.getTotalCharge() == 2);
  }
}

Möglichst einfaches Design

Die Designstrategie sieht vor, mit einem schlichten Design zu starten und dieses fortlaufend zu verbessern. Tatsächlich werden Designelemente, die komplizierter sind, als momentan unbedingt notwendig wäre, aufgeschoben, selbst wenn nur für wenige Minuten. Das bedeutet, Sie wählen von vielen verschiedenen Lösungswegen denjenigen, der am einfachsten erscheint, um einen Testfall zu erfüllen. Sie programmieren nur, was Sie jetzt tatsächlich benötigen, nicht, was Sie später vielleicht benötigen. Sie gehen sogar soweit, daß Sie unnötige Flexibilität wieder aus dem Code entfernen. Sie treten den Beweis dafür an, daß die aktuelle Lösung zu einfach ist, indem Sie einen Testfall schreiben, der ein komplexeres Design rechtfertigt.

Ulrich: Also gut. Du willst, daß ich nur den Test zum Laufen bringe und alles andere für einen Moment vergesse.

Felix: Ganz genau. Was würdest Du tun, wenn Du nur diesen einen Test implementieren müsstest?

Ulrich: Hah, auch das ist einfach.


public class Customer {
  public void rentMovie(int daysRented) {
  }

  public int getTotalCharge() {
    return 2;
  }
}

Felix: Wie extrem! Aber gut...

Ein wenig Testen, ein wenig Programmieren...

Das Zusammenspiel von testgetriebenem Programmieren und einfachem Design ergibt den Zyklus des minutenweisen Programmierens. Tatsächlich wird nie länger als zehn Minuten programmiert, ohne die Feedbackschleife unmittelbar durch konkrete Tests zu schliessen. Das bedeutet, Sie schreiben neuen Code in so winzigen Schritten, daß Ihr Code gerade mal den aktuellen Testfall erfüllt. Sie testen ein wenig, Sie programmieren ein wenig. Dann testen Sie wieder und programmieren... Minute für Minute feiern Sie einen kleinen Erfolg. Sie schreiben keine ganze Klasse in einem Rutsch. Vielmehr schreiben Sie nur ein paar Zeilen Code, maximal eine Methode auf einmal.

Ulrich: Als nächstes möchte ich zwei und drei ausgeliehene DVDs testen.

Felix: Immer langsam... Schreib erst mal den Test für zwei Filme. Der Zweite wird für zwei Tage entliehen. Die Summe soll 4 Euro betragen. Laß uns dazu die assertEquals Methode benutzen. Als Parameter erhält sie den erwarteten Wert und das tatsächliche Resultat.


public class CustomerTest...
  public void testRentingTwoMovies() {
    Customer customer = new Customer();
    customer.rentMovie(1);
    customer.rentMovie(2);
    assertEquals(4, customer.getTotalCharge());
  }
}

Ulrich: Okay, dieser Test wird nicht laufen.

Felix: Woher weisst Du das? Schau nach, Du weisst nie!

Ulrich: Sagen wir einfach, ich bin mir ziemlich sicher.

Felix: Gut, wenn Du es also weisst und nehmen wir an, der Test zeigt grün, würde das demnach bedeuten, daß entweder unser Test falsch ist oder aber der Code Dinge tut, die er nicht machen darf, richtig? Mach den Test!

Ulrich: Also gut... Der Test zeigt rot.

Felix: Diesmal kommst Du auch nicht mehr so einfach davon...


public class Customer {
  private int totalCharge = 0;

  public void rentMovie(int daysRented) {
    totalCharge += 2;
  }

  public int getTotalCharge() {
    return totalCharge;
  }
}

Evolutionäres Design

Organisches Wachstum scheint eine gute Strategie zu sein, um auf Veränderung und Ungewissheit reagieren zu können. Tatsächlich werden Anforderungsänderungen als Chance und nicht als Problem betrachtet. Das bedeutet, Sie verhalten sich im Design so, als wüssten Sie wirklich nicht, was die nächsten Anforderungen sein würden. Sie entwerfen stets die einfachste Lösung und bringen Ihren Code anschliessend in die einfachste Form. Sie vertrauen auf die Tatsache, daß sauber strukturierter Code in jede Richtung mitziehen kann und daß der Code, den Sie gestern geschrieben haben, Sie heute und morgen dabei unterstützen wird, weiterhin Code zu schreiben, und Sie nicht zunehmend daran hindert.

Ulrich: Was ist der nächste Testfall?

Felix: Ein dritter Film, der drei Tage entliehen wird.

Ulrich: Wieviel kostet der Film, wenn er erst nach drei Tagen zurückgegeben wird?

Felix: Jeder weitere Tag kommt auf 1.5 Euro.

Ulrich: Also 3.5 Euro am Tag drei. Macht zusammen 7.5 Euro. Ausserdem können wir Fließkommazahlen nicht mit unendlicher Genauigkeit vergleichen.


public class CustomerTest...
  public void testRentingThreeMovies() {
    Customer customer = new Customer();
    customer.rentMovie(1);
    customer.rentMovie(2);
    customer.rentMovie(3);
    assertEquals(7.5, customer.getTotalCharge(), 1e-3);
  }
}

Felix: Ab jetzt müssen wir 1.5 Euro draufschlagen.

Ulrich: Das bedeutet auch, daß totalCharge ab sofort Fließkommazahl sein möchte.


public class Customer {
  private double totalCharge = 0;

  public void rentMovie(int daysRented) {
    totalCharge += 2;
    if (daysRented > 2) {
      totalCharge += 1.5;
    }
  }

  public double getTotalCharge() {
    return totalCharge;
  }
}

Felix: Nun meckert aber der Compiler rum... Wir müssen wohl auch unseren vorherigen Testfall zum Vergleich von Fließkommazahlen bringen.


public class CustomerTest...
  public void testRentingTwoMovies() {
    Customer customer = new Customer();
    customer.rentMovie(1);
    customer.rentMovie(2);
    assertEquals(4, customer.getTotalCharge(), 1e-3);
  }
}

Natürlicher Abschluß einer Programmierepisode

Bewußt aufhören zu können ist einer der stärksten Programmierzüge. Tatsächlich ist eine Aufgabe fertig, wenn alle Randbedingungen getestet sind, die dazu führen können, daß etwas schief geht. Das bedeutet, Sie schreiben nicht für jede Methode einen Test, sondern nur für solche, die unter Umständen fehlschlagen könnten. Sie halten Ihr Wissen über den Code fest, während Sie Tests dafür schreiben. Sie wissen, daß Sie erreicht haben, was Sie sich vorgenommen hatten, wenn alle Ihre Tests erfüllt sind. Zum Abschluß sei es Ihnen gegönnt, über Ihre Programmierepisode zu reflektieren. Sie wollen ja schließlich jeden Tag etwas dazulernen.

Ulrich: Ein Film, der vier Tage entliehen wird?

Felix: Kostet 5 Euro und macht dann insgesamt 12.5 Euro.


public class CustomerTest...
  public void testRentingFourMovies() {
    Customer customer = new Customer();
    customer.rentMovie(1);
    customer.rentMovie(2);
    customer.rentMovie(3);
    customer.rentMovie(4);
    assertEquals(12.5, customer.getTotalCharge(), 1e-3);
  }
}

Felix: Laß mich mal tippen!

Ulrich: Okay!


public class Customer...
  public void rentMovie(int daysRented) {
    totalCharge += 2;
    if (daysRented > 2) {
      totalCharge += (daysRented - 2) * 1.5;
    }
  }
}

Felix: So, das hätten wir. Haben wir irgendwelche Tests vergessen?

Ulrich: Müsste mit dem Teufel zugehen...

Refactoring

Gutes Design ist nie einfach und niemand bekommt die Dinge im ersten Versuch in den Griff. Tatsächlich entsteht ein Design durch schrittweises Wachstum und ständige Überarbeitung. Das bedeutet, daß Sie alle Erfahrungen in das Design zurückfliessen lassen und das Design verbessern, nachdem der Code geschrieben wurde. Sie refaktorisieren, um Ihren Code so einfach und so verständlich wie möglich zu machen und jede Art von Redundanz zu beseitigen.

Ulrich: Wenn ich mir unseren Code ansehe, frage ich mich, was die vielen Zahlen bedeuten. Wir sollten denen Namen geben!

Felix: Vorschläge?


public class Customer...
  static final double BASE_PRICE = 2;       // Euro
  static final double PRICE_PER_DAY = 1.5;  // Euro
  static final int DAYS_DISCOUNTED = 2;
}

Ulrich: Eigentlich finde ich die Änderung ja zu klein, um nach jedem Schritt zu testen...

Felix: Du hast Mut, aber wir haben ja noch die Tests...


public class Customer...
  public void rentMovie(int daysRented) {
    totalCharge += BASE_PRICE;
    if (daysRented > DAYS_DISCOUNTED) {
      totalCharge += (daysRented - DAYS_DISCOUNTED) * PRICE_PER_DAY;
    }
  }
}

Ulrich: Irgendwie passen die Konstantennamen nun aber doch nicht mehr zu der Klasse.

Felix: Stimmt, das hat jetzt schon sehr viel mit der eigentlichen Preisberechnung für den Film zu tun.

Ulrich: Ziehen wir eine neue Klasse Movie raus, um dem Ausdruck zu verleihen!?

Felix: Einverstanden, aber erst die Tests...


public class MovieTest extends junit.framework.TestCase {
  public MovieTest(String name) {
    super(name);
  }

  public void testGetCharge() {
    assertEquals(2.0, Movie.getCharge(1), 1e-3);
    assertEquals(2.0, Movie.getCharge(2), 1e-3);
    assertEquals(3.5, Movie.getCharge(3), 1e-3);
    assertEquals(5.0, Movie.getCharge(4), 1e-3);
  }
}

Felix: Die Preisberechnung verschieben wir einfach auf diese Klasse...


public class Movie {
  static final double BASE_PRICE = 2;       // Euro
  static final double PRICE_PER_DAY = 1.5;  // Euro
  static final int DAYS_DISCOUNTED = 2;

  public static double getCharge(int daysRented) {
    double result = BASE_PRICE;
    if (daysRented > DAYS_DISCOUNTED) {
      result += (daysRented - DAYS_DISCOUNTED) * PRICE_PER_DAY;
    }
    return result;
  }
}

Felix: Unser Customer wird dadurch wieder ganz schlank...


public class Customer...
  public void rentMovie(int daysRented) {
    totalCharge += Movie.getCharge(daysRented);
  }
}

Fortlaufende Integration

Neuer Code wird schnellstmöglich in die neueste Version integriert. Tatsächlich wird mehrmals täglich ein getesteter Build des gesamten Programms erstellt. Das bedeutet, daß Sie am Ende jeder Programmierepisode und wenigstens einmal am Tag Ihren geschriebenen Code in das Programm integrieren und versionieren. Sie laden Ihre Änderungen auf einen dedizierten Integrationsrechner, integrieren Ihren Code und beseitigen entstehende Konflikte. Sie führen die Tests aus und geben Ihren Code frei, sobald alle Tests erfolgreich ausgeführt werden. Die gesamte Integrationsprozedur dauert gerade so lange, daß Sie sich eine frische Tasse Kaffee kochen und sie austrinken können.

Felix: Laß uns unseren Code integrieren und Mittag machen!

Ulrich: Milchkaffee oder Chinese?

Nächster Artikel in dieser Serie: Unit Tests mit JUnit

Danksagungen

Leah Striker, Michael Schürig, Meike Budweg, Tammo Freese, Ulrike Jürgens, Hans Wegener, Marko Schulz, Antonín Andert, Manfred Lange und Julian Mack haben das Beispiel entstehen sehen und nützliche Verbesserungsvorschläge gemacht.

Der Ehrentitel eines Meisterrezensenten sei an Rolf F. Katzenberger verliehen.

29.3.2008

Tonabnehmer #15: Jutta Eckstein, Johannes Link, Jens Coldewey, Henning Wolf -
7 Jahre Agiles Manifest

Jutta EcksteinJohannes LinkJens ColdeweyHenning Wolf

Im Februar 2001 war das Agile Manifest geboren. In dieser Roundtable-Diskussion machen wir eine Bestandsaufnahme, Reflektion, Analyse: Was haben sieben Jahre Agile Entwicklung gebracht?

Mit dabei: Jutta Eckstein (Blog, Profil), Johannes Link (Blog, Profil), Jens Coldewey (Blog, Profil), Henning Wolf (Blog, Profil)

17.11.2005

Mein Buch ist raus

Buchcover Lange hat's gebraucht ... und nun ist es endlich da: Testgetriebene Entwicklung mit JUnit und FIT ist soeben erschienen!

Mehr Details dazu auf meiner Buchseite, bei Amazon und dem dpunkt.verlag.