JUnit Eine Testumgebung für Java JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Gliederung • Was ist JUnit / Motivation • Allgemeine Struktur von JUnit • Beispiel / Verfeinerung • Zusammenfassung JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Was ist JUnit? • • • • • JUnit JUnit JUnit JUnit JUnit ist ein Test-Framework. bietet Klassen an um geschriebenen Quelltext leicht zu prüfen. verlangt während der Tests keine Benutzerinteraktion. verlangt ein wenig Disziplin. ist einfach anzuwenden. Was ist JUnit nicht? • Ein Wundermittel – die Tests schreiben sich nicht von selbst. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Motivation – warum JUnit? • Tests sind mindestens so wichtig wie Programmierung an sich. • Tests werden oftmals vernachlässigt: – Wohlgefühl während des Programmierens. – Straffer Zeitplan. • Es gibt nur beschränkte Möglichkeiten um Tests durchzuführen: – Debuger • Je umfangreicher das Projekt, desto mühseliger und komplexer. – Standardausgabe • Unübersichtlich in Code und Ausgabe, Beispiel: for-Schleife. • JUnit schafft Abhilfe. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Grundprinzipien! • Erst denken, dann coden. • Erst testen (!) dann coden. • Test a little, write a little, test a little, write a little. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Struktur von JUnit • Installation: einfach die junit.jar dem CLASSPATH hinzufügen. • Alle zum Testen notwendigen Klassen sind im Paket junit.framework enthalten. • Weiteres Prinzip: zur jeder verfassten Klasse eine Testklasse entwerfen. • Zum Schreiben von Tests werden lediglich benötigt: TestCase, Assert, (TestSuite) • Zum Ausführen der Tests werden benötigt: TestRunner JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase (1) • TestCase stellt bei der Implementierung eigener Tests die abzuleitende Klasse dar. • Das Interface Test soll vorerst vernachlässigt werden. public abstract class TestCase implements Test { private final String fName; public TestCase(String name) { fName = name }; public void run() { setUp(); runTest(); tearDown(); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase (2) • setUp(): Bereitet die Umgebung darauf vor den Test durchzuführen (Initialisierung von Fixtures). • runTest(): führt den eigentlichen Test aus. • tearDown(): Kann dazu benutzt werden die mit setUp() initialisierten Werte aufzuräumen (z.B. Netzwerkverbindung kappen). • Template: protected void setUp() { }; protected void runTest() { }; protected void tearDown() { }; JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase (3) • Protokollierung der Fehler durch TestResult. public abstract class TestCase implements Test { // ... public void run(TestResult result) { result.startTest(this); setUp(); runTest(); tearDown(); } public TestResult run() { TestResult result = new TestResult(); run(result); return result; } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Grundsätzlicher Testablauf (1) • Jeder Test wird grundsätzlich gekapselt: – Vor jedem Test werden die Werte mit setUp() initialisiert, – mit runTest() der Test durchgeführt und – mit tearDown() aufgeräumt. • Jeder Aufruf der drei Routinen ist durch try-catch-Blöcke gekapselt: – In jeden Fall werden alle Tests durchgeführt. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Grundsätzlicher Testablauf (2) new XXXTest(“testFoo“); setUp(); runTest(); tearDown(); new XXXTest(“testFoo“); setUp(); runTest(); tearDown(); new XXXTest(“testFoo“); setUp(); runTest(); tearDown(); JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestResult (1) • Die Klasse TestResult zählt die durchgeführten Tests. public class TestResult extends Object { protected int fRunTests; public TestResult() { fRunTests = 0; } public synchronized void startTest(Test test) { fRunTests++; } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestResult (2) • Weiterhin werden hier die Fehler gespeichert. public class TestResult extends Object { // ... protected Vector fFailures; protected Vector fErrors; public synchronized void addError(Test test, Throwable t) { fErrors.addElement(new TestFailure(test, t)); } public synchronized void addFailure(Test test, Throwable t) { fFailures.addElement(new TestFailure(test, t)); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Fehler oder Fehler? • JUnit unterscheidet zwei Arten von Fehlern: – failures: Fehler die durch die negative Auswertung einer zuvor gestellten Behauptung entstanden sind. – errors: Fehler die unerwartet entstanden sind, wie z.B. eine ArrayIndexOutOfBoundException • Die Klasse TestFailure dient nur zur Speicherung der Fehler im Vector. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase (4) public void run(TestResult result) { result.startTest(this); setUp(); try { runTest(); } catch (AssertionFailedError e1) { result.addFailure(this, e1); } catch (Throwable e2) { result.addError(this, e2); } finally { tearDown(); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase, Assert und TestRunner • JUnit stellt die abstrakte Klasse TestCase zur Verfügung. • TestCase wird auf einen speziellen Testfall abgeleitet und erweitert. • Framework führt die abgeleiteten Testfälle aus. • Assert: eine Behauptung. • Wir behaupten, dass das Ergebnis eines Tests ein bestimmtes Ergebnis zurückliefern sollte. • Der TestRunner führt den eigentlichen Test des Codes durch. – Text basierend – Swing basierend JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Assert • Assert = Behauptungen. – True: Wahrheit – False: Falschheit – Null: Wert gleich Null. – NotNull: Wert nicht gleich Null. – Same: Referenz stimmt überein. – NotSame: Referenz stimmt nicht überein. – Equals: Ruft Object.equals auf. • Werfen AssertionFailedError wenn Test fehlschlägt. Assert.assertTrue(expected.equals(result)); Assert.assertEquals(foo, coo); Assert.assertEquals(“foo coo test“, foo, coo); JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Ein Beispiel – Klasse Money • Klasse zum Speichern von Geldbeträgen. • Unterstützung von verschiedenen Währungen. – Vorerst: nur eine Währung. • Addition von Währungen soll ermöglicht werden. Es gilt: • Erst testen, dann coden! JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Simpler Testfall • Zwei Beträge: 12 und 14 Euro. • Ergebnis der Addition ist richtig wenn 12 + 14 = 26 Euro. public class MoneyTest extends TestCase { public void testSimpleAdd() { Money m12EUR = new Money(12, "EUR"); Money m14EUR = new Money(14, "EUR"); Money expected = new Money(26, "EUR"); Money result = m12EUR.add(m14EUR); Assert.assertTrue(expected.equals(result)); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Identifikation • Um einen bestimmten Testfall später leichter identifizieren zu können, wird ein Name vergeben. • Der TestRunner wird diesen später anzeigen. public class MoneyTest extends TestCase { // ... public MoneyTest(String name) { super(name); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube MoneyTest – Stand 1 import junit.framework.*; public class MoneyTest extends TestCase { public MoneyTest(String name) { super(name); } public void testSimpleAdd() { Money m12EUR = new Money(12, "EUR"); Money m14EUR = new Money(14, "EUR"); Money expected = new Money(26, "EUR"); Money result = m12EUR.add(m14EUR); Assert.assertTrue(expected.equals(result)); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Money • Ausgehend vom Test können wir nun die Klasse konstruieren. class Money { private double amount; private String currency; public Money(double amount, String currency) { this.amount = amount; this.currency = currency; } public double getAmount() { return this.amount; } public String getCurrency() { return this.currency; } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Money - Addition • Hinzufügen der Addition. – Vorerst nur Addition von gleichen Währungen. public Money add(Money oMoney) { return new Money(this.amount + oMoney.getAmount(), this.currency); } Was ist mit der Gleichheit? • Ist gewährleistet wenn Betrag und Währung übereinstimmen, nicht die Referenz, also: – Object.equals muss überschrieben werden, jedoch: • Zuerst die Anforderung an Money.equals festlegen. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Gleichheit - Anforderungen • • • • • Darf nicht null gleichen. Muss sich selbst gleichen. Muss neuem Objekt mit gleichem Betrag und Währung gleichen. Darf nicht anderem Betrag/Währung gleichen. etc. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Gleichheit - Test • Der Test auf Gleichheit ergibt sich zu: public void testEquals() { Money m12EUR = new Money(12, "EUR"); Money m14EUR = new Money(14, "EUR"); Assert.assertTrue(!m12EUR.equals(null)); Assert.assertEquals(m12EUR, m12EUR); Assert.assertEquals(m12EUR, new Money(12, "EUR")); Assert.assertTrue(!m12EUR.equals(m14EUR)); } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Addition und Gleichheit: Codeverdopplung • Codeverdopplung schon in diesen kleinen Tests: Money m12EUR = new Money(12, "EUR"); Money m14EUR = new Money(14, "EUR"); • Abhilfe: Fixtures (Inventar bzw. Ausstattung). • Fixtures definieren Objekte die in mehreren Tests verwendet werden können. • Fixtures werden durch Überschreiben der abstrakte Methode TestCase.setUp() initialisiert. – Initialisierung findet vor jedem einzelnen Testlauf statt. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Fixture (MoneyTest – Stand 2) import junit.framework.*; public class MoneyTest extends TestCase { private Money f12EUR; private Money f14EUR; protected void setUp() { this.f12EUR = new Money(12, "EUR"); this.f14EUR = new Money(14, "EUR"); } public void testSimpleAdd() { Money expected = new Money(26, "EUR"); Money result = this.f12EUR.add(this.f14EUR); Assert.assertTrue(expected.equals(result)); } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Fixture (MoneyTest – Stand 2) • Und der Test auf Gleichheit: public void testEquals() { Assert.assertTrue(!this.f12EUR.equals(null)); Assert.assertEquals(this.f12EUR, this.f12EUR); Assert.assertEquals(this.f12EUR, new Money(12, "EUR")); Assert.assertTrue(!this.f12EUR.equals(this.f14EUR)); } } Und nun: Money.equals • Basierend auf dem Test kann nun Money mit equals erweitert werden. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Money.equals public boolean equals(Object aObject) { if(aObject instanceof Money) { Money aMoney = (Money) aObject; return aMoney.getCurrency().equals(this.currency) && aMoney.getAmount() == this.amount; } return false; } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Ein erster Test • Nachdem wir beide, MoneyTest und Money erstellt haben, kann nun ein erster Test erfolgen. • Die Testumgebung kann dabei – als Swing-Anwendung bzw. – Textbasierend ablaufen. public class MoneyTest extends TestCase { // ... public static void main(String[] args) { junit.swingui.TestRunner.run(MoneyTest.class); //junit.textui.TestRunner.run(MoneyTest.class); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Der Test – was passiert? • junit.swingui.TestRunner.run(MoneyTest.class)? • Dies ist die einfachste Art des Testens. • TestRunner.runTest() durchsucht die Testklasse nach Methoden die: – Mit der Zeichenkette „test“ anfangen und – keine Parameter entgegennehmen sowie – keinen Rückgabewert haben (void). • Die Rheinfolge der Aufrufe ist grundsätzlich undefiniert. • Andere Möglichkeiten? Ja: – Individuelle Tests – Testsuiten (test suite) JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Individuelle Tests • Dienen dazu spezielle Tests, also „nicht alle“ Tests die in einer Testklasse deklariert sind aufzurufen (Selektion). • Zwei Arten von individuellen Tests: – statisch – dynamisch • Statisch: Typsicher, jedoch länger. • Dynamisch: Kurz, jedoch nicht typsicher. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Individuelle Tests - statisch • Statisch bedeutet: überschreiben der abstrakten TestCase.runTest Methode durch eine anonyme innere Klasse: TestCase test = new MoneyTest("simple add") { public void runTest() { testSimpleAdd(); } }; • Wie schon besprochen: unübersichtlich und lang, jedoch typsicher. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Individuelle Tests - dynamisch • Der TestRunner findet die entsprechende Testmethode während der Laufzeit. • Deklaration: TestCase test = new MoneyTest("testSimpleAdd"); • Dabei wird die Testklasse nach der Methode testSimpleAdd durchsucht und falls gefunden diese aufgerufen. • NoSuchMethodException ist möglich (Runtime)! – z.B. bei Tippfehlern JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube TestCase (5) protected void runTest() throws Throwable { Method runMethod = null; try { runMethod = getClass().getMethod(fName, new Class[0]); } catch (NoSuchMethodException e) { assert("Method \""+fName+"\" not found", false); } try { runMethod.invoke(this, new Class[0]); } // catch InvocationTargetException and IllegalAccessException } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Testsuiten (test suite), Klasse TestSuite (1) • Dienen dazu verschiedene Tests in einer bestimmten Rheinfolge aufzurufen. • Suiten werden durch die Deklaration der Methode suite durch den TestRunner identifiziert. public class MoneyTest extends TestCase { // ... public static Test suite() { TestSuite suite = new TestSuite(); suite.addTest(new MoneyTest("testEquals")); // dynamisch! suite.addTest(new MoneyTest("testSimpleAdd")); // dynamisch! return suite; } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Testsuiten (2) • Suiten können jedoch auch dazu verwendet werden, verschiedene Klassen eines Paketes bzw. Projektes auf einmal zu testen. • Die main-Methode wird dabei in eine neue Klasse verschoben welche – die Tests der einzelnen Klassen übernimmt. • Hierzu eignet sich z.B. folgendes Konstrukt (GirlTest und BoyTest werden vorausgesetzt): JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Testsuiten (3) import junit.framework.*; public class AllTests { public static Test suite() { TestSuite suite = new TestSuite(); suite.addTestSuite(BoyTest.class); suite.addTestSuite(GirlTest.class); suite.addTestSuite(MoneyTest.class); return suite; } public static void main(String[] args) { junit.swingui.TestRunner.run(AllTests.class); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Übersicht TestCase und TestSuite • Durch das gemeinsame Interface Test ist es möglich Tests von Suiten von Suiten von Cases zu tätigen. – Vereinfachung: Mit einem einzelnem Aufruf Test von ganzen Paketen möglich. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Übersicht Testablauf - TestRunner JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Entwicklung und Tests • Wann sollte getestet werden? – Möglichst gleich nach dem Kompilieren. – Auf diese Weise erfahren wir schnell wann der Code anfängt zu funktionieren und wann er damit aufhört. – Hierzu kann z.B. der textbasierende TestRunner benutzt werden. • Wie soll man entwickeln? – Ein wenig testen, ein wenig entwickeln. – Hierzu: Die Anforderungen an den zu schreibenden Code sind nie mehr so genau bewusst wie während des Niederschreibens. – Deshalb: Test schreiben, Code schreiben, compilieren und gleich Testen. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Weitere Entwicklung (1) • Ausgehend vom bisherigen Stand von Money Erweiterung auf mehrere Währungen. • Bei Addition Wechsel auf die erste übergebene Währung. • Menge an Geld nur tagesgenau wichtig (Speicherung in Money). • Wir erweitern den Test wie folgt: public void testChange() { Assert.assertEquals(this.f12EUR.add(this.f10USD), this.f12EUR.add(new Money(10 * Bank.getTodaysRate("USD","EUR"),"EUR"))); } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Weitere Entwicklung (2) • Erweiterung Fixture: public class MoneyTest extends TestCase { // ... private Money f10USD; protected void setUp() { // ... this.f10USD = new Money(10, "USD"); } } • Ergibt Fehler, da Money.add zur Zeit nur eine Währung unterstützt. – Folglich: Nun Erweiterung von Money. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Weitere Entwicklung (3) • Ausgehend vom negativen Testergebnis nun die Korrektur von Money. public Money add(Money oMoney) { if(this.currency.equals(oMoney.getCurrency())) { return new Money(this.amount + oMoney.getAmount(), this.currency); } else { return new Money(this.amount + oMoney.getAmount() * Bank.getTodaysRate(oMoney.getCurrency(), this.currency), this.currency); } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Testen von Exceptions (1) • Manchmal möchte man sich vergewissern, dass eine bestimmte Exception geworfen wird. • Annahme: Es gibt keine negativen Geldbeträge. Wenn also ein Money(-12, “EUR“) erstellt wird, soll eine Exception geworfen werden. • Problem: Eine Exception wird JUnit als einen Fehler erkennen. • Lösung: try, catch in der Testmethode. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Testen von Exceptions (2) public class MoneyTest extends TestCase { //... public void testNegative() { try { new Money(-12, “EUR“); fail("IllegalArgumentException erwartet!"); } catch (IllegalArgumentException expected) { } } } public class MoneyTest extends TestCase { //... public Money(int amount, String currency) { if(amount < 0) { throw new IllegalArgumentException("Negative amount"); } this.amount = amount; this.currency = currency; } } JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Zusammenfassung • Ein einfach strukturiertes jedoch sehr gut durchdachtes Werkzeug. • Tests verlangen, nachdem sie einmal verfasst wurden vom Entwickler kein aktives Denken (außer im Fehlerfall). • Tests unterstützen den Entwickler während der ganzen Entwicklung: – Wir merken schnell wann Code aufhört zu funktionieren. – Wir müssen die Tests nicht im eigentlichen Code integrieren, demnach entfallen evtl. Doppelarbeiten die mit Löschen von Tests (Standardausgabe) zusammenhängen. • Fazit: sehr empfehlenswert. JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube Schlussbemerkung • JUnit wurde durch JUnit geprüft. • Der TestTest hat funktioniert. • Mehr Informationen: – www.junit.org (englisch) – www.frankwestphal.de/UnitTestingmitJUnit.html (deutsch) JUnit - eine Testumgebung für Java, Informatikseminar WS02/03, Kamil Kube