6.7.2005

JUnit 4.0

Eine kleine Sneak-Preview auf das neue JUnit, das im Sommer/Herbst erscheinen wird ... (im Vergleich zu JUnit 3.8.1)

Mit JUnit 4 haben Kent Beck und Erich Gamma nach mittlerweile sieben Jahren zum ersten Mal die Architektur ihres Test-Frameworks grundlegend geändert und alle Story-Karten auf die mit Java 1.5 eingeführten Annotationen gesetzt. Annotationen sind ein neues Ausdrucksmittel der im Trend liegenden Metaprogrammierung. Mithilfe von Annotationen können Sie Ihren Code mit frei wählbaren Anmerkungen versehen und auf die so markierten Codeelemente über den Reflection-Mechanismus später wieder zugreifen. In JUnit 4 wird dieses Sprachkonstrukt nun dazu verwendet, jede x-beliebige Methode jeder x-beliebigen Klasse als ausführbaren Testfall kennzeichnen zu können. Hier ist ein Test der neuen Schule:


import junit.framework.TestCase;

import org.junit.Test;
import static org.junit.Assert.*;

public class EuroTest extends TestCase {
@Test public void testadding() {
Euro two = new Euro(2.00);
Euro sum = two.add(two);
assertEquals("sum", new Euro(4.00), sum);
assertEquals("two", new Euro(2.00), two);
}
}

Sie erkennen, dass die Namenskonvention public void test...() wie auch das Ableiten der Klasse TestCase der Vergangenheit angehören. Sie kleben künftig einfach eine @Test Annotation an Ihre Testfälle und können Ihre Methoden nennen, wie Ihnen gerade gefällt. Doch gehen wir die Neuigkeiten doch einmal mit dem Vergrößerungsglas durch ...

Mit JUnit 4 kommt ein neuer Namensraum: Im Package org.junit steckt der neue annotationsbasierte Code. Das junit.framework Paket bleibt soweit bestehen und hat lediglich kleine Änderungen erfahren, um die Aufwärtskompatibilität herzustellen. Für Tests, die der neuen Schule folgen, benötigen wir von dem alten Zeugs jedoch nichts mehr:


import junit.framework.TestCase;

Stattdessen importieren wir jetzt die @Test Annotation und Methoden der Assert Klasse aus dem neuen JUnit-Package:


import org.junit.Test;
import static org.junit.Assert.*;

Falls Sie mit den neuen Sprachkonstrukten noch nicht vertraut sind: Ab Java 1.5 können Sie mithilfe von statischen Imports die statischen Methoden einer anderen Klasse einfach in den Namensraum Ihrer eigenen Klasse einblenden. Mit der Zeile:


import static org.junit.Assert.assertEquals;

... könnten wir beispielsweise die assertEquals Methode importieren, so als wäre diese eigentlich bei uns definiert. Der oben verwendete Joker holt einfach alle statischen Methoden auf einen Schwung.

Als Nächstes ist augenfällig, dass unsere Klasse nicht mehr von der Klasse TestCase abgeleitet ist. Ab sofort können Sie Ihre Tests nämlich in jede beliebige Klasse stecken. Die einzige Bedingung ist: Ihre Klasse muss über einen öffentlichen Default-Konstruktor instanzierbar sein:


public class EuroTest extends TestCase {

Welche Methoden als Testfälle auszuführen sind, markieren wir jetzt mithilfe der @Test Annotation. Den Klammeraffen nicht vergessen! Welche Namen Sie Ihren Methoden geben, ist egal. Den test... Präfix können Sie als Zeichen alter Tradition oder aus guter Konvention beibehalten oder es auch lassen. Einzige Bedingung: Ihre Methode muss öffentlich sein, darf keine Parameter und keinen Rückgabewert haben:


@Test public void testadding() {
Euro two = new Euro(2.00);
Euro sum = two.add(two);
assertEquals("sum", new Euro(4.00), sum);
assertEquals("two", new Euro(2.00), two);
}
}

