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