Zur Homepage www.HI-Tier.de programmers love JUnit
Home Nach oben
Öffentlicher Bereich für Entwickler

 

[translated into German; source is at junit.org, © JUnit.org 2004, was: Java Report Article , Last-Modified: Sat, 20 Dec 2003 18:45:21 GMT]

Infiziert vom JUnit Test: Programmierer lieben das Schreiben von Tests

Testen ist nicht eng mit Entwicklung verknüpft. Dies hält Sie davon ab, den Fortschritt der Entwicklung zu messen- Sie können nicht sagen, wann etwas zu arbeiten beginnt und wann etwas seine Arbeit einstellt. Mit JUnit können Sie einfach und schrittweise eine Testumgebung aufbauen, die Ihnen helfen wird, Ihren Fortschritt zu messen, unbeabsichtigte Seiteneffekte zu entdecken und Ihre Entwicklungsbemühungen zu konzentrieren.

Inhalt

bulletDas Problem
bulletBeispiel
bulletTestverfahren
bulletFazit

Das Problem

Jeder Programmierer weiß, daß er Tests für seinen Code schreiben sollte. Wenige tun es. Die Standardantwort auf "Warum nicht?" ist "Ich bin in Eile." Das führt schnell zu einem bösartigen Zyklus- je bedrückter man sich fühlt, desto weniger Tests schreibt man. Je weniger Tests man schreibt, desto weniger produktiv ist man und desto weniger stabil wird der eigene Code. Je weniger produktiv und genau man ist, desto mehr bedrückter wird man.

Programmierer verausgaben sich nur durch solche Zyklen. Ausbrechen erfordert einen äußeren Einfluß. Wir haben diesen benötigten äußeren Einfluß in Form einer einfachen Testumgebung gefunden, die uns ein wenig Testen ermöglicht, der einen großen Unterschied macht.

Der beste Weg, Sie vom Nutzen selbst geschriebener Tests zu überzeugen, wird sein, sich mit Ihnen hinzusetzen und ein wenig zu entwickeln. Währenddessen würden wir neue Fehler aufdecken, diese mit Tests abfangen, sie korrigiert, treten wieder auf, werden erneut korrigiert und so weiter. Sie würden den Nutzen des ständigen Feedbacks erkennen, den Sie durch das Schreiben, Speichern und erneutem Laufenlassen Ihrer eigenen Testeinheiten erhalten.

Leider ist dies nur ein Artikel und kein Büro, das über der bezaubernden Altstadt von Zürich herausragt, mit der Hektik eines mittelalterlichen Geschäftstreibens draußen und dem Dröhnen des Techno aus dem Plattenladen im Stockwerk darunter, so daß wir den Prozeß der Entwicklung simulieren müssen. Wir schreiben ein einfaches Programm und seiner Tests, und zeigen Ihnen die Ergebnisse der Testläufe. Auf diese Weise können Sie ein Gefühl für das von uns verwendete Verfahren bekommen und dieses nahelegen, ohne für unsere Anwesenheit zu bezahlen.

Beispiel

Achten Sie beim Lesen auf das Zusammenspiel des Codes und den Tests. Die Vorgehensweise hier ist, wenige Zeilen Code zu schreiben, dann einen Test, der läuft, oder sogar besser einen, der nicht läuft, und dann den Code, der ihn zum laufen bringt.

Das Programm, das wir schreiben, wird das Problem lösen, das das Rechnen mit mehrfachen Währungen abbildet. Berechnungen zwischen gleichen Währungen sind trivial, denn man kann einfach beide Werte addieren. Einfache Zahlen reichen aus. Sie können vorhandene Währungen ganz und gar ignorieren.

Die Dinge werden dann wesentlich interessanter, wenn es mehrere Währungen umfaßt. Sie können nicht einfach für eine Berechung eine Währung in eine andere umwandeln, nachdem es keinen individuellen Umrechnungskurs gibt- vielleicht wollten Sie den Wert eines Wertpapierdepots zum gestrigen Kurs mit dem heutigen Kurs vergleichen. 

Fangen wir einfach an und definieren eine Klasse Money, die einen Wert einer einzelnen Währung repräsentiert. Wir repräsentieren den Wert durch ein einfaches int. Um eine volle Genauigkeit zu erhalten, würden Sie vielleicht double oder java.math.BigDecimal verwenden, um beliebig genaue vorzeichenlose Dezimalzahlen speichern zu können. Wir repräsentieren eine Währung durch eine Zeichenkette, die die dreistellige ISO-Abkürzung (USD, CHF, usw.) aufnehmen kann. In komplexeren Implementationen könnte die Währung eine eigene Klasse vertragen.