Das ist, auf einer Seite zusammengefasst, was sich grob geändert hat.

JUnit 4 führt sechs unterschiedliche Annotationen ein:

  • @Test kennzeichnet Methoden als ausführbare Testfälle.
  • @Before und @After markieren Setup- bzw. Teardown-Aufgaben, die für jeden Testfall wiederholt werden sollen.
  • @BeforeClass und @AfterClass markieren Setup- bzw. Teardown-Aufgaben, die nur einmal pro Testklasse ausgeführt werden sollen.
  • @Ignore kennzeichnet temporär nicht auszuführende Testfälle.

@Before und @After

Setup- und Teardown-Methoden werden wie Testfälle via Annotation gekennzeichnet:

  • @Before Methoden werden vor jedem Testfall ausgeführt,
  • @After Methoden nach jedem Testfall.

Auch diese Methoden können beliebige Namen tragen, müssen nun aber öffentlich zugänglich sein, parameterlos und ohne Rückgabewert. Der Fixture-Aufbau erfolgt auf dem gewohnten Weg:


import org.junit.Before;

public class EuroTest {
private Euro two;

@Before public void setUp() {
two = new Euro(2.00);
}

@Test public void amount() {
assertEquals(2.00, two.getAmount(), 0.001);
}

@Test public void adding() {
Euro sum = two.add(two);
assertEquals("sum", new Euro(4.00), sum);
assertEquals("two", new Euro(2.00), two);
}
}

Neu ist, dass auch mehrere @Before und @After Methoden pro Klasse laufen können. Eine bestimmte Ausführungsreihenfolge wird dabei jedoch nicht zugesagt. Vererbte und nicht überschriebene Methoden werden in symmetrischer Weise geschachtelt gerufen:

  • @Before Methoden der Oberklasse vor denen der Unterklasse
  • @After Methoden der Unterklasse vor denen der Oberklasse

@BeforeClass und @AfterClass

Für kostspieligere Test-Setups, die nicht für jeden einzelnen Testfall neu aufgebaut und danach gleich wieder abgerissen werden können, existieren zwei weitere Annotationen:

  • @BeforeClass läuft für jede Testklasse nur ein einziges Mal und noch vor allen @Before Methoden,
  • @AfterClass entsprechend für jede Testklasse nur einmal und zwar nach allen @After Methoden.

Mehrere @BeforeClass und @AfterClass Annotationen pro Klasse sind zugelassen. Die so markierten Methoden müssen jedoch statisch sein:


import org.junit.BeforeClass;
import org.junit.AfterClass;

public class EuroTest...
@BeforeClass public static void veryExpensiveSetup() { ... }
@AfterClass public static void releaseAllResources() { ... }
}

Die @BeforeClass Methoden einer Oberklasse würden entsprechend noch vorher ausgeführt, alle ihre @AfterClass Methoden anschließend.

Erwartete Exceptions

Zum Testen von Exceptions können Sie der @Test Annotation über ihren optionalen Parameter expected mitteilen, dass die Ausführung Ihres Testfalls zu einer gezielten Exception führen soll:


public class EuroTest...
@Test(expected = IllegalArgumentException.class)
public void negativeAmount() {
final double NEGATIVE_AMOUNT = -2.00;
new Euro(NEGATIVE_AMOUNT); // should throw the exception
}
}

Wird keine Exception geworfen oder eine Exception anderen Typs, schlägt dieser Testfall eben genau fehl.

Wenn Sie das Exception-Objekt in dem Test noch weiter unter die Lupe nehmen wollen, um beispielsweise dessen Felder zu überprüfen, sollten Sie den altbekannten Weg über den try/catch Block nehmen. Ansonsten ist diese Annotation sehr elegant.

Timeouts

