Einführung in JUnit 4

Werbung
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Einführung in JUnit 4
– für LOMF-Programmierer –
Version: 1.2
Fragen, Anregungen und Korrekturen zu diesem Dokument bitte per E-Mail an:
[email protected]
Inhaltsverzeichnis
1 Einleitung..........................................................................................................................................2
2 Ein kleiner Crash-Kurs......................................................................................................................2
2.1 Aufgabe......................................................................................................................................3
2.2 Schnittstelle formulieren............................................................................................................3
2.3 Testrahmen (Testcase) entwickeln.............................................................................................4
2.4 Testfälle schreiben.....................................................................................................................5
2.5 Testfälle ausführen.....................................................................................................................7
3 Mehr über Testfälle............................................................................................................................8
3.1 Trennen von Initialisierung und Auswertung.............................................................................8
3.2 Testfälle und Ausnahmen.........................................................................................................10
3.2.1 Unerwünschte Ausnahmen...............................................................................................11
3.2.2 Erwünschte Ausnahmen...................................................................................................11
4 Zusammenfassung...........................................................................................................................12
Anhang A: Erstellung eines Java-Projekts mit JUnit-Unterstützung.................................................13
Anhang B: Versionshistorie................................................................................................................19
Literaturverzeichnis............................................................................................................................19
Abbildungsverzeichnis
Abbildung 1: Erstellen eines Testcases.................................................................................................4
Abbildung 2: JUnit-Fenster mit fehlgeschlagenen Tests......................................................................7
Abbildung 3: JUnit-Fenster mit erfolgreichen Tests............................................................................8
Abbildung 4: Anlegen eines Java-Projekts, Teil 1..............................................................................13
Abbildung 5: Anlegen eines Java-Projekts, Teil 2..............................................................................14
Abbildung 6: Integration der JUnit-Unterstützung: "Libraries" auswählen.......................................15
Abbildung 7: Integration der JUnit-Unterstützung: "Add Library" auswählen.................................16
Abbildung 8: Integration der JUnit-Unterstützung: JUnit-Bibliothek auswählen..............................16
Abbildung 9: Integration der JUnit-Unterstützung: JUnit-Version auswählen..................................17
Abbildung 10: Abschluss der JUnit-Integration, Teil 1......................................................................17
Abbildung 11: Abschluss der JUnit-Integration, Teil 2......................................................................18
Abbildung 12: Neues Java-Projekt mit JUnit-Unterstützung.............................................................18
Tabellenverzeichnis
Tabelle 1: Von JUnit angebotene Methoden zum Vergleichen von Soll- und Ist-Werten.....................5
1
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
1 Einleitung
Herzlich willkommen in der Welt von Java! Als erfahrener LOMF 1-Recke hast du bereits die Höhen
und Tiefen der funktionalen Programmierung gemeistert und die Grundlagen der Software-Entwicklung verinnerlicht. Begriffe wie Algorithmus, Funktion, Argument, Parameter oder Spezialisieren sind für dich keine Fremdwörter mehr. Und auch die allerersten Grundlagen des objektorientierten Paradigmas sind dir bereits vermittelt worden.
Umso verständlicher, dass du dich eben nun fragst, wie in Java Programme effektiv und effizient
getestet werden können. Wie? Das weißt du bereits? Dann kannst du getrost den Rest des Dokuments überspringen und dich wichtigeren Aufgaben widmen. Allerdings lernt man bekanntlich nie
aus, und vielleicht enthält dieses Dokument doch ein oder zwei praktische Tipps, die dir bei deiner
täglichen Arbeit in Java helfen können...
Als LOMF-Programmierer war dir das Testen im Rahmen der Software-Entwicklung sicherlich in
Fleisch und Blut übergangen. “Kein Code ohne Testfälle” war die fast schon allgegenwärtige Regel,
die dich vor unliebsamen Überraschungen bewahren sollte. Kein Wunder, möchte man meinen,
schließlich ist LOMF mit voller Absicht so entwickelt worden, das Testen steckt LOMF sozusagen
“im Blut”.
In Java sieht die Sache etwas anders aus. Hier ist das Testen nicht dermaßen in der Sprache verankert, wie es bei LOMF der Fall ist. Doch flexible Alternativen und mächtige Werkzeuge existieren
auch hier. Insbesondere eine weit verbreitete Bibliothek wird uns in den nächsten Abschnitten besonders beschäftigen. Die Rede ist von JUnit2, einer Sammlung von Klassen, die sich einfaches Testen von Software auf die Fahne geschrieben haben. Entworfen und implementiert wurde diese Bibliothek von Erich Gamma und Kent Beck, zwei Persönlichkeiten, die einem (angehenden) Programmierer heutzutage zumindest nicht unbekannt sein sollten.3
Wir starten also mit JUnit als Test-Framework4, und – weil das Arbeiten mit einem “gewöhnlichen”
Text-Editor meist nicht besonders produktiv ist5 – mit der Java-Entwicklungsumgebung eclipse6.
Diese Entwicklungsumgebung hat den großen Vorteil, dass sie JUnit bereits “kennt” und keine
künstlichen “Klimmzüge” gemacht werden müssen, um seine Software schnell und einfach testen
zu können. Wir erinnern uns: Wenn das Testen nicht schnell und einfach ist, wird es nicht getan.
Und das willst du am allerwenigsten, denn schließlich liest du ja ein Dokument übers Testen, oder?
Hinweis: Im Folgenden wird vorausgesetzt, dass du mit den grundlegenden Funktionen von eclipse
vertraut bist und weißt, wie man in eclipse ein Java-Projekt anlegt, Quelltext-Dateien bearbeitet und
Java-Programme zum Ausführen bringt. Dies ist keine Einführung in eclipse, sondern in JUnit!
Falls du dir nicht sicher bist, schau bitte im Anhang A nach, wie du ein Java-Projekt mit JUnitUnterstützung anlegen kannst.
2 Ein kleiner Crash-Kurs
Dieses Kapitel stellt die grundsätzliche Funktionalität von JUnit dar, ohne dass du dich gleich in unwesentlichen Details verlierst. Es wird ein kleines LOMF-Programm vorgestellt (natürlich mitsamt
1
2
3
4
5
6
Less Overhead More Fun, entwickelt von Prof. Dr. Michael Löwe
s. http://junit.org/
Was, du kennst sie noch nicht? Dann mach deine Hausaufgaben und lies [GHJV95] und [Beck03], zwei Klassiker
der Computer-Literatur!
Ein Framework ist eine besondere Art Bibliothek, die so konzipiert ist, dass nicht dein Programm die Bibliothek
ruft, sondern andersherum: Das Framework ruft dein Programm, wenn es notwendig wird. Der Kontrollfluss ist also
umgedreht. Dies ist auch bekannt als sog. Hollywood-Prinzip (“Don't call us, we call you”).
zumindest nicht für Anfänger!
s. http://www.eclipse.org/
2
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Testfällen!), das in ein funktional äquivalentes Java-Programm umgewandelt werden soll. Dabei
spielen in unserer Betrachtung die Testfälle eine besonders wichtige Rolle. Du wirst in diesem Abschnitt lernen, wie man Testfälle in Java/JUnit7 formuliert und durchführt. Du wirst auch sehen, wie
die Testfälle eines LOMF-Programms sich direkt in die Welt von Java/JUnit übertragen lassen. Und
los geht's...
2.1 Aufgabe
Gegeben ist das folgende kleine LOMF-Programm zur rekursiven Berechnung der Fakultät:
fac (n : Card) : Card
[*
Effects :
Computes n! == fac (n).
Notes :
n! is recursively defined as:
0! == 1
n! == n * ((n - 1)!) [n > 0]
*]
::=
n.= (0).? (
1,
/* 0! == 1 (bottom case) */
n.* (n.- (1).fac ()) /* recursive invocation */
)
Zu dem Programm existieren folgende LOMF-Testfälle:
fac
fac
fac
fac
fac
fac
(0) == 1
(1) == 1
(2) == 2
(3) == 6
(4) == 24
(10) == 3628800
Die Aufgabe ist nun, die Funktion fac() in die Programmiersprache Java zu übertragen. Da du ja
dir vermutlich vorstellen kannst, dass bei einer solchen Konvertierung einiges schief gehen kann,
willst du natürlich erst die Testfälle in Java schreiben. Wie das geht, beschreibt der nächste Abschnitt.
Zuerst aber legst du in eclipse ein passendes und vorerst leeres Java-Projekt an (siehe Anhang A:
Erstellung eines Java-Projekts mit JUnit-Unterstützung). Der Name “factorial” bietet sich eventuell
an.
2.2 Schnittstelle formulieren
Bevor du die Testfälle übernehmen (genauer: konvertieren) kannst, musst du dir über die Schnittstelle8 der zu testenden Einheit Gedanken machen. Dies unterscheidet sich nicht von der Entwicklung in LOMF, wo es hieß: Zuerst Signatur (ohne Implementierung!), dann Testfälle, dann Implementierung. Du musst nun also die Schnittstelle festlegen, über welche die zu übertragende Funktion (sprich: fac()) angesprochen werden kann. Für das weitere Vorgehen gehen wir von folgender
Klassendefinition aus (die du über “File → New → Class” generieren kannst):
package factorial;
7
8
eigentlich “in Java in Verbindung mit JUnit”, aber das jedesmal hinzuschreiben ist einfach zu mühselig...
Achtung: Hier ist “Schnittstelle” in der allgemeinen Bedeutung als “Spezifikation” gemeint (Eingabe, Ausgabe, Verhalten, Vor- und Nachbedingungen) und nicht in der speziellen Bedeutung als “Schnittstellen-Klasse”!
3
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
public class Factorial {
public int compute (int number) {
return 0;
}
}
Diese Definition ist offensichtlich noch nicht vollständig. Sie ist aber syntaktisch einwandfrei, und
genau darum geht es im ersten Schritt. Die Funktionalität wirst du erst implementieren, nachdem du
die Testfälle geschrieben haben wirst.
2.3 Testrahmen (Testcase) entwickeln
Nachdem du das Problem der Schnittstelle gelöst hast, machst du dich nun endlich daran, die Testfälle zu implementieren und für JUnit geeignet zu “verpacken”. Diese “Verpackung” wird Testrahmen bzw. Testcase genannt. Dazu legst du zuerst eine neue Klasse mit dem Namen FactorialTest
an, welche die Testfälle beinhalten wird. Diese Klasse erbt von TestCase, einer Klasse des JUnitFrameworks. Warum dies so sein muss, wird später deutlich. Halten wir vorerst fest, dass durch die
enge Verwandtschaft das JUnit-Framework später die Tests lokalisieren und ausführen kann. Die
Klasse dient dir nun als Testrahmen für die noch zu schreibenden Testfälle.
In eclipse kannst du nun nun zum ersten Mal von der Integration der JUnit-Bibliothek Gebrauch
machen: Dazu rufst du nun über “File → New → JUnit Test Case” einen Dialog auf (1), in dem du
die Standard-Vorgaben einfach übernimmst und auf “Fertig stellen” drückst. Nun generiert das integrierte JUnit-Plugin die entsprechende Klasse automatisch samt notwendiger Oberklasse und
import-Klausel.
Du solltest jetzt folgenden Quelltext vor dir sehen:
4
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
package factorial;
import static org.junit.Assert.*;
import org.junit.Test;
public class FactorialTest {
}
@Test
public void test() {
fail("Not yet implemented");
}
Die import-Direktiven sind wichtig, um die benötigten JUnit-Funktionen ohne zusätzliche Namensraum-Qualifikation nutzen zu können. Die Methode test wird als Beispiel automatisch generiert, wir brauchen sie jedoch nicht, weil wir unsere eigenen Testfälle entwickeln wollen, was im
nächsten Abschnitt beschrieben wird. Somit lösche bitte die Methode test, bevor du zum nächsten
Abschnitt übergehst.
2.4 Testfälle schreiben
Du hast nun einen funktionierenden Testrahmen (na ja, noch funktioniert anscheinend gar nichts,
aber das wird sich bald ändern!) Nun schreibst du die Testfälle, indem du jedem LOMF-Testfall
eine eigene Test-Methode spendierst. Dabei musst du folgende Regeln einhalten:
1) Die Methode muss die Annotation @Test besitzen.
2) Der Rumpf enthält einen Aufruf einer der verfügbaren assert-Operationen oder den Aufruf der
fail-Operation.
Punkt 1 ist notwendig, damit das JUnit-Framework deine Tests finden und ausführen kann. Punkt 2
ist wichtig, damit das JUnit-Framework überhaupt mitbekommt, wenn irgendwelche getesteten Bedingungen nicht in Ordnung sind. Die wichtigsten assert-Methoden sind:
assert-Methode
Funktion
assertTrue (condition)
testet, ob condition wahr ist
assertFalse (condition)
testet, ob condition falsch ist
assertEquals (expected, actual)
testet, ob entweder expected und actual beide
null sind oder expected.equals(actual)
wahr ist
assertNull (object)
testet, ob object == null wahr ist
assertNotNull (object)
testet, ob object != null wahr ist
assertSame (expected, actual)
testet, ob expected == actual wahr ist
Tabelle 1: Von JUnit angebotene Methoden zum Vergleichen von Soll- und Ist-Werten
In unserem Beispiel prüfen wir ausschließlich auf Gleichheit zweier Zahlen, also ist assertEquals
die beste Wahl. Warum, wirst du dich fragen? Schließlich haben primitive Datentypen wie int keine Operation equals! Das stimmt, aber assertEquals ist für alle möglichen primitiven Datentypen, einschließlich int, überladen. Dabei werden die primitiven Datentypen in die entsprechenden
Wrapper-Objekte gepackt (z.B. bei int in Integer-Objekte). Diese werden anschließend per
equals verglichen. Deshalb ist es immer angebracht, assertEquals zu benutzen, wenn es um den
5
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Vergleich auf Äquivalenz geht. assertSame solltest du nur verwenden, wenn du wirklich ObjektIdentität prüfen möchtest, und das ist selten notwendig.
Auch solltest du die assert-Methoden, die von JUnit angeboten werden, nicht mit dem assertSchlüsselwort verwechseln, dass ab der Version 1.4 des JDKs 9 existiert. Die JUnit-Methoden sind
völlig normale (statische) Methoden, die in der Klasse org.junit.Assert definiert sind. Dazu aber
später mehr.
Genug geredet, nun wird kodiert (der unveränderte Quelltext wird grau dargestellt):
package factorial;
import static org.junit.Assert.*;
import org.junit.Test;
public class FactorialTest {
@Test
public void fac0 () {
assertEquals (1, new Factorial ().compute (0));
}
@Test
public void fac1 () {
assertEquals (1, new Factorial ().compute (1));
}
@Test
public void fac2 () {
assertEquals (2, new Factorial ().compute (2));
}
@Test
public void fac3 () {
assertEquals (6, new Factorial ().compute (3));
}
@Test
public void fac4 () {
assertEquals (24, new Factorial ().compute (4));
}
@Test
public void fac10 () {
assertEquals (3628800, new Factorial ().compute (10));
}
}
Wie du siehst, existiert nun für jede Zeile im LOMF-Test eine korrespondierende Test-Methode.
Vielleicht wundert dich, dass die assertEquals-Argumente gegenüber den LOMF-Vergleichen in
der Reihenfolge vertauscht sind. Dies ist Absicht: Die Signatur von assertEquals erwartet zuerst
den Soll-Wert und anschließend den Ist-Wert. Warum ist das so wichtig, wirst du bestimmt denken?
Schließlich ist Gleichheit eine Äquivalenzrelation und somit symmetrisch! Das ist richtig, aber es
geht hier um die Aussagekraft der Meldungen, wenn ein Testfall fehlschlägt. Wenn jetzt Testfall
fac0 ausgeführt wird, schlägt der Vergleich fehl, und JUnit generiert die Fehlermeldung:
java.lang.AssertionError: expected:<1> but was:<0>
Im umgekehrten Fall wird die Fehlermeldung
java.lang.AssertionError: expected:<0> but was:<1>
9
Kurzform für Java Development Kit
6
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
generiert. Und die ist zweifelsohne unsinnig, oder?
2.5 Testfälle ausführen
Nach soviel getaner Arbeit willst du jetzt sicherlich die Früchte deines Tuns ernten. Dazu wählst du
die Klasse im Editor aus, welche die auszuführenden Testfälle enthält (in unserem Beispiel also die
Klasse FactorialTest), und wählst den Menüpunkt “Run → Run As → JUnit Test”. Die eclipseUmgebung lädt und startet daraufhin eine JUnit-Klasse, welche die Testfälle aus der markierten
Klasse extrahiert und nacheinander ausführt.
Nach der Ausführung erhältst du unter dem Editor neben “Konsole” und “Tasks” eine Lasche “JUnit”10, die Details zu den ausgeführten Testfällen anzeigt (vgl. 2). Das wichtigste hierbei ist der dicke Balken im oberen Teil des Fensters. Wenn er grün ist, waren alle Tests erfolgreich. Wenn er hingegen rot ist, hat mindestens ein Testfall aus irgendeinem Grund nicht funktioniert!
Abbildung 2: JUnit-Fenster mit fehlgeschlagenen Tests
Es gibt – aus der Sicht von JUnit – zwei Arten von Fehlern, die bei Tests gefunden werden können.
Störungen (bzw. Failures) liegen vor, wenn der Test auf Grund eines Aufrufs einer JUnit-Methode
fehlschlägt, also z. B. durch den Aufruf einer assert-Methode, wenn die zu testende Bedingung
nicht eintrifft. Fehler (bzw. Errors) liegen vor, wenn die Test-Methode durch eine nicht vorhergesehene Ausnahme beendet wird. Diese beiden Fälle werden gesondert behandelt, und das aus gutem
Grund. Denn unbehandelte Ausnahmen werden als “außergewöhnlich” betrachtet, etwas, was nicht
vorkommen sollte. Fehlgeschlagene Tests auf Grund von fehlgeschlagenen Vergleichen hingegen
sind “vorherzusehen” und während der Entwicklung meistens keine Ausnahme.
Wie du siehst, sind alle Testfälle fehlgeschlagen, der Balken ist rot. Das ist natürlich kein Wunder.
Jetzt geht es darum, die Funktion so zu “korrigieren”, dass die Testfälle erfolgreich sind. Nach
[Beck03] gibt es dazu mehrere Möglichkeiten11:
1) “Obvious Implementation”: Die offensichtliche Implementierung wird kodiert.
2) “Fake It”: Die Implementierung wird geringfügig modifiziert, dass die Testfälle erfolgreich sind;
danach wird refaktorisiert, indem eingeführte Konstanten schrittweise in Variablen umgewandelt
werden.
3) “Triangulation”: Aus zwei oder mehr Testfällen wird abstrahiert.
In unserem Fall ist die Implementierung der Fakultät relativ einfach, da wir auch eine funktionsfähige Grundlage besitzen. Wir wählen also Methode 1 und kodieren:
10
11
Da dies ein verschiebbares Fenster ist, kann es sich gelegentlich auch woanders befinden, etwa als Lasche neben
dem Paket-Explorer!
Dies ist nur eine sehr, sehr kurze Einführung in die Test-getriebene Entwicklung. Für weitere Details vgl. [Beck03].
7
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
package factorial;
public class Factorial {
public int compute (int number) {
if (number == 0)
return 1;
else
return number * compute (number - 1);
}
}
Nun führst du die Tests noch einmal aus, und voilà: der Balken ist grün, die Tests waren erfolgreich
(vgl. 3). Unsere Aufgabe ist also geschafft. Oder nicht?
Abbildung 3: JUnit-Fenster mit erfolgreichen Tests
3 Mehr über Testfälle
3.1 Trennen von Initialisierung und Auswertung
Wenn du dir die Testfälle noch einmal genauer anschaust, wirst du feststellen, dass in jedem einzelnen Testfall ein Factorial-Objekt erzeugt wird, um die Fakultät zu berechnen. Weil der auszuführende Programmcode zur Erzeugung des Objekts in jeder Testmethode derselbe ist, bietet es sich an,
die Erzeugung “herauszuziehen” und in eine eigene Methode zu verlagern. Wir nennen diese Methode setUp und rufen sie dann in jeder Testmethode auf:
package factorial;
import static org.junit.Assert.*;
import org.junit.Test;
public class FactorialTest {
private Factorial m_factorial;
public void setUp () {
m_factorial = new Factorial ();
}
@Test
public void fac0 () {
setUp ();
assertEquals (1, m_factorial.compute (0));
}
@Test
public void fac1 () {
setUp ();
8
Einführung in JUnit 4, Version 1.2
}
Drachau/Schulz
assertEquals (1, m_factorial.compute (1));
}
@Test
public void fac2 () {
setUp ();
assertEquals (2, m_factorial.compute (2));
}
@Test
public void fac3 () {
setUp ();
assertEquals (6, m_factorial.compute (3));
}
@Test
public void fac4 () {
setUp ();
assertEquals (24, m_factorial.compute (4));
}
@Test
public void fac10 () {
setUp ();
assertEquals (3628800, m_factorial.compute (10));
}
Bevor wir irgendetwas weiter an dem Quelltext verändern, müssen wir die Tests laufen lassen.
Warum? Bei jeder Änderung können sich Fehler einschleichen! Deshalb haben wir uns die Testfälle
gebastelt: damit wir Fehler finden! Auch wenn Testfälle geändert werden, wollen wir sichergehen,
dass keine unerwünschten Nebeneffekte (an die wir nicht gedacht haben) auftreten. Deshalb führen
wir die Tests aus und fühlen uns gut, weil wir den grünen Balken sehen.
Jetzt wirst du sicherlich einwenden, dass in diesem speziellen Fall durch die Refaktorisierung nicht
viel gewonnen ist: Der Quelltext ist wesentlich länger geworden. Dieser Einwand ist berechtigt,
wird aber durch den offensichtlichen strukturellen Gewinn kompensiert: Nun ist die Testfall-Initialisierung strikt von der Testfall-Auswertung getrennt.
Dies ist ein großer Vorteil, weil JUnit diese Trennung von initialisierenden, auswertenden (und abschließenden, s. u.) Operationen “von Haus aus” unterstützt. Wenn wir nämlich der oben eingefügten Methode setUp einfach die Annotation @Before spendieren, können wir uns die ganzen Aufrufe
dieser Methode sparen, weil JUnit vor (= „before“!) dem Ausführen jeder einzelnen Testfall-Methode diese Methode aufrufen wird – und das völlig automatisch!
Um die Annotation @Before auch nutzen zu können, müssen wir sie jedoch vorher noch importieren. Dies dient lediglich dem Zweck, nicht @org.junit.Before schreiben zu müssen.
Wir verändern also den Quelltext wie folgt:
package factorial;
import static org.junit.Assert.*;
import org.junit.Test;
import org.junit.Before;
public class FactorialTest {
private Factorial m_factorial;
@Before
public void setUp () {
m_factorial = new Factorial ();
9
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
}
@Test
public void fac0 () {
//setUp ();
assertEquals (1, m_factorial.compute (0));
}
@Test
public void fac1 () {
//setUp ();
assertEquals (1, m_factorial.compute (1));
}
@Test
public void fac2 () {
//setUp ();
assertEquals (2, m_factorial.compute (2));
}
@Test
public void fac3 () {
//setUp ();
assertEquals (6, m_factorial.compute (3));
}
@Test
public void fac4 () {
//setUp ();
assertEquals (24, m_factorial.compute (4));
}
@Test
public void fac10 () {
//setUp ();
assertEquals (3628800, m_factorial.compute (10));
}
}
(Die Aufrufe von setUp kannst du natürlich auch löschen; sie sind in diesem Beispiel auskommentiert, damit du im Dokument erkennen kannst, dass an dieser Stelle eine Veränderung stattgefunden
hat.)
Nun lässt du abermals alle Testfälle durchlaufen und siehst am grünen Balken, dass die durchgeführten Änderungen keinen negativen Einfluss auf die Richtigkeit der Tests haben. Der Test-Code
ist jedoch übersichtlicher geworden, da Erzeugung und Benutzung der zu testenden Objekte sichtbar voneinander getrennt wurden: Die Initialisierung der Testfälle steht in der setUp-Methode, die
Ausführung der Testfälle ist in den test...-Methoden zu finden.
3.2 Testfälle und Ausnahmen
Wenn Programmcode getestet werden soll, der in der Lage ist, Ausnahmen auszulösen, ist etwas
mehr (aber nicht wirklich viel) Arbeit notwendig. Zuerst musst du zwischen zwei möglichen Situationen unterscheiden:
•
Du möchtest Programmcode testen, der eigentlich keine Ausnahme auslösen sollte. Falls eine
Ausnahme dennoch auftritt, ist der Testfall fehlgeschlagen.
•
Du möchtest Programmcode testen, der eine Ausnahme auslösen soll. Falls keine Ausnahme auftritt, ist der Testfall fehlgeschlagen.
10
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
3.2.1 Unerwünschte Ausnahmen
Der erste Fall ist der einfachere. Hier erweiterst du deine Test-Methode einfach um eine passende
throws-Klausel. Gesetzt den Fall, wir haben den folgenden Testfall:
import static org.junit.Assert.*;
import org.junit.Test;
public class MyClassTest {
@Test
public void testDoSomething () {
assertEquals (42, new MyClass ().doSomething ());
}
}
Nehmen wir an, dass doSomething eine Ausnahme vom Typ MyException auslösen könnte (aber
nicht sollte), so erweitern wir die Methode um die passende throws-Klausel:
import static org.junit.Assert.*;
import org.junit.Test;
public class MyClassTest {
@Test
public void testDoSomething () throws MyException {
assertEquals (42, new MyClass ().doSomething ());
}
}
Das war auch schon alles.
3.2.2 Erwünschte Ausnahmen
Gelegentlich kommt es vor, dass du in einem Testfall prüfen möchtest, ob eine bestimmte Ausnahme ausgeworfen wird. Normalerweise erwartest du, dass keine Ausnahmen während eines Testfalls
auftreten (ansonsten kommt es ja bekanntlich zu einem Fehler). Um das Gegenteil zu erreichen,
d. h. die Existenz einer Ausnahme als ”richtig” und die Abwesenheit derselben als ”falsch” zu behandeln, müssen wir etwas mehr tun als gewöhnlich.
Gesetzt den Fall, wir haben den folgenden (noch nicht fertigen) Testfall:
import static org.junit.Assert.*;
import org.junit.Test;
public class MyOtherClassTest {
@Test
public void testThrowException () {
new MyOtherClass ().throwException ();
}
}
Wir wissen, dass throwException eine Ausnahme vom Typ MyOtherException auswerfen soll,
und wir wollen dies überprüfen. Dazu müssen wir die Anweisung oder den Code-Block, der die
Ausnahme generiert, in einen try/catch-Block einschließen, um die die Ausnahme aufzufangen:
import static org.junit.Assert.*;
import org.junit.Test;
public class MyOtherClassTest {
@Test
11
Einführung in JUnit 4, Version 1.2
}
Drachau/Schulz
public void testThrowException () {
try {
new MyOtherClass ().throwException ();
}
catch (MyOtherException e) {
// alles in Ordnung!
}
}
Nun überlegen wir: Wenn die Ausnahme ausgeworfen wird, geraten wir unweigerlich in den
catch-Block. Da in diesem Fall jedoch alles in Ordnung ist (schließlich haben wir die Ausnahme
erwartet), bleibt der Anweisungsteil im catch-Block leer. Wenn jedoch keine Ausnahme ausgeworfen wird, beendet sich der Testfall bis jetzt ebenfalls auf “natürlichem” Wege. Das ist schlecht,
denn so bekommt JUnit nicht mit, dass eigentlich eine Störung aufgetreten ist (eine erwartete Ausnahme ist nicht eingetreten). Deshalb ändern wir den obigen Code leicht ab und rufen nach der Anweisung, die eine Ausnahme auswerfen soll, eine JUnit-Operation auf, um einen Fehler zu melden:
import static org.junit.Assert.*;
import org.junit.Test;
public class MyOtherClassTest {
@Test
public void testThrowException () {
try {
new MyOtherClass ().throwException ();
fail ("MyOtherException expected");
}
catch (MyOtherException e) {
// alles in Ordnung!
}
}
}
Die Methode fail, die hier aufgerufen wird, dient dazu, einen Testfall mit einem Fehler zu beenden. Optional kann man (wie im Beispiel) eine zusätzliche Nachricht übergeben, um den ausgelösten Fehler genauer zu kennzeichnen.12
4 Zusammenfassung
Herzlichen Glückwunsch! Du hast die JUnit-Einführung für LOMF-Programmierer durchgearbeitet
und verstanden, wie du die aus LOMF gewohnte Test-getriebene Entwicklung auf Java übertragen
kannst. Neben grundlegenden Konzepten wie TestCases und asserts hast du auch gelernt, wie du
die Initialisierung von Testfällen auslagern kannst. Auch weißt du jetzt, wie du in deinen Testfällen
mit Ausnahmen umgehen kannst. Nun bist du hoffentlich fit für deine nächsten Java-Projekte. Viel
Spaß!
12
Es gibt auch noch eine andere Möglichkeit, dieses Problem zu lösen, nämlich mit Hilfe des Parameters „expected“Parameters der Annotation @Test. Diese Vorgehensweise ist jedoch nicht empfehlenswert, da man keinen Einfluss
darauf hat, in welchen Code-Abschnitten der Test-Methode die Ausnahme erwartet wird und in welchen nicht.
12
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Anhang A: Erstellung eines Java-Projekts mit JUnit-Unterstützung
Das Anlegen eines Java-Projekts mit JUnit-Unterstützung läuft prinzipiell genauso ab wie das Anlegen eines jeden anderen Java-Projekts: Zuerst musst du über das Menü (oder über die Symbolleiste)
den entsprechenden Punkt auswählen (4) und dann den Projektnamen vergeben. Anschließend ist es
wichtig, nicht die Schaltfläche „Finish“, sondern die Schaltfläche „Next“ auszuwählen (5). Anschließend musst du unter „Libraries“ (6) eine neue „Library“ hinzuzufügen (7), nämlich JUnit (8).
Du solltest die Version 4 auswählen (9), weil diese Version in diesem Dokument beschrieben wird.
Nach zweimaliger Auswahl der Schaltfläche „Finish“ (10 und 11) ist das Java-Projekt samt JUnitUnterstützung in eclipse vorhanden (12).
Abbildung 4: Anlegen eines Java-Projekts, Teil 1
13
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
14
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
15
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
16
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Abbildung 9: Integration der JUnit-Unterstützung: JUnit-Version auswählen
17
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
18
Einführung in JUnit 4, Version 1.2
Drachau/Schulz
Anhang B: Versionshistorie
Version
Datum
Änderung
0.1
08.03.2004 erste Version
0.2
12.03.2004 Anhang A hinzugefügt, optische Verbesserungen
0.3
17.03.2004 kleine Verbesserungen
0.4
16.08.2005 Thema “Ausnahme-Behandlung” hinzugefügt, optische Verbesserungen
0.5
16.08.2005 kleinere Korrekturen
1.0
12.07.2015 Umstellung auf JUnit 4
1.1
12.07.2015 Layout korrigiert, Schriftarten ersetzt
1.2
13.07.2015 Tabellenunterschrift korrigiert, fehlende Test-Annotationen ergänzt
Literaturverzeichnis
[GHJV95]
[Beck03]
Gamma, Erich; Helm, Richard; Johnson, Ralph; Vlissides, John: Design Patterns,
1995, Addison-Wesley
Beck, Kent: Test-Driven Development, 2003, Addison-Wesley
19
Herunterladen