class Money {
   private int fAmount;
   private String fCurrency;

   public Money(int amount, String currency) {
      fAmount= amount;
      fCurrency= currency;
   }

   public int amount() {
      return fAmount;
   }

   public String currency() {
      return fCurrency;
   }
}

Wenn Sie zwei Moneys der gleichen Währung addieren, dann enthält das resultierende Money den Wert der Summe aus den beiden anderen Werten.

public Money add(Money m) {
   return new Money(amount()+m.amount(), currency());
}

Statt nun einfach weiterzuprogrammieren, wollen wir ständigen Feedback haben und praktizieren "etwas codieren, etwas testen, etwas codieren, etwas testen". Um unsere Tests zu implementieren, verwenden wir das JUnit-System. Um Tests zu schreiben, benötigen Sie die neueste Ausgabe von JUnit (oder Sie schreiben Ihr eigenes Pendant- es ist nicht so viel Arbeit).

JUnit definiert, wie Sie ihre Testfälle strukturieren und stellt die Tools zur Verfügung, um sie laufen zu lassen. Sie implementieren einen Test als Subclass von TestCase. Um unsere Money-Implementierung zu testen, definieren wir daher MoneyTest als Subclass von TestCase. In Java sind Klassen in Packages eingeteilt und wir müssen uns entscheiden, wohin wir MoneyTest ablegen. Wir verfahren momentan so, daß MoneyTest im selben Package wie die zu testende Klasse abgelegt wird. Auf diese Weise hat die Klasse Zugriff auf die privaten Methoden des Package. Wir fügen eine Testmethode testSimpleAdd hinzu, die die einfache Version von Money.add() von oben anwenden wird. Eine JUnit-Testmethode ist eine gewöhnliche Methode ohne irgendeinen Parameter.

public class MoneyTest extends TestCase {
   //…
   public void testSimpleAdd() {
      Money m12CHF= new Money(12, "CHF");           // (1)
      Money m14CHF= new Money(14, "CHF"); 
      Money expected= new Money(26, "CHF");
      Money result= m12CHF.add(m14CHF);             // (2)
      Assert.assertTrue(expected.equals(result));   // (3)
   }
}

Der Testfall testSimpleAdd() enthält

  1. Code, der die Objekte anlegt, mit denen wir während des Tests umgehen. Dieser Testkontext wird üblicherweise als Testvorrichtung bezeichnet. Alles, was wir für den Text von testSimpleAdd() benötigen, sind ein paar Money-Objekte.
  2. Code, der die Objekte in der Vorrichtung anwendet
  3. Code, der das Ergebnis überprüft.

Bevor wir das Ergebnis überprüfen können, müssen wir etwas abschweifen, da wir einen Weg benötigen, der testet, ob zwei Money-Objekte gleich sind. Das Javaidiom dafür ist das Überschreiben der Methode equals, die in Object definiert ist. Bevor wir equals implementieren, schreiben wir einen Test für equals in MoneyTest.

public void testEquals() {
   Money m12CHF= new Money(12, "CHF");
   Money m14CHF= new Money(14, "CHF");

   Assert.assertTrue(!m12CHF.equals(null));
   Assert.assertEquals(m12CHF, m12CHF);
   Assert.assertEquals(m12CHF, new Money(12, "CHF"));   // (1)
   Assert.assertTrue(!m12CHF.equals(m14CHF));
}

Die Methode equals in Object liefert true, wenn beide Objekte gleich sind. Jedoch ist Money ein Objekt mit Werten. Zwei Moneys werden als gleich betrachtet, wenn sie die gleiche Währung und Werte haben. Um diese Eigenschaft zu prüfen, haben wir einen Test (1) hinzugefügt, der die Gleichheit von Moneys überprüft, wenn sie den selben Wert haben, aber nicht das selbe Objekt sind.

Als nächstes schreiben wir die Methode equals in Money:

public boolean equals(Object anObject) {
   if (anObject instanceof Money) {
      Money aMoney= (Money)anObject;
      return aMoney.currency().equals(currency()) && amount() == aMoney.amount();
   }
   return false;
}

