|
|
[translated into German; source is at junit.org, © JUnit.org 2004, was: Java Report Article , Last-Modified: Sat, 20 Dec 2003 18:45:21 GMT] Infiziert vom JUnit Test: Programmierer lieben das Schreiben von TestsTesten ist nicht eng mit Entwicklung verknüpft. Dies hält Sie davon ab, den Fortschritt der Entwicklung zu messen- Sie können nicht sagen, wann etwas zu arbeiten beginnt und wann etwas seine Arbeit einstellt. Mit JUnit können Sie einfach und schrittweise eine Testumgebung aufbauen, die Ihnen helfen wird, Ihren Fortschritt zu messen, unbeabsichtigte Seiteneffekte zu entdecken und Ihre Entwicklungsbemühungen zu konzentrieren. Inhalt
Das ProblemJeder Programmierer weiß, daß er Tests für seinen Code schreiben sollte. Wenige tun es. Die Standardantwort auf "Warum nicht?" ist "Ich bin in Eile." Das führt schnell zu einem bösartigen Zyklus- je bedrückter man sich fühlt, desto weniger Tests schreibt man. Je weniger Tests man schreibt, desto weniger produktiv ist man und desto weniger stabil wird der eigene Code. Je weniger produktiv und genau man ist, desto mehr bedrückter wird man. Programmierer verausgaben sich nur durch solche Zyklen. Ausbrechen erfordert einen äußeren Einfluß. Wir haben diesen benötigten äußeren Einfluß in Form einer einfachen Testumgebung gefunden, die uns ein wenig Testen ermöglicht, der einen großen Unterschied macht. Der beste Weg, Sie vom Nutzen selbst geschriebener Tests zu überzeugen, wird sein, sich mit Ihnen hinzusetzen und ein wenig zu entwickeln. Währenddessen würden wir neue Fehler aufdecken, diese mit Tests abfangen, sie korrigiert, treten wieder auf, werden erneut korrigiert und so weiter. Sie würden den Nutzen des ständigen Feedbacks erkennen, den Sie durch das Schreiben, Speichern und erneutem Laufenlassen Ihrer eigenen Testeinheiten erhalten. Leider ist dies nur ein Artikel und kein Büro, das über der bezaubernden Altstadt von Zürich herausragt, mit der Hektik eines mittelalterlichen Geschäftstreibens draußen und dem Dröhnen des Techno aus dem Plattenladen im Stockwerk darunter, so daß wir den Prozeß der Entwicklung simulieren müssen. Wir schreiben ein einfaches Programm und seiner Tests, und zeigen Ihnen die Ergebnisse der Testläufe. Auf diese Weise können Sie ein Gefühl für das von uns verwendete Verfahren bekommen und dieses nahelegen, ohne für unsere Anwesenheit zu bezahlen. BeispielAchten Sie beim Lesen auf das Zusammenspiel des Codes und den Tests. Die Vorgehensweise hier ist, wenige Zeilen Code zu schreiben, dann einen Test, der läuft, oder sogar besser einen, der nicht läuft, und dann den Code, der ihn zum laufen bringt. Das Programm, das wir schreiben, wird das Problem lösen, das das Rechnen mit mehrfachen Währungen abbildet. Berechnungen zwischen gleichen Währungen sind trivial, denn man kann einfach beide Werte addieren. Einfache Zahlen reichen aus. Sie können vorhandene Währungen ganz und gar ignorieren. Die Dinge werden dann wesentlich interessanter, wenn es mehrere Währungen umfaßt. Sie können nicht einfach für eine Berechung eine Währung in eine andere umwandeln, nachdem es keinen individuellen Umrechnungskurs gibt- vielleicht wollten Sie den Wert eines Wertpapierdepots zum gestrigen Kurs mit dem heutigen Kurs vergleichen. Fangen wir einfach an und definieren eine
Klasse class Money { private int fAmount; private String fCurrency; public Money(int amount, String currency) { fAmount= amount; fCurrency= currency; } public int amount() { return fAmount; } public String currency() { return fCurrency; } } Wenn Sie zwei public Money add(Money m) { return new Money(amount()+m.amount(), currency()); } Statt nun einfach weiterzuprogrammieren, wollen wir ständigen Feedback haben und praktizieren "etwas codieren, etwas testen, etwas codieren, etwas testen". Um unsere Tests zu implementieren, verwenden wir das JUnit-System. Um Tests zu schreiben, benötigen Sie die neueste Ausgabe von JUnit (oder Sie schreiben Ihr eigenes Pendant- es ist nicht so viel Arbeit). JUnit definiert, wie Sie ihre Testfälle strukturieren und
stellt die Tools zur Verfügung, um sie laufen zu lassen. Sie implementieren
einen Test als Subclass von public class MoneyTest extends TestCase { //… public void testSimpleAdd() { Money m12CHF= new Money(12, "CHF"); // (1) Money m14CHF= new Money(14, "CHF"); Money expected= new Money(26, "CHF"); Money result= m12CHF.add(m14CHF); // (2) Assert.assertTrue(expected.equals(result)); // (3) } } Der Testfall
Bevor wir das Ergebnis überprüfen können, müssen wir etwas abschweifen,
da wir einen Weg benötigen, der testet, ob zwei public void testEquals() { Money m12CHF= new Money(12, "CHF"); Money m14CHF= new Money(14, "CHF"); Assert.assertTrue(!m12CHF.equals(null)); Assert.assertEquals(m12CHF, m12CHF); Assert.assertEquals(m12CHF, new Money(12, "CHF")); // (1) Assert.assertTrue(!m12CHF.equals(m14CHF)); } Die Methode Als nächstes schreiben wir die Methode public boolean equals(Object anObject) { if (anObject instanceof Money) { Money aMoney= (Money)anObject; return aMoney.currency().equals(currency()) && amount() == aMoney.amount(); } return false; } Nachdem Mit einer
Methode Nachdem
wir nun zwei Testfälle implementiert haben, stellen wir fest, daß wir Code
für die Tests mehrfach aufgesetzt haben. Es wäre schön, wenn wir den Code zur
Testvorrichtung wiederverwenden könnten. In anderen Worten: wir hätten gerne
eine einheitliche Vorlage für die Tests. Mit JUnit läßt sich das durch das
Sichern in Vorlagevariablen unserer public class MoneyTest extends TestCase { private Money f12CHF; private Money f14CHF; protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); } } Wir können die beiden Methoden der Testfälle umschreiben und dabei den gleichartigen Setupcode entfernen. public void testEquals() { Assert.assertTrue(!f12CHF.equals(null)); Assert.assertEquals(f12CHF, f12CHF); Assert.assertEquals(f12CHF, new Money(12, "CHF")); Assert.assertTrue(!f12CHF.equals(f14CHF)); } public void testSimpleAdd() { Money expected= new Money(26, "CHF"); Money result= f12CHF.add(f14CHF); Assert.assertTrue(expected.equals(result)); } Zwei zusätzliche Schritte sind nötig, um die beiden Testfälle abzuarbeiten:
JUnit unterstützt zwei Arten von einzelnen Testabläufen:
Auf die statische Art überschreibt man die Methode TestCase test= new MoneyTest("simple add") { public void runTest() { testSimpleAdd(); } }; Eine Schablonenmethode[1] in der
Superklasse stellt sicher, daß Die dynamische Art, einen durchzuführenden Testfall anzulegen,
verwendet Reflektion, um runTest zu implementieren. Es setzt voraus, daß der
Name des Tests der Name der aufzurufenden Methode des Testfalls ist. Um den Test
TestCase test= new MoneyTest("testSimpleAdd"); Die dynamische Art und Weise ist wesentlich kompakter, dafür aber weniger
typensicher. Ein Fehler im Namen des Tests bleibt unbemerkt, bis man ihn
abarbeitet und eine Als letzten Schritt müssen wir eine Testsammlung
definieren, um beide Testfälle durchführen lassen zu können. In JUnit
erfordert dies die Definition einer statischen Methode namens public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); return suite; } Seit JUnit 2.0 gibt es eine noch einfachere dynamische Art. Man übergibt public static Test suite() { return new TestSuite(MoneyTest.class); } Hier der analoge Code auf statische Art: public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest( new MoneyTest("money equals") { protected void runTest() { testEquals(); } } ); suite.addTest( new MoneyTest("simple add") { protected void runTest() { testSimpleAdd(); } } ); return suite; } Nun ist man bereit, die Test laufen zu lassen. JUnit wird mit grafischen
Oberflächen ausgeliefert, die Tests laufen lassen können. Schreiben Sie den
Namen der Testklasse in das Feld oben im Fenster. Drücken Sie dann den Run
Knopf. Während des Testlaufs zeigt JUnit den Fortschritt mit einer
Fortschrittsanzeige unterhalb des Eingabefeldes an. Der Graph ist anfangs grün,
ändert sich aber in rot, sobald ein Test nicht erfolgreich ist. Fehlgeschlagene
Tests werden in einer Liste unten angezeigt. Bild 1 zeigt
das
Nachdem wir
überprüft haben, daß der einfache Fall mit der Währung geklappt hat, fahren
wir mit mehrfachen Währungen fort. Wie oben bereits erwähnt, ist das Problem
mit unterschiedlichen Währungsumrechnungen, daß es keinen einzelnen
Wechselkurs gibt. Um dieses Problem zu umgehen, führen wir ein Ein class MoneyBag { private Vector fMonies= new Vector(); MoneyBag(Money m1, Money m2) { appendMoney(m1); appendMoney(m2); } MoneyBag(Money bag[]) { for (int i= 0; i < bag.length; i++) appendMoney(bag[i]); } } Die Methode protected void setUp() { f12CHF= new Money(12, "CHF"); f14CHF= new Money(14, "CHF"); f7USD= new Money( 7, "USD"); f21USD= new Money(21, "USD"); fMB1= new MoneyBag(f12CHF, f7USD); fMB2= new MoneyBag(f14CHF, f21USD); } Mit dieser Vorrichtung wird der Testfall zu public void testBagEquals() { Assert.assertTrue(!fMB1.equals(null)); Assert.assertEquals(fMB1, fMB1); Assert.assertTrue(!fMB1.equals(f12CHF)); Assert.assertTrue(!f12CHF.equals(fMB1)); Assert.assertTrue(!fMB1.equals(fMB2)); } Dem "etwas codieren, etwas testen" folgend, lassen wir unseren
erweiterten Test mit JUnit laufen und stellen fest, daß nach wie vor alles gut
läuft. Mit MoneyBag in der Hand können wir nun die Methode public Money add(Money m) { if (m.currency().equals(currency()) ) return new Money(amount()+m.amount(), currency()); return new MoneyBag(this, m); } So wie es definiert wurde, läßt es sich nicht compilieren, da ein interface IMoney { public abstract IMoney add(IMoney aMoney); //… } Um die unterschiedlichen Repräsentationen vor dem Clienten vollständig zu
verstecken, müssen wir Umrechnungen zwischen allen möglichen public void testMixedSimpleAdd() { // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} Money bag[]= { f12CHF, f7USD }; MoneyBag expected= new MoneyBag(bag); Assert.assertEquals(expected, f12CHF.add(f7USD)); } Die anderen Tests folgen dem selben Muster:
Als nächstes erweitern wir die Testsammlung entsprechend: public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testMoneyEquals")); suite.addTest(new MoneyTest("testBagEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); suite.addTest(new MoneyTest("testMixedSimpleAdd")); suite.addTest(new MoneyTest("testBagSimpleAdd")); suite.addTest(new MoneyTest("testSimpleBagAdd")); suite.addTest(new MoneyTest("testBagBagAdd")); return suite; } Nachdem die Testfälle definiert wurden, können wir mit deren
Implementierung beginnen. Die Herausforderung ist hier, mit allen
unterschiedlichen Kombinationen von class Money implements IMoney { public IMoney add(IMoney m) { return m.addMoney(this); } //… } class MoneyBag implements IMoney { public IMoney add(IMoney m) { return m.addMoneyBag(this); } //… } Um dies compilieren zu können, muß das interface IMoney { //… IMoney addMoney(Money aMoney); IMoney addMoneyBag(MoneyBag aMoneyBag); } Um die Implementation des double dispatch zu vervollständigen,
müssen diese Methoden implementiert werden. Dies ist diesselbe in public IMoney addMoney(Money m) { if (m.currency().equals(currency()) ) return new Money(amount()+m.amount(), currency()); return new MoneyBag(this, m); } public IMoney addMoneyBag(MoneyBag s) { return s.addMoney(this); } Nachfolgend die Implementation in public IMoney addMoney(Money m) { return new MoneyBag(m, this); } public IMoney addMoneyBag(MoneyBag s) { return new MoneyBag(s, this); } Wir lassen die Tests laufen und sie gelingen. Beim Betrachten der
Implementation entdecken wir jedoch einen anderen interessanten Fall. Was
passiert, wenn das Ergebnis einer Addition einen public void testSimplify() { // {[12 CHF][7 USD]} + [-12 CHF] == [7 USD] Money expected= new Money(7, "USD"); Assert.assertEquals(expected, fMS1.add(new Money(-12, "CHF"))); } Wird in diesem Stil entwickelt, dann wird man öfters solche Gedanken haben und sofort einen Test schreiben anstatt direkt den dazugehörigen Code zu programmieren. Es ist keine Überraschung, daß unser Test mit einer roten
Fortschrittsanzeige endet, der einen Fehler anzeigt. Nun korrigieren wir den
Code in public IMoney addMoney(Money m) { return (new MoneyBag(m, this)).simplify(); } public IMoney addMoneyBag(MoneyBag s) { return (new MoneyBag(s, this)).simplify(); } private IMoney simplify() { if (fMonies.size() == 1) return (IMoney)fMonies.firstElement(); return this; } Nun lassen wir den Test wieder laufen und voila, wir beenden ihn mit grün. Der obige Code löst nur einen kleinen Teil der Umrechnungsprobleme der Mehrfachwährungen. Wir müßten mehrere Wechselkurse repräsentieren, formatierte Ausgabe bereitstellen und andere Rechenoperationen anbieten und das alles mit ausreichender Geschwindigkeit. Wir hoffen jedoch, daß man sehen konnte, wie man den Rest der Objekte und einen Test auf einmal entwickeln kann - etwas testen, etwas codieren, etwas testen, etwas codieren. Im einzelnen zusammengefaßt war dies:
Testmethoden
Martin Fowler macht es leicht für Sie. Er sagt: "Wann immer Sie versucht
sind, irgend einen Ausdruck als Ausgabe zu formulieren oder in einen Debugger
eingeben, sollten Sie stattdessen einen Test schreiben." Man wird zuerst
denken, daß man ständig am Testvorlagen schreiben ist und das Testen wird
einen etwas ausbremsen. Bald wird man jedoch anfangen, die Bibliothek der
Vorlagen wieder zu verwenden und neue Tests werden dann relativ einfach sein,
indem einfach eine Methode zur bestehenden Man kann jederzeit mehr Tests schreiben. Man wird jedoch feststellen, daß nur ein Bruchteil der Tests, die man sich vorstellen kann, auch wirklich sinnvoll sind. Was man möchte ist, daß man Tests schreibt, die fehlschlagen, obwohl sie funktionieren sollten oder Tests, die erfolgreich sind, obwohl sie fehlschlagen sollten. Eine andere Sichtweise ist, daß man an deren Kosten-Nutzen-Faktor denkt. Man möchte Tests schreiben, die es Ihnen mit Informationen danken. Hier ein paar der Zeitpunkte, an denen man sinnvollerweise zur Testumgebung zurückkehren sollte:
Noch ein wichtiger Hinweis für Ihre Tests: Wenn ein Test zum Laufen gebracht wurde, sollten Sie sicherstellen, daß sie weiterhin laufen werden. Es gibt einen großen Unterschied zwischen einer funktionierenden Sammlung und einer defekten. Idealerweise sollte man jeden Test laufen lassen, wenn man eine Methode ändert. Ihre Sammlung wird bald praktisch zu groß sein, um sie ständig laufen zu lassen. Optimieren Sie den Vorbereitungscode, so daß alle Tests durchlaufen können. Es sollten mindestens spezielle Testsammlungen angelegt werden, die alle Tests enthalten, die möglicherweise von der momentanen Entwicklung betroffen sind. Anschließend lassen Sie diese Sammlung bei jedem Compilervorgang laufen. Stellen Sie außerdem sicher, daß jeder Test mindestens einmal am Tag läuft: über Nacht, während des Mittagessens, während eines der langen Meetings, ... FazitDieser Artikel hat das Testen nur die oberflächlich angekratzt. Er hat sich jedoch auf den Stil des Testens konzentriert, anhand dessen man mit erstaunlich kleinem Aufwand einen schnelleren, produktiveren, vorausschaubareren und weniger gestreßten Entwickler hervorbringt. Sobald man vom Testen infiziert wurde, wird sich die Einstellung bezüglich Entwicklung wahrscheinlich verändern. Nachfolgend einige Änderungen, die wir festgestellt haben. Es gibt einen großen Unterschied zwischen Tests, die alle korrekt ablaufen und solchen, die es nicht tun. Ein Teil der infizierten Tester ist nicht im Stande heimzugehen, bevor der Test nicht 100% richtig ist. Wenn man die Testsammlung zehn oder hundert Mal pro Stunde laufen läßt, wird man nicht mehr so viel Chaos anrichten können, so daß man zu spät zum Abendbrot kommt. Manchmal wird man keine Lust dazu haben, Test zu schreiben, insbesondere am Anfang. Tut es nicht. Man sollte dabei berücksichtigen, daß man in größere Schwierigkeiten kommt, je mehr Zeit man mit Debugging verbringt und wieviel Streß man mehr hat, wenn man die Tests nicht hat. Wir waren überrascht, wie viel mehr Spaß programmieren macht, wie viel mehr wir bemüht sind und wie wenig Streß wir haben, wenn wir durch Tests unterstützt werden. Der Unterschied ist so immens groß, daß er uns davon abhält, keine Tests zu schreiben, wenn wir keine Lust dazu haben. Man wird fähig sein, wesentlich mehr zu verbessern, wenn man die Tests einmal hat. Man wird am Anfang nicht verstehen, wieviel man da dennoch machen kann. Versuchen Sie sich dabei zu erwischen, wenn Sie sagen "Oh, ich denke, ich sollte das so und so anordnen. Ich kann das jetzt aber nicht ändern. Ich möchte da nichts kaputt machen." Sie sollten dann den Code sichern und ihn die nächsten Stunden aufräumen (dieser Teil funktioniert am besten, wenn Ihnen jemand über die Schultern schaut, während Sie arbeiten). Führen Sie all die Änderungen durch während die Tests laufen. Sie werden erstaunt sein, wieviel Boden Sie in ein paar Stunden gut machen können, ohne sich Sorgen machen zu müssen, daß dabei etwas kaputt gehen könnte. Wir haben
beispielsweise die Sie werden Ihr Team dazu bringen wollen, Tests zu schreiben. Wir fanden heraus, daß es am einfachsten ist, wenn dies durch direkten Kontakt geschieht. Beim nächsten Mal, wenn jemand um Hilfe beim Debuggen fragt, bringen Sie ihn/sie dazu, Testvorrichtungen und zu erwartende Ergebnisse zu diskutieren. Dann sagen Sie, daß "ich gerne das, was Sie mir gerade gesagt haben, in einer Form niederschreiben möchte, in der wir es verwenden können". Lassen Sie ihn/sie zusehen, wenn Sie einen kleinen Test schreiben. Lassen Sie ihn laufen. Korrigieren Sie ihn. Schreiben Sie einen anderen. Ziemlich bald werden die anderen ihre eigenen Tests schreiben. Nun, geben Sie JUnit eine Chance. Wenn Sie es verbessern können, teilen Sie uns bitte die Änderungen mit, damit wir sie weitergeben können. Unser nächster Artikel wird sich detailiert mit dem JUnit-System befassen. Wir werden Ihnen zeigen, wie es aufgebaut ist und etwas über die Philosophie der Entwicklung am System erzählen. Wir möchten Martin Fowler -der sowohl ein guter Programmierer als auch Analyst ist, wie man ihn sich nur wünschen kann- für seine hilfreichen Kommentare danken, auch wenn die sich auf frühe Versionen von JUnit bezogen haben. Anmerkungen
|