Geschrieben von: Christoph Wille
Kategorie: .NET Allgemein
This printed page brought to you by AlphaSierraPapa
Das Unit Testing ist in der Java Welt schon lange ein Begriff und wird auch oft eingesetzt. Im heutigen Artikel beschäftigen wir uns mit der Frage warum man Unit Testing auch in .NET einsetzen soll, wie man es machen kann, und welche Vorteile man daraus ziehen kann.
Das Testen ist meist ein Anhängsel am Entwicklungsprozess, und die meisten Entwickler verlassen sich auf die Testabteilung (oder sogar Enduser), daß Fehler gefunden werden. Wird getestet, dann passiert dies oft ohne wirkliche Struktur, denn wer kennt das nicht:
public clas Euro { public float GetEuroAmount(float dm) { float euro = dm / 1.955; // Console.WriteLine("dm->euro {0}=={1}, dm, euro); return euro; } }
Das ist natürlich keine Lösung: der Produktionscode und Testcode sind miteinander vermischt, was bedeutet, daß kein automatisches Testen möglich ist. Nur der Entwickler selbst weiß mit dem Output etwas anzufangen, und daß die Intention des Debugcodes nicht klar dokumentiert, ist nur der Zuckerguß auf diesem Problem.
Unter den Gründen die für das Unit Testing sprechen stellt sich einer besonders in den Vordergrund: der Entwickler weiß, daß sobald der Testcode läuft, sein getesteter Produktionscode in Ordnung ist. Wird später etwas am Algorithmus des Produktionscodes umgestellt, so ist ein Fehler sehr schnell und vor allem Methodengenau lokalisierbar - automatisch. Weitere Vorteile von Unit Testing sind daß der Entwickler der erste Verwender seines Produktionscodes ist, was zu besserem Design und besserem Verständnis der implementierten Features führt. Unit Testing ist auch unter dem Begriff "Test a Little, Code a Little" bekannt, und wir werden uns diese Methodologie später im Artikel ansehen.
Wenn es so viele Vorteile gibt, warum macht dann nicht jeder Unit Testing? Nun, wie Entwickler so sind, sie haben immer mindestens eine Ausrede parat:
Die grundsätzliche Idee ist, Produktions-Code und Testing-Code voneinander zu trennen. Weiters sollte ein Test pro zu testender Methode existieren, niemals den Test mehrerer Methoden in eine Testmethode verpacken (dann weiß man ja erst recht wieder nicht, was den Fehler ausgelöst hat). Ein Test für unsere Euro Klasse könnte zu aussehen:
public class TestEuro { public void TestGetEuroAmount() { Assertion.AssertEquals(new Euro().GetEuroAmount(1.955f), 1f); } }
Die Namenskonvention wäre, alles mit Test zu beginnen, sowohl den Klassennamen als auch die Testmethoden. Der Grund liegt in der leichten Assoziation von Test und Produktionscode.
Die Assertion Klasse kommt von einem Unit Testing Framework, nämlich #unit. Warum sollte man ein Unit Testing Framework einsetzen, wenn man es doch selbst schreiben könnte? Nun, ein Framework hat den Vorteil getestet zu sein, und es soll durch einfache Verwendbarkeit den Programmierer motivieren, Unit Tests zu schreiben.
Die Mutter aller Unit Testing Frameworks ist JUnit, das von Kent Beck und Erich Gamma entwickelt wurde. Fast alle anderen Unit Testing Frameworks halten sich an das Klassendiagramm dieses Java Tools. Unter .NET gibt es einen direkten Port, nämlich NUnit, der sich stark an JUnit anlehnt. Dies bringt aber etliche Nachteile mit sich:
Was kann #unit noch alles? Nun, es ist vollständig für .NET konzipiert. Das heißt, fast alles baut auf Attributen auf, und anstatt Java-like setUp und tearDown Methoden werden Konstruktoren und das IDisposable Interface unterstützt. Wer von NUnit nach #unit umsteigt wird sehen, daß die Umstellung einfach ist: man muß fast nur Code entfernen, es wird übersichtlicher als zuvor.
Als erstes muß man sich #unit downloaden. #unit ist Teil von #develop, und dessen Installationsprogramm. Nach der Installation von #develop sollte man einen weiteren Schritt setzen, der zwar optional, aber für das Schreiben von Unit Tests sehr angenehm ist: #unit in den Global Assembly Cache (GAC) installieren. Dazu gehen Sie ins #develop Verzeichnis.
Und führen dort in der Kommandozeile folgenden Befehl aus:
gacutil /i ICSharpCode.SharpUnit.dll
Dieser Befehl installiert #unit in den GAC, und nach diesem einmaligem Prozedere können wir Unit Tests schreiben. Beginnen wir damit, in #develop ein neues Projekt für unsere Euro Komponente anzulegen (File/New/New Project):
Im Project Scout klicken Sie dann mit der rechten Maustaste auf den Projektnamen (nicht die Combine, welcher der erste Eintrag ist), und wählen dann Project Options:
In der Project Options Dialogbox gehen Sie auf den Settings Tab, und stellen dort das Compile Target auf Library (Bibliothek) um:
Nun können wir mit dem Programmieren unserer Euro Komponente beginnen. Dazu fügen wir eine neue Datei dem Projekt hinzu, indem wir aus dem Kontextmenü für das Projekt Add/New File auswählen. Fügen Sie eine leere C# Datei ein, und benennen diese in euro.cs um. Der Inhalt soll am Anfang nur aus den folgenden Zeilen bestehen (im Ernst):
public class Euro { }
Trotz seiner Kurzheit läßt sich das Projekt erfolgreich kompilieren (F8 drücken oder Build aus dem Kontextmenü auswählen). Der Grund für die Kürze liegt im "Test a Little, Code a Little" Ansatz - wir wollen immer die Unit Test mitziehen. Daher fügen wir ein neues Projekt dazu:
Geben Sie dem Projekt den Namen TestEuro (wieder leeres C# Projekt wählen), und stellen Sie das Compile Target auch auf Library um. Ihr Project Scout sollte jetzt wie folgt aussehen:
Fügen Sie in das TestEuro Projekt eine neue leere Datei ein, benennen diese TestEuro.cs, und schreiben folgenden Code:
public class TestEuro { public void TestGetEuroAmount() { Euro euro = new Euro(); } }
Wenn Sie das TestEuro nun via Build aus dem Kontextmenü kompilieren wollen, bekommen Sie folgenden Fehler in der Task List:
Der Grund ist, daß das TestEuro Projekt nichts von der Euro Klasse weiß - ein Verweis auf das Projekt muß gelegt werden. Dies geschieht via Add Reference aus dem Kontextmenü des References Projektitems des TestEuro Projekts. Gehen Sie auf den Projects Tab, selektieren Euro aus der oberen Listbox, klicken Select und dann OK.
Damit kompiliert unser Testprojekt. Erster Schritt in "Test a Little, Code a Little" bestanden.
Gehen wir weiter. Vervollständigen wir die Methode TestGetEuroAmount:
public class TestEuro { public void TestGetEuroAmount() { Euro euro = new Euro(); Assertion.AssertEquals(euro.GetEuroAmount(1.955f), 1f); } }
Unser Test ist, daß eine Umrechnung von 1.955 DM einen Euro ergeben muß. Ist dies nicht der Fall, wird von #unit ein Fehler registriert. Apropos #unit: dessen Namespace und Assembly müssen wir noch referenzieren. Letzteres geht über Add Reference, diesmal aber kein Projekt, sondern eine Assembly aus dem GAC:
Und nun fehlt uns nur noch das using Statement am Anfang der EuroTest.cs:
using ICSharpCode.SharpUnit;
Wenn man jetzt kompiliert, bekommt man eine Fehlermeldung aus dem "Test a Little, Code a Little" Ansatz:
c:\UnitTesting\TestEuro\TestEuro.cs(6,26): error CS0117: 'Euro' does not contain a definition for 'GetEuroAmount'
Dieser Fehler war erwartet - unser Test referenziert eine Methode, die es noch nicht gibt. Bauen wir sie in Euro.cs ein:
public class Euro { public double GetEuroAmount(double dm) { return 0f; } }
Zuerst das Euro Projekt builden, danach das EuroTest Projekt - keine Fehler mehr. Nun kann man Run Tests auswählen:
#unit startet mit der TestEuro.dll Assembly, nur hat die Sache einen Haken - wir haben noch keine Tests definiert (nur implementiert). Es fehlen uns die notwendigen Testattribute in TestEuro.cs:
using ICSharpCode.SharpUnit; [TestSuite("Euro Test Suite")] public class TestEuro { [TestMethod("Testet die Funktion auf korrekte Umrechnung")] public void TestGetEuroAmount() { Euro euro = new Euro(); Assertion.AssertEquals(euro.GetEuroAmount(1.955f), 1f); } }
Es sind das TestSuite und das TestMethod Attribut dazugekommen. Das erste braucht man für die Testklasse, das zweite für jede Testmethode. Ausgelesen werden die Attribute durch #unit via Reflection.
Nach Neukompilierung und Run Tests sieht es in #unit nun freundlicher aus:
Allerdings holt uns der Run Tests Button wieder auf den Boden zurück, da es einen Fehler gibt: und der ist, daß Euro.dll nicht gefunden werden kann. Einerseits könnte man Euro.dll umkopieren ins TestEuro Verzeichnis, aber halt - ich hatte doch gesagt, daß #unit Projekte testen kann, bei denen die Dateien nicht im selben Verzeichnis sind. Der Trick sind Manifestdateien, die immer AssemblyName.testconfig benannt sein müssen, und Informationen beinhalten, von wo #unit Assemblies nachladen soll. In unserem Fall heißt die Manifestdatei TestEuro.dll.testconfig, und ihr Inhalt sieht so aus:
<TestConfig> <Codebases> <Path location="..\Euro\" /> </Codebases> </TestConfig>
Damit werden immer die letztaktuellen Assemblies aus dem Verzeichnis ..\Euro nachgeladen, in unserem Fall nur Euro.dll. Nun bekommen wir folgenden Fehler:
Ein roter Balken ist schlecht - wir wollen einen grünen (fehlerfrei). Der Grund ist: "Test a Little, Code a Little". Wir haben ein return 0 bei GetEuroAmount implementiert, um zu sehen ob die Funktion korrekt aufgerufen wird. Die korrekte Implementierung fehlt uns noch (Euro.cs):
public double GetEuroAmount(double dm) { return (dm / 1.955f); }
Das Euro Projekt neu kompiliert, zurück nach #unit und Run Tests erneut geklickt:
Ja was ist denn da falsch? Nun, unser Unit Test ist es! Mathematische Rechenergebnisse sollte man immer mit einem Delta vergleichen, niemals direkt. Was lernen wir daraus: Unit Tests sind auch Code, der sorgfältig geschrieben werden muß, weil sonst der ganze Unit Test für umsonst ist. Korrigiert sieht EuroTest.cs so aus:
using ICSharpCode.SharpUnit; [TestSuite("Euro Test Suite")] public class TestEuro { [TestMethod("Testet die Funktion auf korrekte Umrechnung")] public void TestGetEuroAmount() { Euro euro = new Euro(); Assertion.AssertEquals(euro.GetEuroAmount(1.955f), 1f, 0.001f); } }
Und nun wird auch der Balken grün:
Nach dem kleinen Umweg über den Hinweis daß sich auch Bugs in Unit Tests einschleichen können, sind wir nun am Ende des Kochrezepts angelangt: weitere Methoden und ihre Testmethoden werden nach dem gleichen Schema eingebaut - "Test a Little, Code a Little".
Zum Abschluß noch eine Liste der Methoden, die von der Assertion Klasse angeboten werden. Diese sind immer das Herzstück Ihrer Testmethoden, denn sie teilen #unit einen Fehler mit:
public static void Assert(string message, bool condition) public static void Assert(bool condition) public static void AssertEquals(string message, double expected, double current, double delta) public static void AssertEquals(double expected, double current, double delta) public static void AssertEquals(string message, float expected, float current, float delta) public static void AssertEquals(float expected, float current, float delta) public static void AssertEquals(string message, object expected, object current) public static void AssertEquals(object expected, object current) public static void AssertNotNull(string message, object o); public static void AssertNotNull(object o) public static void AssertNull(string message, object o) public static void AssertNull(object o) public static void AssertSame(string message, object expected, object current) public static void AssertSame(object expected, object current) public static void Fail() public static void Fail(string message)
Als Schlußbemerkung möchte ich Ihnen folgendes auf den Weg mitgeben: Unit Testing ist ungewohnt. Nehmen Sie sich Zeit. Es zahlt sich aus, spätestens bei der Integration eines Projekts, automatisiertem Testen oder Bug Fixing. Und schreiben Sie Ihre Tests umsichtig.
This printed page brought to you by AlphaSierraPapa
Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20020619.zip
#unit (Teil von #develop)
http://www.icsharpcode.net/opensource/sd/download/
Junit
http://www.junit.org
Nunit
http://nunit.sourceforge.net
©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.