Nachdem equals als Parameter jede Art von Objekt bekommen kann, müssen wir zuerst dessen Typ überprüfen, bevor wir es als Money casten können. Nebenbei ist es ein empfohlener Brauch, auch die Methode hashCode zu überschreiben, wann auch immer Sie die Methode equals überschreiben. Kommen wir aber wieder zurück zu unserem Testfall.

Mit einer Methode equals in der Hand können wir das Ergebnis von testSimpleAdd überprüfen. In JUnit wird dies über den Aufruf von Assert.assertTrue bewerkstelligt, welcher einen Fehler auslöst, der von JUnit aufgezeichnet wird, wenn der Parameter nicht true ist. Nachdem Aussagen über Gleichheit häufig auftreten, gibt es auch eine bequeme Methode Assert.assertEquals. Zusätzlich zum Test auf Gleichheit mit equals liefert es auch den Wert der beiden Objekte, sollten sie sich unterscheiden. Dies zeigt uns sofort, warum ein Test im Ergebnisbericht von JUnit fehlgeschlagen ist. Der Wert ist eine Repräsentation des Objekts als Zeichenkette, der von der Methode toString erzeugt wird. Es gibt noch weitere Varianten von assertXXXX, die hier nicht weiter betrachtet werden.

Nachdem wir nun zwei Testfälle implementiert haben, stellen wir fest, daß wir Code für die Tests mehrfach aufgesetzt haben. Es wäre schön, wenn wir den Code zur Testvorrichtung wiederverwenden könnten. In anderen Worten: wir hätten gerne eine einheitliche Vorlage für die Tests. Mit JUnit läßt sich das durch das Sichern in Vorlagevariablen unserer TestCase Unterklasse und dem Überschreiben der Methode setUp bewerkstelligen. Die symmetrische Funktion zu setUp ist tearDown, die man überschreiben kann, um die Vorlagen am Ende eines Tests wieder freizugeben. Jeder Test wird mit seiner eigenen Vorlage durchgeführt und JUnit ruft für jeden Test setUp und tearDown auf, so daß innerhalb der Tests keine Seiteneffekte auftreten können.

public class MoneyTest extends TestCase {
   private Money f12CHF;
   private Money f14CHF; 

   protected void setUp() {
      f12CHF= new Money(12, "CHF");
      f14CHF= new Money(14, "CHF");
   }
}

Wir können die beiden Methoden der Testfälle umschreiben und dabei den gleichartigen Setupcode entfernen.

public void testEquals() {
   Assert.assertTrue(!f12CHF.equals(null));
   Assert.assertEquals(f12CHF, f12CHF);
   Assert.assertEquals(f12CHF, new Money(12, "CHF"));
   Assert.assertTrue(!f12CHF.equals(f14CHF));
}

public void testSimpleAdd() {
   Money expected= new Money(26, "CHF");
   Money result= f12CHF.add(f14CHF);
   Assert.assertTrue(expected.equals(result));
}

Zwei zusätzliche Schritte sind nötig, um die beiden Testfälle abzuarbeiten:

  1. festlegen, wie die individuellen Testfälle abgearbeitet werden
  2. festlegen, wie eine Testsammlung abgearbeitet wird

JUnit unterstützt zwei Arten von einzelnen Testabläufen:

bulletstatisch
bulletdynamisch

Auf die statische Art überschreibt man die Methode runTest, die von TestCase vererbt wurde, und ruft den gewünschten Testfall auf. Eine bequeme Möglichkeit ist die Verwendung einer anonymen inneren Klasse. Wichtig ist, daß jedem Test ein Name zugeordnet werden muß, damit dieser identifiziert werden kann,.wenn er fehlschlägt.

TestCase test= new MoneyTest("simple add") {
   public void runTest() {
   testSimpleAdd();
   }
};

Eine Schablonenmethode[1] in der Superklasse stellt sicher, daß runTest ausgeführt wird, wenn es soweit ist.

Die dynamische Art, einen durchzuführenden Testfall anzulegen, verwendet Reflektion, um runTest zu implementieren. Es setzt voraus, daß der Name des Tests der Name der aufzurufenden Methode des Testfalls ist. Um den Test testSimpleAdd aufzurufen, wird ein MoneyTest wie folgt angelegt:

TestCase test= new MoneyTest("testSimpleAdd");

Die dynamische Art und Weise ist wesentlich kompakter, dafür aber weniger typensicher. Ein Fehler im Namen des Tests bleibt unbemerkt, bis man ihn abarbeitet und eine NoSuchMethodException erhält.  Beide Verfahren haben ihre Vorteile, so daß wir Ihnen die Entscheidung überlassen, welches Sie verwenden.

