Fortgeschrittene Themen


... [ Seminar "JUnit" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ...

Ü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:
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:

[ nach oben ]

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


[ nach oben ]

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.



[ nach oben ]

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:
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


[ nach oben ]

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.



... [ Seminar "JUnit" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ... [ nach oben ] ...

valid html4 logo Code generated with AusarbeitungGenerator Version 1.1, weblink