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

Schlagwörter:

blättern: Extreme Programming
zurück: Agile Softwareentwicklung