Als letzten Schritt müssen wir eine Testsammlung definieren, um beide Testfälle durchführen lassen zu können. In JUnit erfordert dies die Definition einer statischen Methode namens suite. Die Methode suite ist wie die Methode main, die die Aufgabe hat, die Tests durchzuführen. Innerhalb suite muß man die durchzuführenden Tests zu einem TestSuite Objekt hinzufügen und dieses zurückgeben. Eine TestSuite kann eine Sammlung von Tests durchführen. Sowohl TestSuite als auch TestCase implementieren ein Interface Test, welches die Methoden definiert, um einen Test durchführen zu können. Dies erlaubt das Erzeugen von Tests durch das Zusammenstellen von beliebigen TestCases und TestSuites. Kurz gesagt, TestSuite ist ein Kompositum[1]. Der folgende Code zeigt das Erzeugen einer Testsammlung auf die dynamische Art, um einen Test durchzuführen:

public static Test suite() {
   TestSuite suite= new TestSuite();
   suite.addTest(new MoneyTest("testEquals"));
   suite.addTest(new MoneyTest("testSimpleAdd"));
   return suite;
}

Seit JUnit 2.0 gibt es eine noch einfachere dynamische Art. Man übergibt TestSuite nur die Klasse mit den Tests und es extrahiert die Testmethoden automatisch.

public static Test suite() {
   return new TestSuite(MoneyTest.class);
}

Hier der analoge Code auf statische Art:

public static Test suite() {
   TestSuite suite= new TestSuite();
   suite.addTest(
      new MoneyTest("money equals") {
         protected void runTest() { testEquals(); }
      }
   );

   suite.addTest(
      new MoneyTest("simple add") {
         protected void runTest() { testSimpleAdd(); }
      }
   );
   return suite;
}

Nun ist man bereit, die Test laufen zu lassen. JUnit wird mit grafischen Oberflächen ausgeliefert, die Tests laufen lassen können. Schreiben Sie den Namen der Testklasse in das Feld oben im Fenster. Drücken Sie dann den Run Knopf. Während des Testlaufs zeigt JUnit den Fortschritt mit einer Fortschrittsanzeige unterhalb des Eingabefeldes an. Der Graph ist anfangs grün, ändert sich aber in rot, sobald ein Test nicht erfolgreich ist. Fehlgeschlagene Tests werden in einer Liste unten angezeigt. Bild 1 zeigt das TestRunner Fenster, nachdem wir unsere triviale Testsammlung ausgeführt haben.


Bild 1: Ein erfolgreicher Lauf

Nachdem wir überprüft haben, daß der einfache Fall mit der Währung geklappt hat, fahren wir mit mehrfachen Währungen fort. Wie oben bereits erwähnt, ist das Problem mit unterschiedlichen Währungsumrechnungen, daß es keinen einzelnen Wechselkurs gibt. Um dieses Problem zu umgehen, führen wir ein MoneyBag ein, das die Wechselkursumwandlungen verzögert. Beispielsweise wird das Hinzufügen von 12 Schweizer Franken zu 14 US Dollar als Beutel mit zwei Moneys 12 CHF und 14 USD aufgefaßt. Das Hinzufügen von weiteren 10 Schweizer Franken ergibt einen Beutel mit 22 CHF und 14 USD. Wir können später einen MoneyBag mit unterschiedlichen Wechselkursen festlegen.

Ein MoneyBag wird repräsentiert durch eine Liste von Moneys und stellt verschiedene Konstruktoren zur Verfügung, um einen MoneyBag zu erzeugen. Zu beachten ist, daß die Konstruktoren fürs Package privat sind, da die MoneyBags im Hintergrund angelegt werden, wenn Wechselkursumrechnungen durchgeführt werden.

class MoneyBag {
   private Vector fMonies= new Vector();

   MoneyBag(Money m1, Money m2) {
      appendMoney(m1);
      appendMoney(m2);
   }

   MoneyBag(Money bag[]) {
      for (int i= 0; i < bag.length; i++)   appendMoney(bag[i]);
   }
}

Die Methode appendMoney ist eine interne HIlfsfunktion, die ein Money zur Liste der Moneys hinzufügt und berücksichtigt dabei, daß Moneys mit der gleichen Währung zusammengefaßt werden. MoneyBag benötigt außerdem eine Methode equals mit einem passenden Test. Wir überspringen die Implementation von equals und zeigen nur die Methode testBagEquals. Im ersten Schritt erweitern wir die Vorrichtung, um zwei MoneyBags zu ergänzen.

