Fortgeschrittene Themen
Übersicht: Fortgeschrittene Themen
Im weiteren wollen wir uns fortgeschrittenen Konzepten von JUnit widmen. Dieses Kapitel wird dabei helfen, besser zu verstehen, was JUnit eigentlich mit den Testfällen macht. Dadurch kann die Arbeit mit JUnit viel besser koordiniert werden.
Der Testablauf - Was passiert?
Was macht denn nun der Aufruf von junit.swingui.TestRunner.run(MoneyTest.class)? Hierbei handelt er sich um die einfachste Art des Testens mit JUnit. Die Methode TestRunner.runTest(), aufgerufen aus run(), durchsucht die Klasse MoneyTest einfach nach Methoden welche die folgenden Kriterien erfüllen:
- Sie fangen mit der Zeichenkette "test" an,
- sie nehmen keine Parameter entgegen und
- liefern keinen Rückgabewert zurück.
Daraufhin, also nach der Suche, werden alle gefundenen Funktionen in einer undefinierten Reihenfolge aufgerufen - selbstverständlich immer voneinander durch die Methoden setUp(), runTest() und tearDown() gekapselt. Der Test ist dadurch komplett.
Es gibt jedoch auch weitere Möglichkeiten Tests auszuführen. JUnit unterscheidet zwischen:
- Individuellen Tests und
- Testsuiten (TestSuite).
Individuelle Tests
Idividuelle Tests dienen dem Entwickler dazu, spezielle Tests, also <i>nicht alle</i> Tests die in einer Testklasse deklariert sind, aufzurufen (Selektion). Durch dieses Konstrukt bietet sich dem Entwickler die Möglichkeit mit Zuhilfenahme der Testsuiten Tests z.B. in einer bestimmten Reihenfolge auszuführen.
JUnit erlaubt zwei Arten von individuellen Tests: statische und dynamische. Dabei sind statische Tests typsicher, jedoch länger in der Implementierung des Aufrufs, wobei die Implementierung der dynamischen Tests kürzer, jedoch nicht typsicher ist.
Der Aufruf eines statischen individuellen Tests würde durch das Überschreiben der abstrakten TestCase.runTest Methode durch eine anonyme innere Klasse erfolgen:
01 TestCase test = new MoneyTest("simple add") {
02 public void runTest() {
03 TestSimpleAdd();
04 }
05 };
|
code13.java |
Codebeispiel 13
Wie schon oben erwähnt ist dieser Aufruf ziemlich lang. Typsicherheit ist hierbei jedoch gewährleistet.
Das Konzept des dynamischen Aufrufens von individuellen Testfällen wiederum, baut auf das Auffinden der entsprechenden Testmethode während der Laufzeit auf. Die Deklaration des Aufrufs würde hierbei wir folgt aussehen:
01 TestCase test = new MoneyTest("testSimpleAdd");
|
code14.java |
Codebeispiel 14
Dabei wird die Testklasse nach der Methode testSimpleAdd durchsucht und falls gefunden, diese aufgerufen. Durch das dynamische Suchen ist hier jedoch eine NoSuchMethodException z.B. bei Tippfehlern während der Runtime möglich. Folgend ein Teil des Codes zum dynamischen Auffinden der Testroutinen:
01 protected void runTest() throws Throwable {
02 Method runMethod = null;
03
04 try {
05 runMethod = getClass().getMethod(fName, new Class[0]);
06 } catch (NoSuchMethodException e) {
07 assert("Method \""+fName+"\" not found", false);
08 }
09
10 try {
11 runMethod.invoke(this, new Class[0]);
12 }
13 // catch InvocationTargetException and IllegalAccessException
14 }
|
code15.java |
Codebeispiel 15
Testsuiten
Die sogenannten Testsuiten (Klasse TestSuite) haben zwei Funktionen. Die erste dient dazu, verschiedene Tests im einer <u>bestimmten</u> Reihenfolge aufzurufen. Suiten werden durch die Deklaration der Methode suite in der abgeleiteten Testklasse, durch den TestRunner identifiziert.
01 public class MoneyTest extends TestCase {
02 // ...
03
04 public static Test suite() {
05 TestSuite suite = new TestSuite();
06
07 suite.addTest(new MoneyTest("testEquals")); // dynamisch!
08 suite.addTest(new MoneyTest("testSimpleAdd")); // dynamisch!
09
10 return suite;
11 }
12 }
|
code16.java |
Codebeispiel 16
Weiterhin können Suiten jedoch auch dazu verwendet werden, verschiedene Klassen eines Softwareprojektes bzw. Paketes, auf einmal zu testen. Dies ermöglicht das mit TestCase gemeinsam implementierte Interface Test. Dieses Interface bietet dem JUnit TestRunner die Möglichkeit, entweder einen einfachen Testfall, oder eine vollständige Testsuite zu testen, ohne der Notwendigkeit der Unterscheidung.
Um diese Funktionalität zu erreichen, verschieben wir die Deklaration der main-Methode aus den einzelnen Tests (z.B. MoneyTest) in die Suite hinein. Diese übernimmt dann die Tests der einzelnen Klassen. Zur Veranschaulichung setzten wir die beiden Tests GirlTest und BoyTest voraus:
01 import junit.framework.*;
02
03 public class AllTests {
04 public static Test suite() {
05 TestSuite suite = new TestSuite();
06
07 suite.addTestSuite(BoyTest.class);
08 suite.addTestSuite(GirlTest.class);
09 suite.addTestSuite(MoneyTest.class);
10
11 return suite;
12 }
13
14 public static void main(String[] args) {
15 junit.swingui.TestRunner.run(AllTests.class);
16 }
17 }
|
code17.java |
Codebeispiel 17
Durch einen einzelnen Aufruf, können wir nun also vollständige Pakete testen. Der dadurch entstehende Baum (eine Suite die eine Suite testet, die ein Testcase testet) wird dabei vollständig durch den TestRunner abgearbeitet.
MoneyTest - Erweiterung
Folgend wollen wir eine Erweiterung unser oberen Klasse Money entwickeln. Diese soll es ermöglichen nun auch verschiedene Währungen zu addieren. Welche Anforderungen sind denn an die Addition gegeben, oder besser: wie sieht ein Wechsel von Kursen überhaupt aus? Die Anforderungen lassen sich vereinfach wie folgt beschreiben:
- Bei Addition von zwei verschiedenen Währungen soll der Kurs auf die erste Währung gewechselt werden
- Die Menge des Geldes ist nur tagesgenau wichtig (Speicherung des Betrages in Money).
Hierzu können wir einen eigenen Testfall wie folgt implementieren:
01 public void testChange() {
02 Assert.assertEquals(this.f12EUR.add(this.f10USD),
03 this.f12EUR.add(new Money(10 *
04 Bank.getTodaysRate("USD","EUR"),"EUR")));
05 }
|
code18.java |
Codebeispiel 18
Folglich erweitern wir auch das Fixture:
01 public class MoneyTest extends TestCase {
02 // ...
03
04 private Money f10USD;
05
06 protected void setUp() {
07 // ...
08 this.f10USD = new Money(10, "USD");
09 }
10 }
|
code19.java |
Codebeispiel 19
Das Ausführen des Tests würde an dieser Stelle einen Fehler produzieren, da Money.add nicht die Funktionalität besitzt, verschiedene Währungen richtig miteinander zu addieren.
Diese hier doch ziemlich trivialen Sachverhalte sollen nur die Problematik innerhalb eines großen Projektes darstellen. Oftmals kommt es vor, dass große Klassen um weitere Funktionen erweitert werden sollen, und dass dabei bereits implementierte Teile des Codes nicht richtig mit den Erweiterungen zusammenarbeiten. Gerade in diesen Fällen zeigt JUnit seine Stärke.
Zur Vollständigkeit die korrigierte Version von Money.add:
01 public Money add(Money oMoney) {
02
03 if(this.currency.equals(oMoney.getCurrency())) {
04 return new Money(this.amount + oMoney.getAmount(), this.currency);
05 } else {
06 return new Money(this.amount + oMoney.getAmount() *
07 Bank.getTodaysRate(oMoney.getCurrency(), this.currency),
08 this.currency);
09 }
10 }
|
code20.java |
Codebeispiel 20
Testen von Exceptions
Wir haben am Anfang des Artikels erfahren, dass JUnit intern mit Exceptions zur Fehlererkennung arbeitet. Doch: was geschieht in dem Fall, dass gerade das Werfen einer Exception an einer bestimmten Stelle des Programms auftreten soll, und dieser Sachverhalt zu testen ist? Nun, JUnit bietet deckt auch diesen Fall ab.
Zur Veranschaulichung greifen wir ein letztes Mal auf die Money-Klasse zurück und nehmen dabei an, dass es keine negativen Geldbeträge gibt. Eine Instanz von Money mit z.B. -12 EUR soll hierbei eine Exception herbeiführen. Die Testroutine dazu würde wie folgt aussehen:
01 public class MoneyTest extends TestCase {
02 //...
03
04 public void testNegative() {
05 try {
06 new Money(-12, "EUR");
07
08 fail("IllegalArgumentException erwartet!");
09
10 } catch (IllegalArgumentException expected) {
11 }
12 }
13 }
14
15
16 public class MoneyTest extends TestCase {
17 //...
18 public Money(int amount, String currency) {
19 if(amount < 0) {
20 throw new IllegalArgumentException("Negative amount");
21 }
22
23 this.amount = amount;
24 this.currency = currency;
25 }
26 }
|
code21.java |
Codebeispiel 21
Das erreichen des Aufrufs der Methode fail innerhalb des try-Blocks ist hier der eigentliche Fehler. Durch diesen Aufruf teilen wir JUnit mit, dass ein Fehler aufgetreten ist. Im anderen Fall (Betreten des catch-Blockes), ist der Test erfolgreich durchgelaufen.
Code generated with AusarbeitungGenerator Version 1.1, weblink