JUnit stellt dem Programmierer weitere interessante Möglichkeiten zur Verfügung. So können Sie in Ihrer Testklasse mit Hilfe bestimmter Methoden Initialisierungen vornehmen, Tests können gruppiert oder kurzzeitig abgeschaltet werden. Außerdem können Sie Timeouts definieren, die darüber bestimmen, wann ein Test fehlgeschlagen ist.
Ein weiteres wichtiges Thema, das Entwickler manchmal gerne übersehen, ist die Tatsache, dass auch Exceptions getestet werden müssen: Wird im richtigen Moment die richtige Ausnahme geworfen? Und wird sie auch korrekt abgefangen? Auch zum Testen von Exceptions stellt JUnit Mechanismen zur Verfügung.
Testen von Exceptions
Natürlich muss beim Testen auch sichergestellt werden, dass an den richtigen Stellen die richtigen Ausnahmen ausgelöst werden. Erinnern wir uns an die Kontoklasse aus dem zweiten Teil des Tutorials: Es gibt eine Methode abbuchen(), die sicherstellen muss, dass der Betrag auf dem Konto nicht unter 0 fällt.Zur Erinnerung sehen Sie im Folgenden noch einmal die Konto-Klasse, mit der gearbeitet werden soll. Es ist noch keine Logik implementiert, denn diese soll ja erst nach den Testfällen programmiert werden, wie Sie im ersten Teil des Tutorials gelernt haben.
public class Konto {
private String inhaber;
private String kontonr;
private float betrag;
public Konto(String inhaber,
String kontonr,
float betrag) {
}
public void einzahlen(float betrag) { }
public void auszahlen(float betrag) { }
}
In der Methode abbuchen() wird also eine Exception geworfen werden, wenn der Kontobetrag unter 0 fällt. Wir müssen prüfen, ob diese Ausnahme wirklich ausgelöst wird. Falls nicht, handelt es sich um einen Fehler, der unbedingt behoben werden muss. Aber wie testen wir, ob eine Ausnahme wie erwartet geworfen wurde? Mit den bereits vorgestellt equals-Methoden wird das schwierig.
Der nächste Code-Ausschnitt zeigt eine mögliche Test-Methode für die Methode abbuchen().
@Test public void testeAuszahlenNegativ() {
Konto konto = new Konto("Schmitt", "12345", 500);
try
{
konto.auszahlen(1000);
fail("Betrag unter 0 und es wurde keine Ausnahme ausgelöst!");
} catch (Exception e) {
// Tun, was getan werden muss.
}
}
Wird die Ausnahme wie erwartet ausgelöst, geschieht nichts, da sie abgefangen wird. Wird sie nicht ausgelöst, wird die fail-Methode ausgeführt. Diese löst eine JUnit-Ausnahme mit der entsprechenden Fehlermeldung aus. So wissen wir, dass unser Test nicht funktioniert hat.
Aber auch hier gibt es ein Problem. Was ist, wenn zwar eine Exception ausgelöst wurde, aber die falsche? Wir haben keine Chance herauszufinden, welche Exception fehlerhaft ausgelöst wurde. Wir könnten noch einen try-catch-Block verwenden, aber JUnit bietet eine wesentlich elegantere Lösung: die Annotation @Test wird parametisiert. Was das bedeutet, sehen Sie im folgenden Code-Ausschnitt.
@Test(expected = RuntimeException.class)
public void testeAuszahlenNegativ() {
Konto konto = new Konto("Schmitt", "12345", 500);
konto.auszahlen(1000);
}
JUnit erwartet eine RuntimeException. Wird diese Ausnahme ausgelöst, geschieht nichts. Bei einer anderen Ausnahme wird ein AssertionError mit einer Fehlermeldung erzeugt. Der Vorteil liegt in der Kürze des Codes. Wenn Sie in einer Methode jedoch mehrere Exceptions prüfen wollen, kommen Sie mit dieser kürzeren Lösung nicht weit. Hier gilt wie so oft: Es gibt kein Patentrezept, welche Lösung die bessere ist. Es hängt vom Anwendungsfall ab.
Setup- und Teardown-Methoden
Erweitern wir unser Beispiel ein wenig. Neben der Klasse Konto gibt es noch die Klassen Kunde, Mitarbeiter und Bank. Um die Klasse Bank testen zu können, brauchen wir Objekte von Konto, Mitarbeiter und Kunde. Und zwar jeweils mindestens drei. Für jeden einzelnen Test. Wir müssen also jedes Mal die entsprechenden Objekte erzeugen. Dass das eine enorme Menge an unnötigem Code ist, muss ich Ihnen nicht sagen. Und vielleicht müssen am Ende noch irgendwelche Aufräumarbeiten durchgeführt werden: Datenbank oder Dateien schließen, Ressourcen freigeben usw.
Um dieses Problem zu lösen, bietet JUnit zwei Annotationen: @Before und @After. Beide stehen vor Methoden, die vor oder nach jedem Testfall ausgeführt werden sollen. Wir können sie nutzen, um bspw. benötigte Objekte zu erzeugen. Wenn Sie sich unsere Konto-Testmethoden aus dem zweiten Teil des Tutorials ansehen, erkennen Sie, dass wir am Anfang jeder Methode ein Objekt erzeugt haben. Mit diesen beiden Annotationen können wir uns diese Code-Dopplung sparen. Im Folgenden sehen Sie das korrigierte Beispiel.
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
public class KontoTest {
private Konto konto;
@Before
public void erzeugeObjekt() {
konto = new Konto("Schmitt", "12345", 500);
}
@Test
public void testeEinzahlen() {
konto.einzahlen(500);
assertEquals(konto.getBetrag(), 1000.0, 0.001);
}
@Test public void testeAuszahlen() {
konto.auszahlen(300);
assertEquals(konto.getBetrag(), 200.0, 0.001);
}
}
Sie sehen, dass die Methode erzeugeObjekt() vor dem Aufruf jeder Testmethode aufgerufen wird. Wir erhalten also immer ein neues Konto-Objekt, das immer die gewünschten Daten hat.
Es gibt aber noch ein weiteres Problem. Manchmal gibt es Test-Setups, die zu aufwändig sind, um sie vor jedem Test neu aufzubauen und nach jedem Test wieder "abzureißen". Dieses Problem wird mit den Annotationen: @BeforeClass und @AfterClass gelöst. Beide stehen vor Methoden, die für eine Testklasse nur ein einziges Mal durchgeführt werden.
Die @BeforeClass-Methode wird also noch vor der ersten @Before-Methode durchgeführt. Beachten Sie, dass die @BeforeClass- und die @AfterClass-Methoden statisch sein müssen. Der folgende Code-Ausschnitt skizziert den Einsatz der Annotationen.
public class KontoTest {@BeforeClass
public static void sehrAufwaendigesSetup() { }
@AfterClass
public static void gibRessourcenFrei() { }
}
Timeouts
Interessant ist auch die Möglichkeit, Timeouts für Tests zu definieren. Sie können für einen Testfall angeben, wie viele Millisekunden er laufen soll. Bei Überschreitung des Zeitlimits wird der Test abgebrochen. Den Parameter timeout übergeben Sie der Annotation @Test.public class KontoTest {
@Test(timeout = 1000)
public void laufeMax1000Millisekunden() {
}
}
Tests kurzzeitig ausschalten
JUnit bietet die Möglichkeit, einen Test für kurze Zeit auszuschalten. Dies kann unter bestimmten Umständen interessant sein. Verwenden Sie diese Technik aber nie nach Gutdünken, denn man hat schnell einen ignorierten Test übersehen und einen Fehler nicht korrigiert. Seien Sie mit dem Einsatz der Annotation @Ignore sehr vorsichtig. Die Annotation hat einen Parameter, der einen Kommentar enthält, warum der Test ignoriert wird.
Testfälle gruppieren
Im Allgemeinen wird man die Tests nicht einzeln starten. Interessanter ist es, alle Testfälle zusammenzufassen und sie auf einmal anzustoßen. Wenn Sie 100 Testmethoden einzeln starten müssten, würden Sie letztendlich vermutlich nicht mehr testen als ohne JUnit. Der gesamte Testablauf soll so weit automatisiert werden, dass die Tests ohne manuelles Eingreifen ablaufen können. Dazu liefert JUnit uns die Testsuite mit.
Im Allgemeinen definiert man sich eine Testsuite-Klasse mit einer statischen Methode. Normalerweise werden spezielle Klassen definiert, die nur solche Testsuiten repräsentieren. Testsuiten können selbst weitere Testsuiten enthalten, so dass Sie Hierarchien aufbauen können.
Stellen wir uns wieder das erweiterte Bank-Beispiel vor mit den Klassen Bank, Kunde, Mitarbeiter und Konto. Sie bilden ein TestSuite-Objekt und fügen diesem die verschiedenen Testklassen hinzu. Gehen wir davon aus, dass wir für jede genannte Klasse eine Testklasse haben. Dann bekämen wir folgende Testuite-Klasse (Testsuite-Klassen werden nach Konvention AllTests genannt).
import junit.framework.Test;
import junit.framework.TestSuite;
public class AllTests {
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(KontoTest);
suite.addTestSuite(MitarbeiterTest);
suite.addTestSuite(KundeTest);
suite.addTestSuite(BankTest);
return suite;
}
}
Somit ist es ein Leichtes, alle Tests durch den einfachen Aufruf der statischen Methode zu starten.
... comment