protected void setUp() {
   f12CHF= new Money(12, "CHF");
   f14CHF= new Money(14, "CHF");
   f7USD=  new Money( 7, "USD");
   f21USD= new Money(21, "USD");
   fMB1= new MoneyBag(f12CHF, f7USD);
   fMB2= new MoneyBag(f14CHF, f21USD);
}

Mit dieser Vorrichtung wird der Testfall zu testBagEquals:

public void testBagEquals() {
   Assert.assertTrue(!fMB1.equals(null));
   Assert.assertEquals(fMB1, fMB1);
   Assert.assertTrue(!fMB1.equals(f12CHF));
   Assert.assertTrue(!f12CHF.equals(fMB1));
   Assert.assertTrue(!fMB1.equals(fMB2));
}

Dem "etwas codieren, etwas testen" folgend, lassen wir unseren erweiterten Test mit JUnit laufen und stellen fest, daß nach wie vor alles gut läuft. Mit MoneyBag in der Hand können wir nun die Methode add in Money anpassen.

public Money add(Money m) {
   if (m.currency().equals(currency()) )
      return new Money(amount()+m.amount(), currency());
   return new MoneyBag(this, m);
}

So wie es definiert wurde, läßt es sich nicht compilieren, da ein Money und kein MoneyBag als Rückgabewert erwartet wird. Mit der Einführung von MoneyBag haben wir nun zwei Repräsentationen für Moneys, die wir vor dem Client-Code verstecken wollen. Um dies zu erreichen, führen wir ein Interface IMoney ein, das beide Repräsentationen implementiert. Hier ist das Interface IMoney:

interface IMoney {
   public abstract IMoney add(IMoney aMoney);
   //…
}

Um die unterschiedlichen Repräsentationen vor dem Clienten vollständig zu verstecken, müssen wir Umrechnungen zwischen allen möglichen Moneys und MoneyBags unterstützen. Bevor wir weiterprogrammieren, definieren wir erst ein paar weitere Testfälle. Das zu erwartende Ergebnis MoneyBag verwendet den bequemen Konstruktor von oben, das ein MoneyBag mit einem Array anlegt:

public void testMixedSimpleAdd() { 
   // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} 
   Money bag[]= { f12CHF, f7USD }; 
   MoneyBag expected= new MoneyBag(bag); 
   Assert.assertEquals(expected, f12CHF.add(f7USD)); 
}

Die anderen Tests folgen dem selben Muster:
bullet testBagSimpleAdd - um ein MoneyBag zu einem einzelnen Money hinzuzufügen
bullettestSimpleBagAdd - um ein einzelnes Money zu einem MoneyBag hinzuzufügen
bullet testBagBagAdd - um zwei MoneyBags zu addieren

Als nächstes erweitern wir die Testsammlung entsprechend:

public static Test suite() {
   TestSuite suite= new TestSuite();
   suite.addTest(new MoneyTest("testMoneyEquals"));
   suite.addTest(new MoneyTest("testBagEquals"));
   suite.addTest(new MoneyTest("testSimpleAdd"));
   suite.addTest(new MoneyTest("testMixedSimpleAdd"));
   suite.addTest(new MoneyTest("testBagSimpleAdd"));
   suite.addTest(new MoneyTest("testSimpleBagAdd"));
   suite.addTest(new MoneyTest("testBagBagAdd"));
   return suite;
}

Nachdem die Testfälle definiert wurden, können wir mit deren Implementierung beginnen. Die Herausforderung ist hier, mit allen unterschiedlichen Kombinationen von Money mit MoneyBag klarzukommen. Double dispatch[2] ist eine elegante Möglichkeit, dieses Problem zu lösen. Die Idee hinter double dispatch ist die Verwendung eines zusätzlichen Aufrufs, um den Typ des Arguments zu erhalten, mit dem wir zu tun haben. Wir rufen eine Methode des Parameters auf, deren Name der der originalen Methode entspricht, gefolgt vom Klassennamen des Empfängers. (Anm.d. Übersetzers: Damit ist gemeint, daß beispielsweise in der Klasse MoneyBag die Methode aufgerufen wird, die auf MoneyBag hört (z.B. MoneyBag.addMoneyBag()). In der Klasse Money wäre dies dann die Methode addMoney. Als Parameter wird dann gleichzeitig auch der selbe Klassenname übergeben, etwa MoneyBag.addMoneyBag(this).) Die Methode add in Money und MoneyBag wird zu:

