Ich verwende das Test-Framework JUnit, ein Open-Source-Test-Framework von Erich Gamma und Kent Beck [JUnit]. Das Framework ist sehr einfach, ermửglicht aber alle wichtigen Dinge, die Sie zum Testen benửtigen. In diesem Kapitel ver- wende ich dieses Framework, um Tests für einige I/O-Klassen zu entwickeln.
Ich beginne mit einer Klasse FileReaderTester, um das Lesen von Dateien zu testen. Jede Klasse, die etwas testet, muss eine Unterklasse der Testfallklasse des Frameworks sein. Das Framework verwendet das Kompositum-Muster [Gang of Four], das es ermửglicht, Testfọlle zu Testsuites zusammenzufassen (Abbildung 4-1). Solche Suites kửnnen einzelne Testfọlle, aber auch Folgen von Testfọllen ent- halten. Das macht es leicht, eine Reihe groòer Testsuites aufzubauen und die Tests automatisch auszuführen.
Abbildung 4-1 Die Kompositum-Struktur von Tests Test
ôinterfaceằ
TestSuite TestCase
FileReaderTester test.framework
∗
class FileReaderTester extends TestCase { public FileReaderTester (String name) { super(name);
} }
Die neue Klasse muss einen Konstruktor haben. Anschlieòend kann ich etwas Testcode einfügen. Meine erste Aufgabe ist es, die Testeinrichtung aufzubauen.
Eine Testeinrichtung ist im Wesentlichen ein Objekt, das die Testdaten enthọlt.
Da ich eine Datei lese, brauche ich eine Testdatei wie folgt:
Um diese Datei zu verwenden, bereite ich die Einrichtung vor. Die Klasse TestCase bietet zwei Methoden, um die Testeinrichtung zu manipulieren: setUp erzeugt die Objekte und tearDown entfernt sie. Beide sind in TestCase als Null-Methoden imp- lementiert. Meistens brauche ich tearDown nicht (das kann der Garbage-Collector erledigen), aber es ist vernỹnftig, sie hier einzusetzen, um die Datei zu schlieòen:
class FileReaderTester...
protected void setUp() { try {
_input = new FileReader("data.txt");
} catch (FileNotFoundException e) {
throw new RuntimeException ("unable to open test file");
} }
protected void tearDown() { try {
_input.close();
} catch (IOException e) {
throw new RuntimeException ("error on closing test file");
} }
Nachdem ich nun die Testeinrichtung habe, kann ich beginnen, Tests zu schrei- ben. Der erste besteht darin, die Methode read zu testen. Hierzu lese ich einige Zeichen und prỹfe dann, ob das Zeichen, das ich als nọchstes lese, das richtige ist:
Bradman 99,94 52 80 10 6996 334 29
Pollock 60,97 23 41 4 2256 274 7
Headley 60,83 22 40 4 2256 270* 10
Sutcliffe 60,73 54 84 9 4555 194 16
public void testRead() throws IOException { char ch = '&';
for (int i=0; i < 4; i++) ch = (char) _input.read();
assert('d' == ch);
}
Der automatische Test ist die Methode assert. Ist der Wert innerhalb der Klam- mern wahr, so ist alles in Ordnung. Andernfalls wird ein Fehler angezeigt. Ich zeige spọter, wie das Framework dies macht. Zunọchst zeige ich, wie man Tests ausführt.
Der erste Schritt ist das Erstellen einer Testsuite. Dazu erstelle ich eine Methode namens suite:
class FileReaderTester...
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(new FileReaderTester("testRead"));
return suite;
}
Diese Testsuite enthọlt nur ein Testfallobjekt, eine Instanz von FileReaderTester.
Wenn ich einen Testfall erstelle, so übergebe ich dem Konstruktor einen String mit dem Namen der Methode, die ich testen will. Dies erzeugt ein Objekt, das diese eine Methode testet. Der Test wird durch den Reflektionsmechanismus von Java mit dem Objekt verknỹpft. Sie kửnnen sich das in im Quellcode ansehen, um herauszufinden, wie es funktioniert. Ich behandele es hier einfach als Magie.
Um die Tests auszuführen, verwende ich eine getrennte Klasse TestRunner. Es gibt zwei Versionen von TestRunner: Die eine verwendet ein schickes GUI, die andere eine einfache zeichenorientierte Schnittstelle. Letztere kann ich in main aufrufen:
class FileReaderTester...
public static void main (String[] args) { junit.textui.TestRunner.run (suite());
}
Dieser Code erzeugt ein TestRunner-Objekt und lọsst es die FileReaderTester- Klasse testen.
Wenn ich den Code ausführe, sehe ich:
.
Time: 0.110 OK (1 tests)
JUnit druckt einen Punkt für jeden Test, den es durchführt (so dass ich den Fort- schritt sehen kann). Es gibt aus, wie lange der Test gedauert hat. Dann folgen
ằOKô, wenn nichts schief gegangen ist, und die Anzahl ausgefỹhrter Tests. Ich kann tausend Tests ausfỹhren, und wenn alles gut lọuft, sehe nur dieses OK. Diese einfache Rückkopplung ist entscheidend für selbst testenden Code. Ohne sie wür- den Sie die Tests nie oft genug ausfỹhren. Durch sie kửnnen Sie Massen von Tests ausführen, zum Essen gehen (oder in ein Meeting) und sich die Ergebnisse anse- hen, wenn Sie zurückkommen.
Beim Refaktorisieren führen Sie nur wenige Tests für den Code aus, an dem Sie ge- rade arbeiten. Sie kửnnen nur wenige durchfỹhren, da sie schnell sein mỹssen:
Andernfalls wỹrden Sie gebremst, und Sie wọren versucht, die Tests nicht auszu- führen. Geben Sie dieser Versuchung nicht nach – die Vergeltung folgt bestimmt.
Was passiert, wenn etwas schief geht? Ich demonstriere dies, indem ich extra ei- nen Fehler einbaue:
public void testRead() throws IOException { char ch = '&';
for (int i=0; i < 4; i++) ch = (char) _input.read();
assert('2' == ch); //!!Fehler!!
}
Das Ergebnis sieht so aus:
.F
Time: 0.220
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0 There was 1 failure:
Führen Sie Ihre Tests oft aus. Verwenden Sie Ihre Tests bei jeder Umwandlung – jeden Test mindestens einmal tọglich.
1) FileReaderTester.testRead test.framework.AssertionFailedError
Das Framework alarmiert mich wegen des Fehlers und sagt mir, welcher Test fehl- schlug. Die Fehlermeldung ist allerdings nicht besonders hilfreich. Ich kann die Fehlermeldung verbessern, indem ich eine andere Form der Zusicherung ver- wende:
public void testRead() throws IOException { char ch = '&';
for (int i=0; i < 4; i++) ch = (char) _input.read();
assertEquals('m',ch);
}
Die meisten Zusicherungen vergleichen zwei Werte, um zu sehen, ob sie gleich sind. Deshalb enthọlt das Framework assertEquals. Das ist bequem; es verwendet equals() bei Objekten und == bei Werten, was ich oft vergesse zu tun. Es ermửg- licht auch eine aussagekrọftigere Fehlermeldung:
.F
Time: 0.170
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0 There was 1 failure:
1) FileReaderTester.testRead "expected:"m"but was:"d""
Ich sollte erwọhnen, dass ich beim Schreiben von Tests oft damit beginne, sie scheitern zu lassen. Bei vorhandenem Code ọndere ich entweder diesen, so dass er Fehler liefert (wenn ich an den Code herankomme), oder ich verwende einen fal- schen Wert in der Zusicherung. Ich mache dies, um mir selbst zu beweisen, dass der Test tatsọchlich durchgefỹhrt wird und auch tatsọchlich das testet, was er tes- ten soll (deshalb ọndere ich wenn mửglich den getesteten Code). Das mag para- noid erscheinen, aber es kann Sie sehr verwirren, wenn Tests etwas anderes testen, als Sie annehmen.
Auòer falschen Ergebnissen (die Zusicherungen liefern den Wert falsch), fọngt das Framework auch Fehler ab (unerwartete Ausnahmen). Schlieòe ich einen Stream und versuche anschlieòend von ihm zu lesen, so sollte eine Ausnahme ausgelửst werden. Ich kann dies mit folgendem Code testen:
public void testRead() throws IOException { char ch = '&';
_input.close();
for (int i=0; i < 4; i++)
ch = (char) _input.read();// wird eine Ausnahme auslửsen assertEquals('m',ch);
}
Führe ich dies aus, so erhalte ich:
.E
Time: 0.110
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 0 Errors: 1 There was 1 error:
1) FileReaderTester.testRead java.io.IOException: Stream closed
Es ist nützlich, zwischen falschen Ergebnissen und Fehlern zu unterscheiden, da sie unterschiedlich erscheinen und der Korrekturprozess anders ist.
Abbildung 4-2 Die grafische Benutzerschnittstelle von JUnit
JUnit hat auch ein schickes GUI (siehe Abbildung 4-2). Der Fortschrittsbalken ist grün, wenn alle Tests erfolgreich durchlaufen wurden, und rot, wenn es irgend- welche falschen Ergebnisse gibt. Sie kửnnen das GUI die ganze Zeit offen lassen, und die Umgebung berücksichtigt automatisch alle Änderungen an Ihrem Code.
Das ist eine sehr bequeme Art zu testen.