Hinweis! Eine aktuelle Version des Abschnittes gibt es im Buch „Java ist auch eine Insel. Einführung, Ausbildung, Praxis“, dem 1. Inselbuch. (War für den 2. Band vorgesehen, musste wegen der Fülle des 2. Buchs kurzerhand in den 1. Band geschoben werden. Sorry für Irritationen!)
1.1 Softwaretests
Um möglichst viel Vertrauen in die eigene Codebasis zu bekommen, bieten sich Softwaretests an. Tests sind kleine Programme, die ohne Benutzerkontrolle automatisch über die Quellcodebasis laufen und anhand von Regeln zeigen, dass gewünschte Teile sich so verhalten wie gewünscht. (Die Abwesenheit von Fehlern kann eine Software natürlich nicht zeigen, aber immer zeigt ein Testfall, dass das Programm die Vorgaben aus der Spezifikation erfüllt.)
Obwohl Softwaretests extrem wichtig sind, sind sie unter Softwareentwicklern nicht unbedingt populär. Das liegt unter anderem daran, dass sie natürlich etwas Zeit kosten, die neben der tatsächlichen Entwicklung aufgewendet werden muss. Wenn dann die eigentliche Software geändert wird, müssen auch die Testfälle oftmals mit angefasst werden, so dass es gleich zwei Baustellen gibt. Und da Entwickler eigentlich immer gestern das Feature fertig stellen sollten, fallen die Testes gerne unter den Tisch. Ein weiterer Grund ist, dass einige Entwickler sich für unfehlbare Codierungsgötter halten, die jeden Programmcode (nach ein paar Stunden debuggen) für absolut korrekt, performant und wohl duftend halten.
Wie lässt sich diese skeptische Gruppe nun überzeugen, doch Tests zu schreiben? Ein großer Vorteil von automatisierten Tests ist die Eigenschaft, dass bei großen Änderungen der Quellcodebasis (Refactoring) die Testfälle automatisch sagen, ob alles korrekt verändert wurde. Denn wenn nach dem Refactoring, etwa einer Performance-Optimierung, die Tests einen Fehler melden, ist wohl etwas kaputt-optimiert worden. Da Software einer permanenten Änderung unterliegt und nie fertig ist, sollte das Argument eigentlich schon ausreichen, denn wenn eine Software eine gewisse Größe erreicht hat, ist nicht absehbar, welche Auswirklungen Änderungen an der Quellcodebasis nach sich ziehen. Dazu kommt ein weiterer Grund, sich mit Tests zu beschäftigen. Es ist der positive Nebeneffekt, dass die erzeugte Software vom Design deutlich besser ist, denn testbare Software zu schreiben ist knifflig, führt aber fast zwangsläufig zum besseren Design. Und ein besseres Design ist immer erstrebenswert, denn das erhöht die Verständlichkeit und erleichtert die spätere Anpassung der Software.
1.1.1 Vorgehen bei Schreiben von Testfällen
Die Fokussierung bei Softwaretests liegt auf zwei Attributen: automatisch und wiederholbar. Dazu ist eine Bibliothek nötig, die zwei Dinge unterstützen muss.
· Testfälle sehen immer gleich aus und bestehen aus drei Schritten. Zunächst wird ein Szenario aufgebaut, dann die zu testende Methode oder Methodenkombination aufgerufen und zum Schluss mit der spezifischen API vom Testframework geschaut, ob das ausgeführte Programm genau das gewünschte Verhalten gebracht hat. Das Übernehmen eine Art „stimmt-es-dass“-Methoden, die den gewünschten Zustand mit dem tatsächlichen Abgleichen und beim Konflikt eine Ausnahme auslösen.
· Das Testframework muss die Tests laufen lassen und im Fehlerfall eine Meldung geben; der Teil nennt sich Test-Runner.
Wir werden uns im Folgenden auf sogenannte Unit-Tests beschränken. Das sind Tests, die einzelne Bausteine (engl. units) testen. Daneben gibt es andere Tests, wie Lasttests, Performance-Tests oder Integrationstests, die aber im Folgenden keine große Rolle spielen.
1.2 Das Testframework JUnit
Sun definiert kein allgemeines Standardframework zur Definition von Unit-Testfällen, noch bietet es eine Ablaufumgebung für Testfälle. Diese Lücke füllen Testframeworks, wobei die populärsten im Java-Bereich das freie quelloffene JUnit (http://www.junit.org/) und (mit Abstand) TestNG (http://testng.org/) sind. Im Folgenden wollen wir uns auf JUnit konzentrieren, ein Framework, welches von Kent Beck und Erich Gamma entwickelt wurde. Um die aktuellste Version von JUnit im Klassenpfad zu haben (IDEs tendieren zu älteren Versionen, auch wenn sich JUnit nicht in Lichtgeschwindigkeit entwickelt). Dazu ist von der Download-Seite der JUnit-Homepage, die auf http://sourceforge.net/projects/junit/files/junit/ weiterleitet, das entsprechende Jar-Archiv zu laden, etwa junit-dep-4.7.jar (junit-4.7.jar sollte es für das spätere Hamcrest nicht sein, funktioniert aber sonst), und im Klassenpfad aufzunehmen.
Die Standard-IDEs Eclipse, NetBeans, IntelliJ bringen JUnit gleich mit und bieten Wizards zum einfachen Erstellen von Testfällen aus vorhandenen Klassen an. Über Tastendruck lassen sich Testfälle abarbeiten und ein farbiger Balken zeigt direkt an, ob wir unsere Arbeit gut gemacht haben.
1.2.1 Test-Driven-Development und Test-First
Unser JUnit-Beispiel wollen wir nach einem ganz speziellen Ansatz entwickeln, welches sich Test-First nennt. Dabei wird der Testfall noch vor der eigentlichen Implementierung geschrieben. Die Reihenfolge mit dem Test-First-Ansatz sieht (etwas erweitert) so aus:
- Überlege, welche Klasse und Methode geschrieben werden soll. Lege Quellcode für die Klasse und Variablen/Methoden/Konstruktoren an, sodass sich die Compilationsheit übersetzen lässt.
- Schreibe die API-Dokumentation für die Methoden/Konstruktoren und überlege, welche Parameter, Rückgaben, Ausnahmen nötig sind.
- Teste die API an einem Beispiel, ob sich die Klasse mit Eigenschaften „natürlich“ anfühlt. Falls nötig wechsele zu Punkt 1 und passe die Eigenschaften an.
- Implementiere eine Testklasse.
- Implementiere die Logik des eigentlichen Programms.
- Gibt es durch die Implementierung neue Dinge, die ein Testfall testen sollte? Wenn ja, erweitere den Testfall.
- Führe die Tests aus und wiederhole Schritt 5 bis alles fehlerfrei läuft.
Test-Driven-Development hat den großen Vorteil, überschnelle Entwickler, die ohne groß zu denken zur Tastatur greifen, dann implementieren und nach 20 Minuten wieder alles ändern, zum Nachdenken zwingt. Große Änderungen kosten Zeit und somit Geld und Test-First verringert die Notwendigkeit späterer Änderungen. Denn wenn Entwickler Zeit in die API-Dokumentation investieren und Testfälle schreiben, dann haben sie eine sehr gute Vorstellung über die Arbeitsweise der Klasse und große Änderungen sind seltener.
Der Test-First-Ansatz ist eine Anwendung von Test-Driven-Development (TDD). Hier geht es darum, die Testbarkeit gleich als Ziel bei der Softwareentwicklung zu definieren. Hieran krankten frühere Entwicklungsmodelle, etwa das wohlbekannte Wasserfallmodell, welches Testen an das Ende nach Analyse, Design und Implementierung stellte. Die Konsequenz dieser Reihenfolge war oft ein großer Klumpen von Programmcode der unmöglich zu testen war. Mit TDD soll das nicht mehr passieren. Heutzutage sollten Entwickler sich bei jeder Architektur, jedem Design und der Klasse gleich zu Beginn überlegen, wie das Ergebnis zu testen ist. Untersuchungen zeigen: Mit TDD ist das Design signifikant besser.
Zur Frage wann Tests durchgeführt werden sollen lässt sich nur eins festhalten: So oft wie möglich. Denn je eher ein Test durch eine falsche Programmänderung fehlschlägt, desto eher kann der Fehler behoben werden. Gute Zeitpunkte sind daher vor und hinter größeren Designänderungen und auf jeden Fall vor dem Einpflegen in die Versionsverwaltung. Im modernen Entwicklungsprozess gibt es einen Rechner, auf dem eine Software zur kontinuierlichen Integration läuft (engl. Continuous Integration). Diese Systeme integrieren einen Build-Server, der die Quellen automatisch aus einer Versionsverwaltung auscheckt, compiliert und dann Testfälle und weitere Metriken laufen lässt. Diese Software übernimmt dann einen Integrationstest, da hier alle Module der Software zu einer Gesamtheit zusammengebaut werden und so Probleme aufgezeigt werden, die sich vielleicht bei isolierten Tests auf den Entwicklermaschinen nicht zeigen.
1.2.2 Testen, implementieren, testen, implementieren, testen, freuen
Bisher bietet Java keine einfache Funktion, um Strings umzudrehen. Unser erstes JUnit-Beispiel soll daher um eine Klasse StringUtils gestrickt werden, die eine statische Methode reverse() anbietet. Nach dem TDD-Ansatz implementieren wir eine Klasse, die korrekt übersetzt werden kann, aber bisher ohne Funktionalität ist. (Auf die API-Dokumentation verzichtet das Beispiel.)
com/tutego/insel/junit/util/StringUtils.java, StringUtils
public class StringUtils { public static String reverse( String string ) { return null; } }
Gegen diese eigene API lässt sich nun der Testfall schreiben. Spontan fällt uns ein, dass ein Leerstring umgedreht natürlich auch ein Leerstring ergibt und die Zeichenkette „abc“ umgedreht „cba“ ergibt. Ziel ist es, eine möglichst gute Abdeckung aller Fälle zu bekommen. Wenn wir Fallunterscheidungen im Programmcode vermuten sollten wir versuchen so viele Testfälle zu finden, sodass alle diese Fallunterscheidungen abgelaufen werden. Interessant sind beim Eingeben immer Sonderfälle bzw. Grenzen von Wertebereichen. (Unsere Methode gibt da nicht viel her, aber wenn wir etwa eine Substring-Funktion haben, lassen sich schnell viele Methodenübergaben finden, die interessant sind.)
com/tutego/insel/junit/util/StringUtilsTest.java
package com.tutego.insel.junit.util; import static org.junit.Assert.*; import org.junit.Test; public class StringUtilsTest { @Test public void testReverse() { assertEquals( "", StringUtils.reverse( "" ) ); assertEquals( "cba", StringUtils.reverse( "abc" ) ); } }
Die Klasse zeigt vier Besonderheiten.
- Die Methode, die sich einzelne Szenarien vornehmen und Klassen/Methoden testen, tragen die Annotation @Test.
- Eine übliche Namenskonvention (obwohl nicht zwingend nötig) ist, dass die Methode, die den Test enthält mit dem Präfix „test“ beginnt und mit dem Namen der Methode endet, die sie testet. Da in unserem Fall die Methode reverse() getestet wird, heißt die Testmethode dementsprechend „testReverse“. Eine testXXX()-Methode liefert nie eine Rückgabe. Testmethoden können auch ganze Szenarien testen, die nicht unbedingt an einer Methode festzumachen sind, aber hier testest testReverse() nur die reverse()-Methode.
- JUnit bietet eine Reihe von assertXXX()-Methoden, die den erwarteten Zustand mit dem Ist-Zustand vergleichen; gibt es Abweichungen folgt eine Ausnahme. assertEquals() nimmt einen equals()-Vergleich der beiden Objekte vor. Wenn demnach StringUtils.reverse(„“) die leere Zeichenkette „“ liefert, ist alles in Ordnung und der Test wird fortgesetzt.
- Der statische Import aller statischen Eigenschaften der Klasse org.junit.Assert kürzt die Schreibweise ab, sodass im Programm statt Assert.assertEquals() nur assertEquals() geschrieben werden kann.
1.2.3 JUnit-Tests ausführen
In einer Entwicklungsumgebung lässt sich die Testausführung leicht ablaufen. Eclipse zeigt zum Beispiel die Ergebnisse in der JUnit-View an und bietet mit einem grünen bzw. roten Balken direktes visuelles Feedback.
Natürlich lassen sich die Tests auch von Kommandozeile ausführen, obwohl das selten ist, denn in der Regel werden die Tests im Zuge eines Build-Prozess – den etwa Ant steuert – angestoßen. Wer das dennoch über die Kommandozeile machen möchte, schreibt:
com/tutego/insel/junit/RunTest.java, main()
JUnitCore.main( StringUtilsTest.class.getName() );
1.2.4 assertXXX()-Methoden der Klasse Assert
Assert ist die Klasse mit den assertXXX()-Methoden, die immer dann einen AssertionError auslösen, wenn ein aktueller Wert nicht so wie der gewünschte war. Der JUnit-Runner registriert alle AssertionError und speichert sie für die Statistik. Bis auf drei Ausnahmen beginnen alle Methoden der Klasse Assert mit dem Präfix „assert“ – zwei andere heißen fail() und eine isArray() – und alle Methoden gibt es einmal mit einer Testmeldung, die beim AssertionError dann erscheint, und einmal ohne, wenn JUnit keine extra Meldung angeben soll.
Eigentlich reicht zum Testen die Methode assertTrue(boolean condition) aus. Ist die Bedingung wahr, so ist alles in Ordnung. Wenn nicht, gibt es den AssertionError.
org.junit.Assert |
- static void assertTrue( boolean condition )
- static void assertTrue( String message, boolean condition )
- static void assertFalse( boolean condition )
- static void assertFalse( String message, boolean condition )
Um es Entwicklern etwas komfortabler zu machen, bietet JUnit sechs Kategorien von Hilfsmethoden. Zunächst sind es assertNull() und assertNotNull(), die testen, ob das Argument null bzw. nicht null ist. Ein Aufruf von assertNull(Object object) ist dann nichts anderes als assertTrue(object == null).
- static void assertNotNull( Object object )
- static void assertNotNull( String message, Object object )
- static void assertNull( Object object )
- static void assertNull( String message, Object object )
Die nächste Kategorie testet, ob das Objekt identisch (nicht equals()-gleich) mit einem anderen Objekt ist.
- static void assertNotSame( Object unexpected, Object actual)
- static void assertNotSame( String message, Object unexpected, Object actual)
- static void assertSame( Object expected, Object actual)
- static void assertSame( String message, Object expected, Object actual)
Statt einem Referenztest führen die folgenden Methoden einen equals()-Vergleich durch:
- static void assertEquals( Object expected, Object actual )
- static void assertEquals( String message, Object expected, Object actual )
Zum Testen von primitiven Datentypen gibt es nur zwei Methoden. (Das reicht, denn zum einen werden anderen primitive Typen automatisch typangepasst, zum zweiten kommt Boxing dann ins Spiel, sodass assertEquals(Object, Object) wieder passt.
- static void assertEquals( long expected, long actual )
- static void assertEquals( String message, long expected, long actual )
- static void assertEquals( double expected, double actual, double delta )
- static void assertEquals( String message, double expected, double actual, double delta )
Fließkommazahlen bekommen bei assertEquals() einen Delta-Wert, in dem sich das Ergebnis bewegen muss. Das trägt der Tatsache Rechnung, dass vielleicht in der Bildschirmausgabe zwei Zahlen gleich sind, jedoch nicht bitweise gleich sind, da sich kleine Rechenfehler akkumuliert haben. Sind jedoch die Fließkommazahlen in einem Wrapper, also etwa Double verpackt, leitet ja assertEquals() den Test nur an die equals()-Methode der Wrapper-Klasse weiter, die natürlich kein Delta berücksichtigt.
Als letztes folgen Methoden, die Felderinhalte vergleichen:
- static void assertArrayEquals( byte[] expecteds, byte[] actuals )
- static void assertArrayEquals( String message, byte[] expecteds, byte[] actuals )
- static void assertArrayEquals( char[] expecteds, char[] actuals )
- static void assertArrayEquals( String message, char[] expecteds, char[] actuals )
- static void assertArrayEquals( int[] expecteds, int[] actuals )
- static void assertArrayEquals( String message, int[] expecteds, int[] actuals )
- static void assertArrayEquals( long[] expecteds, long[] actuals )
- static void assertArrayEquals( Object[] expecteds, Object[] actuals )
- static void assertArrayEquals( String message, long[] expecteds, long[] actuals )
- static void assertArrayEquals( String message, Object[] expecteds, Object[] actuals )
- static void assertArrayEquals( short[] expecteds, short[] actuals )
- static void assertArrayEquals( String message, short[] expecteds, short[] actuals )
1.2.5 Matcher-Objekte und Hamcrest
Eine Sonderrolle nehmen zwei assertThat()-Methoden ein. Sie ermöglichen es, Tests deklarativer zu schreiben, so dass sie sich wie englische Sätze lesen lassen. Stellen wir bei einigen Beispielen assertThat() und assertEquals() bei den gleichen Aufgaben gegenüber.
assertXXX() ohne Matcher | assertThat() |
assertNotNull(new Object()); | assertThat(new Object(), is( notNullValue() )); |
assertEquals(„“, StringUtils.reverse( „“ )); | assertThat(„“, is( equalTo( StringUtils.reverse( „“ ) ) )); |
assertSame(„“, „“); | assertThat(„“, is( sameInstance( „“ ) )); |
assertNotSame(„“, „a“); | assertThat(„a“, is( not( sameInstance( „“ ) ) )); |
Zunächst fällt auf, dass als erstes Argument bei assertThat() den Wert beschreibt den wir haben, dieser aber bei den sonstigen assertXXX()-Methoden erst immer hinter dem erwarteten Wert folgt.
Die allgemeine Syntax von assertThat() ist folgende.
org.junit.Assert |
- static <T> void assertThat( T actual, org.hamcrest.Matcher<T> matcher )
- static <T> void assertThat( String reason, T actual, org.hamcrest.Matcher<T> matcher )
Ob der Test korrekt ist oder nicht, entscheidet ein org.hamcrest.Matcher-Objekt. An dem Paket org.hamcrest lässt sich schon ablesen, dass nicht JUnit auf die Idee kam, sondern auf eine Bibliothek namens Hamcrest zurückgreift, die unter http://code.google.com/p/hamcrest/ gehostet ist. Um daher die volle Funktionalität von Hamcrest zu nutzen, sollte von der Webseite das Java-Archiv hamcrest-all-1.2.jar geladen und im Klassenpfad eingebunden werden. (hamcrest-core-1.2.jar ist eine Alternative, enthält aber nur die grundlegenden Klassen und ist nicht so nützlich.)
Da die Matcher über eine clevere Art verschachtelt werden, lesen sich die assertThat()-Aufrufe wie Sätze. is() hat funktional keine Bedeutung, lässt die Aussage aber noch „englischer“ werden. Aber wo sind bei einem Aufruf wie assertThat(„“, not(sameInstance(„a“))); die Objekte? Zunächst gilt, dass die „Wörter“ statische Methoden der Klasse org.hamcrest.CoreMatchers sind. Statisch eingebunden ergibt sich die kurze Schreibweise, die sonst lauten würde: CoreMatchers.not(CoreMatchers.sameInstance(„a“)). Die Matcher-Objekte sind einfach Rückgaben der statischen Methoden. Wir hätten auch schreiben können: new IsNot<String>(new IsSame<String>(„a“)) und das ist unser Matcher-Objekt. Matcher-Objekte besitzen eine Methode boolean matches(Object item), die letztendlich den Test durchführt und assertThat() sagt, ob eine Ausnahme ausgelöst werden muss, weil ein Fehler auftrat.
Die Methoden is(), isInstance(), not(), notNullValue(), equalTo() sind nicht die einzigen aus CoreMatchers. Die folgende Übersicht zählt die aktuellen statischen Methoden auf:
org.hamcrest.CoreMatchers |
- static <T> Matcher<T> allOf( Iterable<Matcher<? extends T>> matchers )
- static <T> Matcher<T> allOf( Matcher<? extends T>… matchers )
- static <T> Matcher<T> any( Class<T> type )
- static <T> Matcher<T> anyOf( Iterable<Matcher<? extends T>> matchers )
- static <T> Matcher<T> anyOf( Matcher<? extends T>… matchers )
- static <T> Matcher<T> anything()
- static <T> Matcher<T> anything( String description )
- static <T> Matcher<T> describedAs( String description, Matcher<T> matcher, Object… values )
- static <T> Matcher<T> equalTo( T operand )
- static Matcher<Object> instanceOf( Class<?> type )
- static Matcher<Object> is( Class<?> type )
- static <T> Matcher<T> is( Matcher<T> matcher )
- static <T> Matcher<T> is( T value )
- static <T> Matcher<T> not( Matcher<T> matcher )
- static <T> Matcher<T> not( T value )
- static <T> Matcher<T> notNullValue()
- static <T> Matcher<T> notNullValue( Class<T> type )
- static <T> Matcher<T> nullValue()
- static <T> Matcher<T> nullValue( Class<T> type )
- static <T> Matcher<T> sameInstance( T object )
Vorteile von Matcher-Objekten
Stellen wir noch einmal
- assertEquals( „“, StringUtils.reverse( „“ ) ); und
- assertThat( „“, is( equalTo( StringUtils.reverse( „“ ) ) ) );
gegenüber. Ist assertThat() die bessere Alternative? Nicht wirklich, denn es gibt keinen Nutzen, wenn assertThat() exakt das übernimmt, was die assertXXX()-Methode macht, also in unserem Fall ein Gleichheitstest. assertThat() ist dann sogar länger.
Interessant wird assertThat() aus zwei Gründen:
1. Es gibt eine große Sammlung an Matcher-Objekten, die einem Programmierer viel Arbeit abnehmen. Wenn es zu prüfen gilt, ob die zu testende Methode eine Datei mit genau 1111 Bytes angelegt hat, so müsste das in Java etwa new File(file).exists() && new File(file).length() == 1111 lauten. Mit entsprechenden Hamcrest-Matchern[1] heißt es dann kompakt:
allOf(exists(), sized((Matcher<Long>)equalTo(0L))).[2] Ein zweites Beispiel betrifft Mengenabfragen. Die praktische Methode hasItems() zum Beispiel testet, ob Elemente in einer Collection sind; ohne Matcher wäre der Test in Java mehr Schreibarbeit.
2. Die assertEquals()-Methode läuft entweder durch, oder bricht mit einer Exception ab, was den Test dann beendet. Wir bekommen beim Abbruch dann den Hinweis, dass der gewünschte Wert nicht mit dem berechneten übereinstimmt, aber wo genau der Fehler ist, fällt weniger auf. assertThat() liefert ausgezeichnete Fehlermeldungen.
Folgendes Beispiel fasst die beiden Vorteile zusammen. Stellen wir uns vor, wir haben eine Datenstruktur (in dem Beispiel eine ArrayList). Sie kann Elemente auch entfernen und das ist genau die Methode removeAll(), dessen Funktionalität wir testen wollen.
ArrayList<String> list = new ArrayList<String>(); Collections.addAll( list, "a", "b", "c", "d", "e" ); list.removeAll( Arrays.asList( "b", "d" ) );
Wie kann ein Test aussehen? Ein Test könnte schauen, ob die Größe der Liste von 5 Elementen auf 3 kommt, wenn die beiden Elemente „b“ und „d“ gelöscht werden. Und der Test kann prüfen, ob b und d wirklich entfernt wurde, aber „a“, „c“ und „e“ weiterhin in der Liste sind. Nach dem Import von
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
lässt sich genau das über assertThat() testen:
assertThat( list, hasSize(3) ); assertThat( list, both( hasItems( "a", "c", "e" ) ).and( not( hasItems( "b", "d" ) ) ) );
Die Methode hasSize() prüft die Größe der Liste, hasItems() testet, ob Elemente in der Datenstruktur sind. Die Kombination both().and() prüft zwei Bedingungen, die beide erfüllt sein müssen. Alternativ wäre auch allOf() möglich.
So machen diese beiden Zeilen den ersten Punkt der beiden Vorteile für Hamcrest-Matcher deutlich.
Der zweite Vorteil waren die Meldungen im Fehlerfall. Ändern wir zum Test das erste hasItems() in hasItems(„_“, „c“, „e“). Der Testlauf wird dann natürlich einen Fehler geben. Die Meldung ist (etwas eingerückt):
Expected: ( (a collection containing "_" and a collection containing "c" and a collection containing "e") and not (a collection containing "b" and a collection containing "d") ) got: <[a, c, e]>
Der Hinweis „got: <[a, c, e]>“ gilt für den ersten Matcher und nicht wie sonst üblich bei assertEquals() für den gesamten Ausdruck. So sind die Aussagen deutlicher lokaler und nicht wie bei Anfragen, die auf assertTrue() zurückgehen, einfach nur, dass false kam, aber true erwartet wurde.
1.2.6 Exceptions testen
Während der Implementierung fallen oft Dinge auf, die die eigentliche Implementierung noch nicht berücksichtigt. Dann sollte sofort diese neu gewonnene Erkenntnis im Testfall einfließen. In unserem Beispiel soll das bedeuten, dass bisher nicht wirklich geklärt ist, was bei einem null-Argument passieren soll. Bisher gibt es eine NullPointerExcpetion und das ist auch völlig in Ordnung, aber in einem Testfall steht das bisher nicht, dass auch wirklich eine NullPointerExcpetion folgt. Diese Fragestellung betont eine gern vergessene Seite des Testens, denn Testautoren dürfen sich nicht nur darauf konzentrieren, was die Implementierung denn so alles richtig machen soll, der Test muss auch kontrollieren, ob im Fehlerfall auch dieser korrekt gemeldet wird. Wenn es nicht in der Spezifikation steht dürfen auf keinen Fall falsche Werte gerade gebügelt werden: falsche Werte müssen immer zu einer Ausnahme oder einem wohl definierten Verhalten führen.
Wir wollen unser Beispiel so erweitern, dass reverse(null) eine IllegalArgumentException auslöst. Auf zwei Arten lässt sich testen, ob die erwartete IllegalArgumentException auch wirklich kommt. Die erste Variante:
com/tutego/insel/junit/utils/StringUtilsTest.java, Ausschnitt
try { StringUtils.reverse( null ); fail( "reverse(null) should throw IllegalArgumentException" ); } catch ( IllegalArgumentException e ) { }
Führt reverse(null) zur Ausnahme, was ja gewollt ist, wird der catch-Block die IllegalArgumentException einfach auffangen, ignorieren und dann geht es in der Testfunktion mit anderen Dingen weiter. Sollte keine Ausnahme folgen, so wird die Anweisung nach dem reverse()-Aufruf ausgeführt, und die ist fail(). Diese Methode löst eine JUnit-Ausnahme mit einer Meldung aus und signalisiert dadurch, dass im Test etwas nicht stimmte. Allerdings bleibt ein Problem: Was ist, wenn zwar eine Ausnahme ausgelöst wird, aber eine Falsche! Eine nicht-Runtime-Exeception kann es nicht sein, dann da würde der Compiler uns zum catch-Block zwingen. Aber was wäre mit einer anderen RuntimeException, etwa der NullPointerException? Diese würde zwar von JUnit abgefangen werden und JUnit würde einen Fehler melden, aber dann ist nicht abzulesen, was das für ein Fehler ist und welche Rolle er spielt. Eine Lösung wäre, noch einen catch-Block anzuhängen und fail() aufzurufen, doch das würde erst einmal Quellcodeduplizierung bedeuten. Daher bietet JUnit eine andere elegante Variante – die Annotation @Test wird parametrisiert.
com/tutego/insel/junit/util/StringUtilsTest.java, testReverseException()
@Test( expected = IllegalArgumentException.class ) public void testReverseException() { StringUtils.reverse( null ); }
JUnit erwartet (engl. expected) eine IllegalArgumentException. Folgt sie nicht, meldet JUnit das als Fehler. Der Vorteil gegenüber der fail()-Variante ist die Kürze, ein Nachteil der, dass dann unter Umständen mehrere testXXX()-Methoden für eine zu testende Methode nötig sind. Wir haben hier eine zweite Methode testReverseException() hinzugenommen. (Der reverse(null)-Test hätte auch am Ende der ersten Methode stehen können, dann müssen wir aber sichergehen, dass dieser Exception-Test am Ende durchgeführt wird.
1.2.7 Tests ignorieren und Grenzen für Ausführungszeiten festlegen
Durch Umstrukturierung von Quellcode kann es sein, dass Testcode nicht länger gültig ist und entfernt oder umgebaut werden muss. Damit der Testfall nicht ausgeführt wird, muss er nicht auskommentiert werden (das bringt den Nachteil mit sich, dass das Refactoring etwa im Zuge einer Umbenennung von Bezeichnern sich nicht auf auskommentierte Bereiche auswirkt.) Stattdessen wird einfach eine weitere Annotation @Ignore an die Methode gesetzt:
@Ignore @Test public void testReverse()
Nach großen Refactorings kann die Software funktional noch laufen, aber vielleicht viel langsamer geworden sein. Dann stellt sich die Frage, ob das im Sinne des Anforderungskataloges noch korrekt ist, wenn ein performantes Programm nach einer Änderung läuft wie eine Schnecke. Um Laufzeitveränderungen als Gültigkeitskriterium einzuführen, kann die Annotation @Test ein timeout in Millisekunden angeben.
@Test( timeout = 500 ) public void test()
Wird die Testmethode dann nicht innerhalb der Schranke ausgeführt, gilt das als versagter Test und JUnit meldet einen Fehler.
1.2.8 Mit Methoden der Assume-Klasse Tests abbrechen
Während die assertXXX()-Methoden zu einer Ausnahme führen und so anzeigen, dass der Test etwas gefunden hat, was nicht korrekt ist, bietet JUnit mit Asssume.assumeXXX()-Methoden die Möglichkeit, die Tests nicht fortzuführen. Das ist sinnvoll etwa dann, wenn die Testausführung nicht möglich ist, wenn etwa der Testrechner keine Grafikkarte hat, das Netzwerk nicht da ist oder das Datensystem voll. Dabei geht es nicht darum, etwa zu testen, wie sich die Routine bei einem nicht vorhandenen Netzwerk verhält – das gilt es natürlich auch zu testen. Aber steht das Netzwerk nicht, können logischerweise auch keine Tests laufen, die das Netzwerk zwingend benötigen.
org.junit.Assume |
- static void assumeTrue( boolean b )
- static void assumeNotNull( Object… objects )
- static void assumeNoException( Throwable t )
- static <T> void assumeThat( T actual, org.hamcrest.Matcher<T> matcher )
Die assumeXXX()-Methoden führen zu keiner Ausnahme, brechen die Testausführung aber ab.
1.3 Wie gutes Design das Testen ermöglicht
Statische Methoden nach dem Muster: Parameter liefert die Eingabe, der Rückgabewert das Ergebnis, sind einfach zu testen. Sie verändern keine Umgebung und Zustände gibt es keine. Der Testfall muss lediglich die Rückgabe untersuchen und das ist einfach. Aufwändiger wird es dann schon, wenn Dinge getestet werden wollen, die aufwändige Systemänderungen nach sich ziehen: Wurde eine Datei angelegt? Stehen Dinge in der Datenbank wie gewünscht? Hat der Cluster die Daten auf andere Server gespiegelt? Liefert ein externes Programm die Rückgabe wie erwartet? Gibt eine native Methode tatsächlich das zurück was sie verspricht und zieht nicht die JVM ins Grab?
Sind Dinge plötzlich nicht mehr testbar, offenbart das im Allgemeinen ein schwaches Design. Häufig liegt es daran, dass eine Klasse zu viele Verantwortlichkeiten hat. Als Beispiel wollen wir uns eine Klasse ansehen, die Visitenkarten im vCard-Format (Dateiendung vcf) schreibt.[3] Um den Quellcode schlank zu halten verzichtet die Klasse VCard auf Setter/Getter.
com/tutego/insel/junit/util/vdf/v1/VCard.java, VCard
public class VCard { public String formattedName; public String email; public void export( String filename ) throws IOException { StringBuilder result = new StringBuilder( "BEGIN:VCARD\n" ); if ( formattedName != null && ! formattedName.isEmpty() ) result.append( "FN:" ).append( formattedName ).append( "\n" ); if ( email != null && ! email.isEmpty() ) result.append( "EMAIL:" ).append( email ).append( "\n" ); new FileWriter( filename ).append( result.append( "END:VCARD" ).toString() ).close(); } }
Wenn die Klasse etwa die Variable formattedName auf „Powerpuff Girls“ steht und email auf „powerpuff@townsville.com“, dann würde die Methode export() eine Datei erstellen mit dem folgenden Inhalt:
BEGIN:VCARD FN:Powerpuff Girls EMAIL:powerpuff@townsville.com END:VCARD
Hauptaufgabe der Klasse ist die korrekte Erstellung des Ausgabeformates nach dem vCard-Standard. Die Klasse lässt sich grundsätzlich testen, aber der Test wird nicht besonders schön. Zunächst müssten unterschiedliche vCard-Eigenschaften gesetzt werden, dann die vCard in eine Datei geschrieben, anschließend die Datei geöffnet, den Inhalt ausgelesen und zum Schluss auf Korrektheit untersucht werden. Das ist kein sympathischer Weg! Die Klasse VCard ist nicht testorientiert entworfen worden. Warum? Neben der Tatsache, dass so ein Test wegen der Dateizugriffe recht lange dauern könnte, lässt sich prinzipiell festhalten, dass die Methode export() zwei Verantwortlichkeiten verbindet, nämlich die Ausgabe in dem speziellen vCard-Format und die Ausgabe in eine Datei. Stünde das Prinzip TDD hinter dem Entwurf, so hätte der Autor die Anteile Format und Ausgabe getrennt. Denn gäbe es eine eigene Methode zur Aufbereitung der Dateien etwa in einem String, so müsste der Test nur diese Methode aufrufen und bräuchte nicht in einen String schreiben. Verbessern wir die Klasse:
com/tutego/insel/junit/util/vdf/v2/VCard.java, VCard
public class VCard { public String formattedName; public String email; public void export( Writer out ) throws IOException { out.write( toString() ); } public void export( String filename ) throws IOException { FileWriter writer = new FileWriter( filename ); export( writer ); writer.close(); } @Override public String toString() { StringBuilder result = new StringBuilder( "BEGIN:VCARD\n" ); if ( formattedName != null && ! formattedName.isEmpty() ) result.append( "FN:" ).append( formattedName ).append( "\n" ); if ( email != null && ! email.isEmpty() ) result.append( "EMAIL:" ).append( email ).append( "\n" ); return result.append( "END:VCARD" ).toString(); } }
Die Variante bringt gleich zwei Verbesserungen mit sich:
a) Die Methode toString() liefert nun den nach dem vCard-Standard aufbereiteten String. Der Test muss nun lediglich ein VCard-Objekt aufbauen, die Variablen setzen, toString() aufrufen und ohne Dateioperationen den String auf Korrektheit testen. Für den Client ändert sich die API aber nicht; er schreibt weiterhin export().
b) Direkt in Dateien zu schreiben ist nicht mehr so richtig zeitgemäß. Das berücksichtigt die Klasse und bietet eine überladene Version von export() mit einem allgemeinen Writer. Sollte dann etwa eine vCard über das Netzwerk verschickt werden, ist das kein Problem und es muss lediglich ein passender Writer für das Netzwerkziel übergeben werden. Vorher wäre das sehr umständlich gewesen: Datei erzeugen, Datei auslesen, String verschicken.
Im Endeffekt ist der Gewinn groß. Der Test ist performanter und das Design führt zu besserem Quellcode. Eine Win-Win-Situation.
Der gewählte Ansatz zeigt den Ansatz, wie bei Implementierungen zu Verfahren ist, die insbesondere mit externen Ressourcen sprechen. Diese gilt es soweit wie möglich rauszuziehen, wenn nötig, auch in einem neuen Typ, der dann als Testimplementierung injiziert werden kann.
1.4 Aufbau größerer Testfälle
1.4.1 Fixtures
Eine wichtige Eigenschaft von Tests ist, dass sie voneinander unabhängig sind. Die Annahme, dass ein erster Test ein paar Testdaten zum Beispiel anlegt, auf die dann der zweite Test zurückgreifen kann ist falsch. Aus dieser Tatsache muss die Konsequenz gezogen werden, dass jede einzelne Testmethode davon ausgehen muss die erste Testmethode zu sein und somit ihren Initialzustand selbst herstellen muss. Es wäre aber unnötige Quellcodeduplizierung, wenn jede Testmethode nun diesen Startzustand selbst aufbaut. Dieser Anfangszustand heißt Fixture (zu Deutsch etwa festes Inventar) und JUnit bietet hier vier Annotationen. Wie sie wirken, zeigt folgendes Beispiel:
com/tutego/insel/junit/util/FixtureDemoTest.java, FixtureDemoTest
public class FixtureDemoTest { @BeforeClass public static void beforeClass() { System.out.println( "@BeforeClass" ); } @AfterClass public static void afterClass() { System.out.println( "@AfterClass" ); } @Before public void setUp() { System.out.println( "@Before" ); } @After public void tearDown() { System.out.println( "@After" ); } @Test public void test1() { System.out.println( "test 1" ); } @Test public void test2() { System.out.println( "test 2" ); } }
Die Annotationen beziehen sich auf zwei Anwendungsfälle:
- @BeforeClass, @AfterClass: Annotiert statische Methoden, die einmal aufgerufen werden, wenn die Klasse für den Test initialisiert wird bzw. wenn alle Tests für die Klasse abgeschlossen sind.
- @Before, @After: Annotiert Objektmethoden, die immer vor bzw. nach einer Testmethode aufgerufen werden.
Läuft unser Beispielprogramm ist die Ausgabe daher wie folgt:
@BeforeClass
@Before
test 1
@After
@Before
test 2
@After
@AfterClass
In die @BeforeClass-Methoden wird üblicherweise das reingesetzt, was teuer im Aufbau ist, etwa eine Datenbankverbindung. Die Ressourcen werden dann in der symmetrischen Methode @AfterClass wieder freigegeben, also zum Beispiel Datenbankverbindungen wieder geschlossen. Da nach einem Test keine Artefakte vom Testfall bleiben sollen führen gute @AfterClass/@After-Methoden sozusagen ein Undo durch.
Beispiel Setzt ein System.setProperty() „globale“ Zustände, oder überschreibt es vordefinierte Properties, so ist @BeforeClass eine guter Zeitpunkt, einen Snapshot zu nehmen und diesen später bei @AfterClass wieder herzustellen.
private static String oldValue; @BeforeClass public static void beforeClass() { oldValue = System.getProperty( "property" ); System.setProperty( "property", "newValue" ); } @AfterClass public static void afterClass() { if ( oldValue != null ) { System.setProperty( "property", oldValue ); oldValue = null; } } |
1.4.2 Sammlungen von Testklassen und Klassenorganisation
Werden die Tests mehr, stellt sich die Frage nach der optimalen Organisation. Als praktikabel hat sich erwiesen, die Testfälle in das gleiche Paket wie die zu testenden Klassen zu setzen, aber den Quellcode physikalisch zu trennen. Entwicklungsumgebungen bieten hierzu Konzepte, etwa Eclipse unterschiedliche src-Order, die zu Klassen im gleichen Klassenpfad führen, dennoch in der IDE visuell getrennt sind. Der Vorteil von Klassen im gleichen Paket ist, das oftmals die Paketsichtbarkeit ausreicht und nicht vorher private Eigenschaften nur für Tests öffentlich gemacht werden müssen.
Wenn es nun für jedes Paket ein Spiegel-Paket mit den Testklassen gibt, wie werden all diese Testsklassen eines Pakets in einem Rutsch ausgeführt? Der Trick besteht im Aufbau einer Test-Suite:
com/tutego/insel/junit/util/TestPackage.java, TestPackage
@RunWith( Suite.class ) @Suite.SuiteClasses( { StringUtilsTest.class, FixtureDemoTest.class } ) public class PackageTest { }
Die Testklasse ist im Rumpf leer und zählt lediglich über Suite.SuiteClasses die Klassen aus.
Üblicherweise besteht ein Projekt aus mehreren hierarchischen Paketen. Wie ist es dann möglich, das ganze Projekt mit allen Pakten zu testen und nicht nur ein Paket? Hier hilft uns die Tatsache, dass eine Suite selbst eine Art Testfall ist, die bei @Suite.SuiteClasses angegeben werden darf. Das heißt praktisch: In jedes Paket wird eine Suite definiert, die alle Testklassen aber auch die Test-Suite der Unterklassen referenziert. In unserem Beispiel heißt dass, wenn wir in com.tutego.insel.junit.util eine Suite PackageTest hatten, die die beiden TestklassenStringUtilsTest und FixtureDemoTest ansprachen, so setzen wir in das übergeordnete Paket com.tutego.insel.junit (ohne util also) ebenfalls eine Suite PackageTest, die dann com.tutego.insel.junit.util.PackageTest referenziert.
com/tutego/insel/junit/TestPackage.java, TestPackage
@RunWith( Suite.class ) @Suite.SuiteClasses( { com.tutego.insel.junit.util.PackageTest.class } ) public class PackageTest { }
Der eigentliche Test wird demnach oben in einer Haupthierarchie gestartet und läuft rekursiv dann alle Unterpakte ab.
1.5 Dummy, Fake, Stub und Mock
Gute objektorientiert entworfene Systeme kennzeichnen sich dadurch, dass es eine hohe Interaktion mit anderen Objekten gibt. Idealerweise zerlegt eine Klasse ein Problem nur bis zu dem Punkt, bis es sich einer anderen Klasse bedienen kann, die dieses einfachere Problem löst. Schwierig wird es, wenn eine eigene Klasse auf eine andere komplexe Klasse zurückgreift und das Objekt nur dann sinnvoll arbeitet, wenn das referenzierte Objekt da ist, und irgendwie sinnvoll antwortet. Diese Abhängigkeit ist ungünstig, denn das Ziel eines guten Test besteht ja darin, lokal zu sein, also die eigentliche Klasse zu testen, und nicht alle referenzierten Klassen um sie herum gleich mit.
In der Praxis begegnen uns drei Fälle:
· Fake-Objekte: Sie sind eine gültige Implementierung einer Schnittselle, haben aber kein Verhalten, sondern der Rumpf der Methoden ist quasi leer. Sie gibt es nur für die Testfälle. Wenn ein Service etwa auf einen anderen Service zurückgreift, um eine E-Mail mit der einzigen angebotenen Methode void send(String msg, String receiver) zu versenden, kann ein Fake-Objekt diesen E-Mail-Service „implementieren“, aber er muss dazu überhaupt kein Verhalten nachbilden.
· Stub-Objekte. Sie implementieren ein bestimmtes Protokoll, sodass sie für den Testfall immer die gleichen Antworten geben können. Wenn etwa der E-Mail Service eine Methode isTransmitted() anbietet, so kann der Stub immer true liefern. Oder ein Stub-Repository liefert statt Kunden aus der Datenbank immer die gleichen 10 vorgefertigten Kunden. Oder statt ein langsamer Web-Service-Aufruf die aktuellen Wetterdaten liefert, gibt der Stub vorgefertigte ab. Stubs sind auch praktisch, wenn zum Beispiel eine Gui-Anwendung programmiert wird, die statt echter Datenbankdaten erst einmal mit den Stubs entwickelt wird und so die Demo-Daten anzeigt. Wenn ein Team die Gui baut, ein anderes Team den Service, so können beide Gruppen unabhängig arbeiten und das Gui-Team muss nicht erst auf die Implementierung warten.
· Mock-Objekte. Sie sind noch funktionsreichhaltiger als Stubs und bilden auch komplexe Interaktionen ab. In der Regel werden Mock-Objekte durch eine Bibliothek wie mockito (http://code.google.com/p/mockito/) oder EasyMock (http://easymock.org/) „aufgeladen“ und zeigen dann das gewünschte Verhalten.
Diese drei Typen können wir unter dem Oberbegriff Dummy-Objekt zusammenfassen. Grundsätzlich gilt bei den vier Begriffen aber, dass sie nicht einheitlich von Autoren verwendet werden.[4]
1.6 JUnit-Erweiterungen, Testzusätze
Das Framework JUnit selbst ist recht kompakt, doch wie es an den Hamcrest-Matchern abzulesen ist, gibt es die Notwendigkeit für komfortable Testmethoden, die häufig wiederkehrende typische Testaufgaben vereinfachen. Dazu zählen nicht nur Methoden, die testen, ob eine Datei vorhanden ist, sondern auch Unterstützungen für Tests mit Datenbankzugriffen, Web-Tests oder Gui-Tests.
Web-Tests
Beim Testen von Web-Anwendungen kommen zwei Verfahren zum Einsatz. Das eine ist ein werkzeugunterstützte Aufzeichnung von Web-Interaktionen und das automatische Abspielen der Folgen für den Test, die andere die programmierte Lösung. Für die Aufzeichnung bietet sich gut das freie Selenium (http://seleniumhq.org/) bzw. die Integration in Firefox mit der Selenium IDE (http://seleniumhq.org/projects/ide/) an. Wer Tests programmieren möchte, findet mit dem Apache HttpUnit (http://httpunit.sourceforge.net/) und dem LiFT-Framwork (https://lift.dev.java.net/) eine gute Basis.
Tests der Datenbankschnittstelle
Der Zugriff zur Datenbank geschieht in der Regel über Repository-Klassen (auch DAO-Klassen genannt). Greift ein Service auf eine Datenbank zu, so geht sie immer über das Repository. Der Test des Services wird dadurch vereinfacht, dass statt einer Datenbank-Reposity-Implementierung ein Repository-Dummy untergeschoben wird. Bleibt die Frage, wie die Reposity-Klassen zu testen sind.
Tests können sehr lange dauern, denn die Interaktion mit der Datenbank ist häufig das langsamste einer ganzen Geschäftsandwendung. Eine Heransgehensweise ist, die Tests lokal im Speicher laufen zu lassen. Dazu werden Im-Memory-Datenbanken wie Derby, H2, HSQLDB verwendet. Die Datenbank ist also rein im Speicher uns so läuft ein Test sehr schnell. Der größte Nachteil dabei ist aber, dass es SQL-Dialekte gibt und eine In-Memory-Oracle Datenbank gibt es bisher nicht. Wenn die Reposity-Implementierung für Massenoperationen auf eine gespeicherte Oracle-Prozedur zurückgreift, so kann das das einfache H2 nicht testen.
Eine weitere Aufgabe ist das Füllen der Datenbank mit Testdaten. Die Open-Source-Software DbUnit (http://www.dbunit.org/) ist hier eine große Hilfe. Externe Daten sind in XML verfasst und könne leicht in die Datenbank importiert werden bevor dann der Test auf diesen Probedaten arbeitet. Die Probedaten werden dann, wenn möglich, in der In-Memory-Datenbank eingefügt oder in einer lokalen Entwicklungsdatenbank. Für fortgeschrittne Tests (und insbesondere zum Abschätzen der Laufzeit) müssen Tests aber auch mit einer Kopie der echten Geschäftsdaten durchgeführt werden.
Ausblick
Neben JUnit gibt es das Testframework TestNG, welches zum Zeitpunkt von JUnit 3.x die Richtung aufzeigte, mit Annotationen Tests zu deklarieren. Erst spät wechselten die JUnit-Entwickler zu Java 5 und aktualisierten zur Version JUnit 4. In der Zwischenzeit, von JUnit 3.x auf JUnit 4 gab es eine stärkere Zuwendung zu TestNG, die aber heutzutage nicht mehr abzulesen ist, da JUnit 4 in letzter Zeit doch wieder aufgeholt hat und interessante Möglichkeiten bieten. Nicht angesprochen wurden in diesem Kapitel zum Beispiel parametrisierte Tests und Datenpunkte (erwartete Werte kommen automatisch über eine Datenstruktur) oder Theorien, wie es das folgende Beispiel kurz zeigt:
@Theory squareRoot( double n ) { assumeTrue( n >= 0 ); assertTrue( sqrRoot(n) >= 0 ); assertEquals( n, sqrRoot(n) * sqrRoot(n), 0.01 ); }
Weiterhin bietet JUnit Rules (Methoden einer mit @Rule annotiere Objektvariablen werden vor, nach oder statt eines Tests aufgerufen) oder auch Scheduling-Strategien mit parallelen Tests als Computer-Modell.
[1] http://www.time4tea.net/wiki/display/MAIN/Testing+Files+with+Hamcrest
[2] Wobei allOf(exists(), sized(0)) noch etwas besser ist – der Autor ist informiert
[3] Mehr Informationen zum Dateiformat gibt http://de.wikipedia.org/wiki/VCard.
[4] Die Seite http://xunitpatterns.com/Mocks,%20Fakes,%20Stubs%20and%20Dummies.html stellt einige Autoren mit ihrer Begriffsnutzung vor.
hallo,
assertEquals( „“, StringUtils.reverse( „“ ) );
bei dem Test sagen Sie, dass der Test laufen wird, die Methode liefert null und null ist dem „“ nicht gleich, somit sollte der Test durchfallen oder ich habe etwas übersehen…
äh wie? das ist ja TDD, also ist der test zuerst und eine Implementierung von reverse() gibt es noch nicht, also liefert die null und der Test sagt das auch, das die Methode noch falsch ist.
Vielen Dank 🙂
Ich finde den Artikel ganz gut, aber die Darstellung des Quellcodes könnte verbessert werden. Auf meinen Bildschirm passen maximal 11 Zeilen, dann muss ich scrollen. Geht das nicht ein bisschen kompakter?
Hallo,
ich verstehe den Teil mit den Suiten nicht ganz.
Mir ist klar wie das Durchreichen der Hierarchien gemeint ist.
Aber wie ist das denn, wenn ich keine Suiten habe, aber überall meine Testklassen und Methoden.
Die werden doch, wenn ich im Projekt unter Eclipse Junit Test starte, auch alle sequenziell abgearbeitet.
Kannst Du dazu etwas mehr erzählen? Der Vorteil ist mir noch nicht ganz ersichtlich.
Eine Suite bindet einfach ganz einfach mehrere Tests (die ja nur eine einzelne Klasse sind) zusammen (Composite Pattern). Die IDE führt vielleicht mehreren Tests aus, doch mal will das ja auch automatisiert über ein Build-Tool, also ohne IDE, ausführen.
Ich ärgere mich gerade etwas die Krätze! Habe auf den Hinweis am Anfang des Artikels, sowie den Werbetext im Internet hin das Buch gekauft und stelle fest, das Softwaretests geschweige denn JUnit dort nicht mit auch nur einem Wort (nicht mal im Stichwortverzeichnis) erwähnt wird. Sorry – das ist Mist!
Band 1:
20Testen mit JUnit1181
20.1Softwaretests1181
20.2Das Test-Framework JUnit1182
20.3Wie gutes Design das Testen ermöglicht1194
20.4Aufbau größerer Testfälle1197
20.5Dummy, Fake, Stub und Mock1200
20.6JUnit-Erweiterungen, Testzusätze1201
20.7Zum Weiterlesen1202
Bin echt kein Querulant und das mit einem Verweis auf das falsche Buch kann ja passieren. Diesen aber einfach so kommentarlos zu korrigieren und zu tun, als wäre man nur zu blöd gewesen, finde ich nicht ok. Ein kleines Sorry wäre hier gut angekommen. P.S.: war bis jetzt Inselfan, nu irgendwie erstmal nicht mehr so…
Besten Dank!