class Money implements IMoney {
   public IMoney add(IMoney m) {
      return m.addMoney(this);
   }
   //…
}

class MoneyBag implements IMoney {
   public IMoney add(IMoney m) {
      return m.addMoneyBag(this);
   }
   //…
}

Um dies compilieren zu können, muß das IMoney interface um zwei Helfermethoden erweitert werden:

interface IMoney {
   //…
   IMoney addMoney(Money aMoney);
   IMoney addMoneyBag(MoneyBag aMoneyBag);
}

Um die Implementation des double dispatch zu vervollständigen, müssen diese Methoden implementiert werden. Dies ist diesselbe in Money:

public IMoney addMoney(Money m) {
   if (m.currency().equals(currency()) )
      return new Money(amount()+m.amount(), currency());
   return new MoneyBag(this, m);
}

public IMoney addMoneyBag(MoneyBag s) {
   return s.addMoney(this);
}

Nachfolgend die Implementation in MoneyBag, die davon ausgeht, daß zusätzliche Konstruktoren ein MoneyBag mit einem Money und ein MoneyBag mit zwei MoneyBags anlegen:

public IMoney addMoney(Money m) {
   return new MoneyBag(m, this);
}

public IMoney addMoneyBag(MoneyBag s) {
   return new MoneyBag(s, this);
}

Wir lassen die Tests laufen und sie gelingen. Beim Betrachten der Implementation entdecken wir jedoch einen anderen interessanten Fall. Was passiert, wenn das Ergebnis einer Addition einen MoneyBag erzeugt, der dann nur ein Money enthält? Werden zum Beispiel -12 CHF zu einem MoneyBag addiert, das bereits 7 USD und 12 CHF enthält, dann bleibt ein Beutel mit nur 7 USD übrig. So ein Beutel sollte offensichtlich identisch sein mit einem einzelnen Money mit 7 USD. Um dieses Problem zu überprüfen, implementieren wir einen Testfall und lassen ihn abarbeiten.

public void testSimplify() {
   // {[12 CHF][7 USD]} + [-12 CHF] == [7 USD]
   Money expected= new Money(7, "USD");
   Assert.assertEquals(expected, fMS1.add(new Money(-12, "CHF")));
}

Wird in diesem Stil entwickelt, dann wird man öfters solche Gedanken haben und sofort einen Test schreiben anstatt direkt den dazugehörigen Code zu programmieren.

Es ist keine Überraschung, daß unser Test mit einer roten Fortschrittsanzeige endet, der einen Fehler anzeigt. Nun korrigieren wir den Code in MoneyBag, um wieder einen grünen Status zu erhalten.

public IMoney addMoney(Money m) {
   return (new MoneyBag(m, this)).simplify();
}

public IMoney addMoneyBag(MoneyBag s) {
   return (new MoneyBag(s, this)).simplify();
}

private IMoney simplify() {
   if (fMonies.size() == 1)   return (IMoney)fMonies.firstElement();
   return this;
}

Nun lassen wir den Test wieder laufen und voila, wir beenden ihn mit grün.

Der obige Code löst nur einen kleinen Teil der Umrechnungsprobleme der Mehrfachwährungen. Wir müßten mehrere Wechselkurse repräsentieren, formatierte Ausgabe bereitstellen und andere Rechenoperationen anbieten und das alles mit ausreichender Geschwindigkeit. Wir hoffen jedoch, daß man sehen konnte, wie man den Rest der Objekte und einen Test auf einmal entwickeln kann - etwas testen, etwas codieren, etwas testen, etwas codieren.