Ein für Performanztests interessanter optionaler Parameter ist timeout. Geben Sie Ihrem Testfall mit auf die Reise, in welcher Zeitspanne von Millisekunden er laufen sollte. Überschreitet er darauf sein Zeitlimit, wird er zwecks Fehlschlags abgebrochen:


public class EuroTest...
@Test(timeout = 100)
public void performanceTest() { ... }
}

@Ignore

Wenn Sie einen Test kurzzeitig außer Gefecht setzen wollen, können Sie das tun:


import org.junit.Ignore;

public class EuroTest...
@Ignore("not today")
@Test(timeout = 100)
public void performanceTest() { ... }
}

Der @Ignore Kommentar darf, sollte aber niemals fehlen! Der Test wird dann im Testlauf unter Protokollierung dieses Textes übergangen. Sorgen Sie jedoch dafür, dass ignorierte Tests schnellstens auf grün kommen und im besten Fall gar nicht erst eingecheckt werden können!

Neue Tests mit altem Runner ausführen

Damit die existierenden und zum Teil in die Entwicklungsumgebungen direkt integrierten TestRunner unsere neuen Tests ausführen können, müssen wir einen kleinen Kunstgriff unternehmen:


import junit.framework.JUnit4TestAdapter;

public class EuroTest...
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(EuroTest.class);
}
}

Etwas unschön, doch nur solange notwendig, bis unsere Werkzeuge die Brücke zur neuen annotationsbasierten Form geschlagen haben, muss die gute alte suite Methode uns als Adapterfunktion herhalten.

Nachtrag: Nach Artikel und Buch hinzugekommen sind die @RunWith und @Suite Annotationen:
  • Lasse Koskela beschreibt, wie man Testsuiten mit JUnit 4 aufbaut,
  • Johannes Link erklärt, wie man Testfälle mit seiner Eclipse-Erweiterung auch projektübergreifend ausführen kann.

Alte Tests mit neuem Runner ausführen

JUnit 4 liefert nur noch einen textuellen Runner mit. Die grafische Integration wird konsequent den Werkzeugherstellern überlassen. Ebenso wandert die Organisation von Testfällen zu den Entwicklungswerkzeugen über. Der neue Runner JUnitCore akzeptiert tatsächlich nur noch ein flaches Array von Testklassen:


import org.junit.runner.JUnitCore;

public class AllTests {
public static void main(String[] args) {
JUnitCore.run(CustomerTest.class,
              EuroTest.class,
              MovieTest.class);
}
}

Testfallerzeugung und Testlauf

Erwähnenswert ist noch, dass JUnit 4 nicht mehr in zwei Phasen läuft: Es werden also nicht erst alle Testfallobjekte auf Halde erzeugt und dann ausgeführt. Die Erzeugung erfolgt just-in-time zum Testlauf.

Schlüsselwort assert

Wenn Sie mögen, können Sie in Ihren Tests ab sofort auch das Java 1.4 Schlüsselwort assert verwenden. Sie müssen lediglich daran denken, die Zusicherungen zur Laufzeit auch mit der Option -ea zu aktivieren. JUnit ist dazu intern zur Verwendung von AssertionError gewechselt, was auch dazu geführt hat, dass nicht mehr zwischen möglichen und unerwarteten Fehlschlägen, Failures und Errors, unterschieden wird. Fehler sind Fehler sind Fehler!

Subtile Unterschiede existieren zwischen altem und neuen Assert: junit.framework.Assert vs. org.junit.Assert. Unter Java 1.5 sollten Sie nur noch die neue Klasse einsetzen. Da Arrays untereinander nun auch über equals vergleichbar sind, werden Sie sogar mit einer neuen assert Methode beschenkt:


assertEquals(Object[] expected, Object[] actual)

Schlagwörter:

blättern: Tonabnehmer 7 - Markus Völter - Softwarearchitektur und Modellgetriebene Entwicklung
zurück: Podcasts in iTunes