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.
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.
- 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?
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.
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...
-
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 dieequals
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 Objektreferenznull
ist. - Beispiele:
assertNull(hashMap.get(key));
-
assertNotNull(Object object)
verifiziert, ob eine Objektreferenz nichtnull
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.
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.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());
- 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.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.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à!
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.
- 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:
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?"
- 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.
- 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:
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?
-
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. - 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.
- 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.
-
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. -
Anschliessend wird eine der
test...
Methoden ausgeführt. -
Nach der Ausführung des Testfalls ruft das Framework die
tearDown
Methode, falls diese redefiniert wurde, und überläßt dasEuroTest
Objekt dann der Speicherbereinigung. - 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:
new EuroTest("testAdding")
setUp()
testAdding()
tearDown()
new EuroTest("testAmount")
setUp()
testAmount()
tearDown()
new EuroTest("testRounding")
setUp()
testRounding()
tearDown()
- 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:
- 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.
- 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:
- 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.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.
- 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.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:
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:
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.
- 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:
- 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:
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.
- 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
- Johannes Link: Softwaretests mit JUnit
- Frank Westphal: Testgetriebene Entwicklung mit JUnit und FIT
- JUnit.org: Testing resources for Extreme Programming
- JUnit Yahoo! Group: Englischsprachige Mailingliste
- Kent Beck, Erich Gamma: Test-Infected - Programmers love writing tests, Java Report, July 1998
- Erich Gamma, Kent Beck: JUnit - A Cook's Tour, Java Report, May 1999
Schlagwörter: junit testgetriebeneentwicklung unittests
blättern: Extreme Programming
zurück: Agile Softwareentwicklung