Im einzelnen zusammengefaßt war dies:
bulletWir schrieben den ersten Test (testSimpleAdd) gleich nachdem wir add() geschrieben hatten. Üblicherweise wird die Entwicklung wesentlich flüssiger von statten gehen, wenn die Tests kurz vor dem Entwickeln geschrieben werden. Es ist der Zeitpunkt, an dem man sich vorstellt, wie der Code arbeiten soll. Das ist der richtige Zeitpunkt, die Gedanken in einem Test festzuhalten.
bulletWir verbesserten die vorhandenen Tests (testSimpleAdd und testEqual) gleich nachdem wir den gemeinsamen Code für setUp eingeführt hatten. Testcode ist an und für sich wie Modellierungscode, der am besten arbeitet, wenn er ausreichend verbessert wurde. Wenn man den selben Testcode an zwei Stellen entdeckt, dann sollte man einen Weg finden, ihn so zu verbessern, daß er nur einmal auftaucht.
bulletWir erzeugten die Methode suite und erweiterten sie, als wir Double Dispatch angewendet haben. Alte Tests am Laufen zu halten ist genauso wichtig wie neue Tests zum Laufen zu bringen. Idealerweise sollten immer alle Tests laufen. Manchmal kann es aber zu lange dauern, wenn sie 10 mal in einer Stunde abgearbeitet werden. Man sollte sicherstellen, daß alle Tests mindestens einmal täglich abarbeiten.
bulletWir haben gleich einen neuen Test angelegt, als wir über die Notwendigkeit nachgedacht haben, daß ein MoneyBag nur sein Element liefern sollte. Es kann schwierig sein zu lernen, auf die Art umzuschalten, aber wir fanden es sinnvoll. Wenn man eine Idee aufgreift, was das System können soll, dann sollte man die Implementierung zurückstellen, statt dessen erst den Test dafür schreiben und diesen laufen lassen (man weiß es nie, aber es könnte ja schon funktionieren). Erst danach an der Implementation arbeiten.

Testmethoden

Martin Fowler macht es leicht für Sie. Er sagt: "Wann immer Sie versucht sind, irgend einen Ausdruck als Ausgabe zu formulieren oder in einen Debugger eingeben, sollten Sie stattdessen einen Test schreiben." Man wird zuerst denken, daß man ständig am Testvorlagen schreiben ist und das Testen wird einen etwas ausbremsen. Bald wird man jedoch anfangen, die Bibliothek der Vorlagen wieder zu verwenden und neue Tests werden dann relativ einfach sein, indem einfach eine Methode zur bestehenden TestCase Unterklasse angefügt wird. 

Man kann jederzeit mehr Tests schreiben. Man wird jedoch feststellen, daß nur ein Bruchteil der Tests, die man sich vorstellen kann, auch wirklich sinnvoll sind. Was man möchte ist, daß man Tests schreibt, die fehlschlagen, obwohl sie funktionieren sollten oder Tests, die erfolgreich sind, obwohl sie fehlschlagen sollten. Eine andere Sichtweise ist, daß man an deren Kosten-Nutzen-Faktor denkt. Man möchte Tests schreiben, die es Ihnen mit Informationen danken.

Hier ein paar der Zeitpunkte, an denen man sinnvollerweise zur Testumgebung zurückkehren sollte:
bulletwährend der Entwicklung - wenn es notwendig ist, dem System eine neue Funktionalität hinzuzufügen, schreibt man zu erst Tests. Man dann mit der Entwicklung fertig sein, wenn der Test läuft.
bulletwährend des Debuggen - wenn jemand einen Fehler in Ihrem Code entdeckt, sollten Sie erst einen Test schreiben, der erfolgreich sein wirdl, wenn der Code funktioniert. Dann debuggen Sie solange, bis der Test erfolgreich ist.

Noch ein wichtiger Hinweis für Ihre Tests: Wenn ein Test zum Laufen gebracht wurde, sollten Sie sicherstellen, daß sie weiterhin laufen werden. Es gibt einen großen Unterschied zwischen einer funktionierenden Sammlung und einer defekten. Idealerweise sollte man jeden Test laufen lassen, wenn man eine Methode ändert. Ihre Sammlung wird bald praktisch zu groß sein, um sie ständig laufen zu lassen. Optimieren Sie den Vorbereitungscode, so daß alle Tests durchlaufen können. Es sollten mindestens spezielle Testsammlungen angelegt werden, die alle Tests enthalten, die möglicherweise von der momentanen Entwicklung betroffen sind. Anschließend lassen Sie diese Sammlung bei jedem Compilervorgang laufen. Stellen Sie außerdem sicher, daß jeder Test mindestens einmal am Tag läuft: über Nacht, während des Mittagessens, während eines der langen Meetings, ...

Fazit

Dieser Artikel hat das Testen nur die oberflächlich angekratzt. Er hat sich jedoch auf den Stil des Testens konzentriert, anhand dessen man mit erstaunlich kleinem Aufwand einen schnelleren, produktiveren, vorausschaubareren und weniger gestreßten Entwickler hervorbringt.

Sobald man vom Testen infiziert wurde, wird sich die Einstellung bezüglich Entwicklung wahrscheinlich verändern. Nachfolgend einige Änderungen, die wir festgestellt haben.

Es gibt einen großen Unterschied zwischen Tests, die alle korrekt ablaufen und solchen, die es nicht tun. Ein Teil der infizierten Tester ist nicht im Stande heimzugehen, bevor der Test nicht 100% richtig ist. Wenn man die Testsammlung zehn oder hundert Mal pro Stunde laufen läßt, wird man nicht mehr so viel Chaos anrichten können, so daß man zu spät zum Abendbrot kommt.

Manchmal wird man keine Lust dazu haben, Test zu schreiben, insbesondere am Anfang. Tut es nicht. Man sollte dabei berücksichtigen, daß man in größere Schwierigkeiten kommt, je mehr Zeit man mit Debugging verbringt und wieviel Streß man mehr hat, wenn man die Tests nicht hat. Wir waren überrascht, wie viel mehr Spaß programmieren macht, wie viel mehr wir bemüht sind und wie wenig Streß wir haben, wenn wir durch Tests unterstützt werden. Der Unterschied ist so immens groß, daß er uns davon abhält, keine Tests zu schreiben, wenn wir keine Lust dazu haben.

Man wird fähig sein, wesentlich mehr zu verbessern, wenn man die Tests einmal hat. Man wird am Anfang nicht verstehen, wieviel man da dennoch machen kann. Versuchen Sie sich dabei zu erwischen, wenn Sie sagen "Oh, ich denke, ich sollte das so und so anordnen. Ich kann das jetzt aber nicht ändern. Ich möchte da nichts kaputt machen." Sie sollten dann den Code sichern und ihn die nächsten Stunden aufräumen (dieser Teil funktioniert am besten, wenn Ihnen jemand über die Schultern schaut, während Sie arbeiten). Führen Sie all die Änderungen durch während die Tests laufen. Sie werden erstaunt sein, wieviel Boden Sie in ein paar Stunden gut machen können, ohne sich Sorgen machen zu müssen, daß dabei etwas kaputt gehen könnte.

Wir haben beispielsweise die Vector gestützte Implementierung des MoneyBag in eine HashTable gestützte umgewandelt. Wir konnten die Umwandlung sehr schnell und sicher durchführen, da wir sehr viele darauf beruhende Tests hatten. Als alle Tests liefen, waren wir sicher, daß wir keine der Antworten, die das System produziert, verändert hatten.

Sie werden Ihr Team dazu bringen wollen, Tests zu schreiben. Wir fanden heraus, daß es am einfachsten ist, wenn dies durch direkten Kontakt geschieht. Beim nächsten Mal, wenn jemand um Hilfe beim Debuggen fragt, bringen Sie ihn/sie dazu, Testvorrichtungen und zu erwartende Ergebnisse zu diskutieren. Dann sagen Sie, daß "ich gerne das, was Sie mir gerade gesagt haben, in einer Form niederschreiben möchte, in der wir es verwenden können". Lassen Sie ihn/sie zusehen, wenn Sie einen kleinen Test schreiben. Lassen Sie ihn laufen. Korrigieren Sie ihn. Schreiben Sie einen anderen. Ziemlich bald werden die anderen ihre eigenen Tests schreiben.

Nun, geben Sie JUnit eine Chance. Wenn Sie es verbessern können, teilen Sie uns bitte die Änderungen mit, damit wir sie weitergeben können. Unser nächster Artikel wird sich detailiert mit dem JUnit-System befassen. Wir werden Ihnen zeigen, wie es aufgebaut ist und etwas über die Philosophie der Entwicklung am System erzählen.

Wir möchten Martin Fowler -der sowohl ein guter Programmierer als auch Analyst ist, wie man ihn sich nur wünschen kann- für seine hilfreichen Kommentare danken,  auch wenn die sich auf frühe Versionen von JUnit bezogen haben.

Anmerkungen

  1. Gamma, E., et al. Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, Reading, MA, 1995
  2. Beck, K. Smalltalk Best Practice Patterns, Prentice Hall, 1996


© 1999-2021 Bay.StMELF, verantwortlich für die Durchführung sind die  Stellen der Länder , fachliche Leitung ZDB: Frau Dr. Kaja.Kokott@hi-tier.de, Technik: Helmut.Hartmann@hi-tier.de
Seite zuletzt bearbeitet: 03. November 2021 16:34, Anbieterinformation: Impressum - Datenschutz - Barrierefreiheit