NUnit - Testgetriebene Entwicklung unter .NET
Transcription
NUnit - Testgetriebene Entwicklung unter .NET
NUnit - Testgetriebene Entwicklung unter .NET Martin Blersch Dr.-Ing. Marc Schanne Forschungszentrum Informatik Universität Karlsruhe Email: [email protected] Forschungszentrum Informatik Universität Karlsruhe Email: [email protected] Zusammenfassung—NUnit ist ein Rahmenwerk für die Erstellung von Komponenten- und Modultests (Unit-Tests) für die .NET-Plattform. Es unterstützt den Entwickler bei der testgetriebenen Entwicklung und trägt zu einer Steigerung der Codequalität bei, indem Fehler im Quellcode schnell erkannt werden können. Dies wiederum hilft Kosten zu sparen, die bei einer nachträglichen Fehlersuche anfallen würden. Wir versuchen die Vorteile durch den Einsatz von NUnit, sowie dessen Grenzen zu zeigen. Die wichtigsten Behauptungen (Asserts) werden vorgestellt und anhand eines Beispiels erklärt. I. S OFTWAREENTWICKLUNG VS . S OFTWARETESTS Häufig wird in der Softwareentwicklung auf ausgiebige Softwaretests aus Zeit- und Geldgründen verzichtet. Tests sind jedoch wichtig, denn sie steigern die Qualität und die Zuverlässigkeit eines Softwareprodukts. Daher nimmt das Testen in großen Softwareprojekten einen ähnlich hohen Stellenwert ein wie das Programmieren selbst und verursacht mittlerweile bis zu 50% der Kosten des kompletten Softwareentwicklungsprozesses. Durch die immer höheren Anforderungen an die Softwarequalität wird diese Rate vermutlich noch einmal steigen. Softwaretests werden oft als lästig empfunden. In der Regel ist die Zeit bei Projekten sowieso sehr knapp bemessen und durch die Entwicklung von guten Tests verlieren die Programmierer nochmals Zeit, in der sie nicht an der Funktionalität der Software arbeiten können. Vielmehr müssen sie Testfälle definieren, implementieren und später auswerten. II. WARUM ALSO T ESTS ? Mit steigendem Fortschritt und Komplexität eines Projekts steigen auch die Kosten für die Fehlerbeseitigung exponentiell. Das frühe Erkennen von Fehlern in Klassen, Methoden und Komponenten durch Softwaretests zahlt sich somit aus und die Qualität der Software nimmt zu. Tests sichern ein Mindestmaß an Leistung und Zuverlässigkeit und sind aus der Softwareentwicklung nicht mehr wegzudenken. Gesucht wird eine kostengünstige Testmethode die möglichst viele Fehler aufdeckt, leicht zu implementieren ist und die automatisiert werden kann, um auch nach Modifikationen von bereits getesteten Teilen der Software erneut benutzt werden zu können (Regressionstests). Dies ist gerade bei Software die häufig geändert werden muss wichtig, jedoch auch bei der Pflege, Weiterentwicklung oder Fehlerkorrektur eines Softwareprodukts. Studien haben gezeigt, dass Komponenten- und Modultests (Unit Tests) im Vergleich zu anderen Testmethoden das beste Kosten-Nutzen-Verhältnis haben. Trotz höherem initialen Aufwand für die Komponentenerstellung erreicht man dadurch geringere Folgekosten bei der späteren Fehlersuche und Beseitigung. Unit Tests sind der effektivste Weg um möglichst viele Fehler im Code aufzudecken. Sie bieten eine Alternative gegenüber Debugging und Standardausgaben, die, gerade bei komplexen Softwareelementen oder bei Schleifen, häufig zu unübersichtlichem Code führen. Bereits bei einer Schleife mit vielen Durchläufen sind die Ausgaben kaum noch überschaubar. Debugging ist gerade bei Programmen mit mehreren Threads nur umständlich möglich und erfordert vom Entwickler hohe Konzentration und Wissen über das Gesamtsystem. Deshalb ist es sehr fehleranfällig. Komponententests ersetzen keine Integrations-, System-, Last- oder Performancetests, denn das korrekte Funktionieren aller Tests bedeutet nicht, dass deren Zusammenspiel ebenfalls frei von Fehlern ist. Dies muss durch Integrationstests überprüft werden. Sie testen das korrekte Zusammenspiel von Komponenten in einem System, die voneinander abhängig sind. Dazu bedient man sich einer Reihe aufeinander abgestimmter Einzeltests. Komponententests kann man auch als Vorstufe von Integrationstests bezeichnen. Zur Sicherstellung des spezifikationsgemäßen Funktionierens des Gesamtsystems sind Tests auf Systemebene nötig. Neben funktionalen Qualitätsmerkmalen wie Korrektkeit und Vollständigkeit überprüfen Systemtests ebenfalls nichtfunktionale Eigenschaften. Last- und Performancetests sind Softwaretests die das Verhalten eines laufenden Systems unter Last beobachten und bewerten. III. KOMPONENTENTESTS MIT NU NIT NUnit ist ein Rahmenwerk zum Schreiben und Ausführen von automatischen Unit Tests für .NET Sprachen. Es bietet eine grafische Benutzerschnittstelle und ist vollständig in C# geschrieben. Anfänglich wurde das Testrahmenwerk durch Philip A. Craig von JUnit portiert und von den Entwicklern Jim Newkirk, Michael Two, Alexei Vorontsov und Charlie Poole weiterentwickelt sowie an die .NET-eigenen Fähigkeiten angepasst. NUnit ist Open Source, steht unter der zlib/libpng Lizenz1 und darf daher auf die eigenen Bedürfnisse angepasst 1 http://www.opensource.org/licenses/zlib-license.html und verändert werden. Die Nutzung ist zudem kostenlos. Eine ausführliche Dokumentation sowie einen Downloadlink zur aktuellen NUnit- Version findet man auf der Herstellerseite2 . IV. VORGEHENSWEISE Parallel zur Programmierung, oder bereits vor dem Implementieren der eigentlichen Klasse oder Funktion, werden passende Unit Tests entwickelt und implementiert. Dies bietet dem Entwickler die Möglichkeit seinen geschriebenen Code umgehend überprüfen und gegebenenfalls ändern zu können. NUnit ist ein Rahmenwerk, das Testerstellung, sowie Zusammenfassen und Ausführen verschiedener Tests unterstützt. Es bietet eine grafische Benutzeroberfläche (GUI), die übersichtlich die fehlgeschlagenen und korrekten Tests anzeigt. Die Tests werden in einer eigenen Testklasse ausgelagert und lassen den Programmcode des Softwareprodukts unberührt. Der Entwickler kann Behauptungen (Asserts) in die Testobjekte einbauen. Treffen diese Behauptungen nicht zu, schlägt der Test fehl. Unit Tests sind White-Box-Tests, das heißt eine Kenntnis über die innere Funktionsweise des zu testenden Systems wird vorausgesetzt. Der Entwickler kann somit testen ob alle internen Operationen der vorgegebenen Spezifikation entsprechen. NUnit unterstützt ihn bei der Testerstellung, Testdatengewinnung, Testtreiber- und StubErstellung, Testausführung und Testauswertung durch Automatisierung. Das Fenster ist, wie in Abbildung 1 dargestellt, in zwei Bereiche aufgeteilt. Die linke Spalte zeigt die ausgewählte Testklasse sowie die darin enthaltenen Tests in einer Baumstruktur. Werden die Tests ausgeführt zeigt ein grüner, gelber oder roter Punkt vor den Tests sowie vor der Testklasse das Ergebnis an. Eine erfolgreiche Behauptung wird durch einen grünen Punkt symbolisiert. Nicht ausgeführte Tests erscheinen gelb, Fehlgeschlagene rot. In derselben Farbe ist in der rechten Spalte ein Balken zu sehen, der das Ergebnis noch einmal verdeutlicht. Darunter werden in einem Fenster die eventuell vorhandenen Fehler aufgelistet. Es zeigt den Namen des fehlgeschlagenen Tests sowie das erwartete Ergebnis der Behauptung (Assertion) und das tatsächliche Ergebnis an. Dies ermöglicht eine schnelle Fehlersuche und -beseitigung. Ein neu zur Testklasse hinzugefügter Unit Test taucht als weiterer Ast in der linken Spalte der grafischen Oberfläche auf. V. T ESTGETRIEBENES E NTWICKELN MIT U NIT-T ESTS Der Einsatz von Unit Tests ist ein wesentlicher Bestandteil in der agilen Softwareentwicklung. Dabei bemüht man sich schnell auf Änderungswünsche des Kunden reagieren zu können, was einen hohen Grad an Flexibilität voraussetzt. Der Code muss ständig neu geschrieben und angepasst werden. Automatisierte Unit Tests sollen sicherstellen, dass das Programm nach einer Änderung noch funktioniert. Agile Prozesse wie Extreme Programming“ verlangen je” weils mindestens einen Test pro Modul bzw. Komponente [1]. Dabei sollte keine Methode ungetestet bleiben. Dies ermöglicht bei einem Fehler im Programm eine genaue Lokalisierung. Er kann schnell entdeckt und behoben werden. Testfälle werden noch vor der eigentlichen Funktionalität implementiert und zwingen so den Entwickler sich früh mit seinem Code-Design auseinander zu setzen. Komponententests in der testgetriebenen Entwicklung sind Grey-Box-Tests, das heißt eine Mischung aus White-Box-Tests und Black-BoxTests (nur die Spezifikation bekannt, nicht derern Implementierung). Eine Mischung dieser beiden Arten bietet die beste Testfallabdeckung, da so die Vorteile beider Arten kombiniert werden kann. Eine stetige Wiederholung aller Tests (Regressionstests) führt zu einer erheblichen Qualitätssteigerung [2]. VI. S TRUKTUR DER U NIT-T ESTS NUnit besitzt eine grafische Benutzeroberfläche und eine Konsole, die dieselbe Funktionalität bietet. Sie geben Auskunft über erfolgreiche oder fehlgeschlagene Tests. 2 http://www.nunit.org/ Abbildung 1. Grafische Benutzeroberfläche von NUnit Neben der grafischen Oberfläche bietet NUnit eine Konsolenversion. Die Konsole speichert die Ergebnisse in einer XML-Datei. Diese Datei kann wiederum mit geeigneten Programmen weiterverarbeitet werden. Man startet die Überprüfung einer Datei durch den Befehl nunit-console <Dateiname>, wobei der Dateiname eine .dll, .csproj oder .nunit (NUnit-Test-) Datei sein kann. Weitere Befehle sind auf der Webseite http://www.nunit.org/ zu finden. VII. AUFBAU EINER T ESTKLASSE Testfälle werden über eigene Benutzerattribute (Custom Attributes) im .NET Quellcode definiert und mittels Reflection ausgelesen. A. Beispielklasse namespace NUnitTestingExamples { using System; using NUnit.Framework; [TestFixture] public class SuccessTests { [SetUp] public void Init() { /* ... */ } [TearDown] public void Dispose() { /* ... */ } [Test] public void Test1() { /* ... */ } [Test] [ExpectedException(typeof(XYZ)] public void Test2() { // Something that throws an // XYZ Exception } [Ignore("skip this test")] public void IgnoredTest() { /* ... */ } } A SSERT.NAME D ER B EHAUPTUNG ( OBJECT EXPECTED , OBJECT ACTUAL ) Gleichheitsbehauptungen (Equality Asserts) AreEqual, AreNotEqual Identitätsbehauptungen (Identity Asserts) Are(Not)Same, Contains Vergleichsbehauptungen (Comparison Asserts) Greater, Less, GreaterOrEqual, LessOrEqual Typ Behauptungen (Type Asserts) Is(Not)InstanceOfType, Is(Not)AssignableFrom Bedingungsbehauptungen (Condition Tests) IsTrue, IsFalse, Is(Not)Null, IsNaN, Is(Not)Empty Gebrauchsmethoden (Utility Methods) Fail(), Ignore() S TRING A SSERT.NAME D ER B EHAUPTUNG ( STRING EXP, STRING ACTUAL ) String Asserts C OLLECTIONA SSERT.NAME D ER B EHAUPTUNG ( ARG1 [, arg2 ]) } CollectionAsserts B. Erklärung • • • • • • • Contains, StartsWith, EndsWith, AreEqualIgnoringCase using NUnit.Framework bindet das NUnit Framework ein. [TestFixture] ist eine Kollektion von Unit Tests. Dieses Attribut muss vor der Klassendeklaration eingefügt werden, um anzugeben, dass die Klasse Tests enthält. [SetUp] ist ein Initialisierungsabschnitt mit Code, der vor den eigentlichen Unit Tests ausgeführt wird. [TearDown] ist ein Codeabschnitt, der im Anschluss an alle Tests ausgeführt wird. [Test] ist ein einzelner Komponententest. Dieser wird vor einer Methode eingefügt, um anzuzeigen, dass es sich um einen Test handelt. [ExpectedException] enthält eine Ausnahme, welche von der Methode zurückgeben werden kann. [Ignore] kann vor einem Unit Test angegeben werden, um diesen zu deaktivieren. NUnit ignoriert alle Tests mit diesem Attribut. Eine Begründung kann mit angegeben werden. VIII. B EHAUPTUNGEN (A SSERTS ) Mit Hilfe von Behauptungen kann der Entwickler überprüfen, ob ein Programm oder Programmabschnitt die vorgegebenen Spezifikationen erfüllt. Sie werden in die zugehörige Testklasse geschrieben und sollen das Verhalten der Methoden bei bestimmten Eingaben oder Parametern überprüfen. Behauptungen sind nach folgendem Schema aufgebaut: Assert.NameDerBehauptung(argument1, argument2, ... ). Sie sind meist mehrfach überladen und existieren mit unterschiedlichen Parameterlisten. Eine genaue Beschreibung der einzelnen Befehle findet man auf der Projektseite von NUnit [3]. Eine kurze Übersicht über die verschiedenen Befehle befindet sich in der Tabelle I. AllItemsAreInstancesOfType, AllItemsAreNotNull, AllItemsAreUnique, AreEqual, AreEquivalent, AreNotEqual, AreNotEquivalent, Contains, DoesNotContain, IsSubsetOf, IsNotSubsetOf, IsEmpty, IsNotEmpty Tabelle I Ü BERSICHT - B EHAUPTUNGEN (A SSERTS ) Beispiel: Assert.AreEqual(object expected, object actual). Neu sind bei NUnit ab Version 2.4 die Behauptungen für Zeichenketten (StringAsserts) und Sammlungen (CollectionAsserts). Ihre Schreibweise weicht von den normalen Asserts ab. Beispiel: StringAssert.Contains("Das Wetter ist schön", "schön") überprüft die Zeichenkette Das ” Wetter ist schön“ auf die Behauptung sie beinhalte (contains) schön“. ” IX. B EISPIEL Als Beispiel soll die Klasse Person dienen. Ein Objekt dieser Klasse besitzt einen Namen, einen Arbeitsbereich und ein bestimmtes Gehalt. Ferner kann eine Person Angestellte unter bzw. Vorgesetzte über sich haben. Diese wiederum sind ebenfalls vom Typ Person und können durch die Methoden setEmployee() und setChief() hinzugefügt werden. Der Einfachheit halber ist nur eine begrenzte Anzahl an Untergebenen (MAX EMPLOYEES) und Vorgesetzten (MAX CHIEFS) möglich. namespace TestProgram public class Person { private string name; private string department; private double salary; private Person[] chiefs; private Person[] employees; const int MAX_EMPLOYEES = 10; const int MAX_CHIEFS = 10; public Person(string name) { department = ""; salary = 0; chiefs = new Person[MAX_CHIEFS]; employees = new Person[MAX_EMPLOYEES]; } public void setChief(Person chief) { if (countChiefs() < MAX_CHIEFS) chiefs[countChiefs()] = chief; } public void setEmployee(Person employee) { if (countEmployees() < MAX_EMPLOYEES) employees[countEmployees()] = employee; } public int countEmployees() { // return amount of employees in list } public int countChiefs() { // return amount of chiefs in list } } Die zugehörige Testdatei muss den selben Namensraum wie die Klasse Person besitzen und das NUnit Rahmenwerk durch die using-Direktive importieren. Die Klasse PersonTest, die die Tests der Klasse Person enthält, muss durch das Benutzerattribut (Custom Attribute) [TestFixture] gekennzeichnet sein. using System; using System.Collections.Generic; using NUnit.Framework; namespace TestProgram { [TestFixture] public class PersonTest { Person person; [SetUp] public void Init() { person = new Person("John"); person.Salary = 5000; person.Department = "Accountancy"; } } } Die Methode Init(), die mit dem [SetUp] Attribut gekennzeichnet ist, erstellt eine Instanz der Klasse Person und weist ihr einen Namen, ein Gehalt sowie einen Arbeitsbereich zu. Wegen des Attributs [SetUp] wird diese Methode vor allen anderen Methoden der Testklasse PersonTest ausgeführt und bietet somit den anderen Testmethoden ein konkretes Objekt für deren Tests. Das folgende Beispiel zeigt einen einfachen Test, gekennzeichnet durch das [Test] Attribut. Er enthält drei Behauptungen (Asserts), die für einen erfolgreichen Ablauf der Testdatei allesamt korrekt sein müssen. Die Behauptung AreEqual erachtet zwei Zeichenketten für gleich, wenn sie denselben Inhalt besitzen. AreSame und AreNotSame prüfen zusätzlich ob es sich dabei um das identische bzw. nicht identische Objekt handelt. [Test] public void NameToStringTest() { Assert.AreEqual(person.ToString(), "John"); Person person2 = new Person("John"); Assert.AreNotSame(person, person2); Assert.AreEqual(person.ToString(), person2.ToString()); } Die Klasse Person enthält zwei Funktionen getChiefs(int pos) und getEmployees(int pos), die den Vorgesetzten bzw. den Untergebenen der jeweiligen Person an der Indexstelle pos zurück liefert. Sie können eine IndexOutOfRange Ausnahme auslösen, falls versucht wird auf einen Index zuzugreifen, der größer ist als die Werte der Konstanten MAX EMPLOYEES und MAX CHIEFS. Die Ausnahme wird in diesem Beispiel geworfen und nicht abgefangen. // throws IndexOutOfRangeException public Person getEmployee(int pos) { try { return employees[pos]; } catch (IndexOutOfRangeException ex) { throw new IndexOutOfRangeException( "An Error occured: " + ex); } } Die dazugehörige Testmethode GetEmployeeExceptionTest soll genau diesen Fall testen und erwartet sogar diese Ausnahme. Dies wird durch das zusätzliche Attribut [ExpectedException], sowie dem erwarteten Ausnahmentyp als Parameter gekennzeichnet (typeof(IndexOutOfRangeException)). [Test] [ExpectedException(typeof (IndexOutOfRangeException))] public void GetEmployeeExceptionTest() { // throws IndexOutOfRangeException Assert.AreEqual(person.getEmployee(100) .ToString(), "Julian"); } X. VORTEILE DURCH DEN E INSATZ VON NU NIT • • • • Tests können beliebig oft ausgeführt und wiederholt werden (reproduzierbar). Mit dem Einsatz eines Unit Test-Tools wird eine höhere Codequalität in kürzerer Zeit und mit weniger Entwicklungskosten erreicht. Automatisierbar Reduziert die Komplexität der Software durch das Zerlegen der Software in kleinere Module und Komponenten. XI. G RENZEN VON NU NIT • • • • • Es ist kein Testen von private Membern möglich, da die TestCase Klasse, von der alle Tests in NUnit ableiten müssen in einer separaten Testklasse liegt. Die Tests müssen als Code in eine Datei geschrieben werden. Nur ein Programmierer kann die Testsuite modifizieren. Mit NUnit ist nur die Geschäftslogik testbar. Für GUIElemente existieren jedoch spezielle Rahmenwerke, unter anderem NUnitASP [4] und NUnitForms [5] Die Testprogramme sind unter Umständen sehr aufwendig. Tests können nur zeigen, dass es Fehler gibt, nicht, dass es keine gibt. XII. B EWERTUNG Obwohl NUnit keine Universallösung ist, bietet es dem Entwickler für die gängigsten Testfälle eine komfortable Suite. Sie unterstützt ihn gerade bei der testgetriebenen Entwicklung durch das einfache Erstellen von Testfällen in einer separaten Datei. Durch die Test-First Strategie, also das Schreiben des Tests vor der eigentlichen Implementierung der Komponente, ist der Entwickler gezwungen, sich frühzeitig mit dem Design des Codes auseinander zu setzen. Dies erhöht die Codequalität. Allerdings zeigen Unit Tests nur vorhandene Fehler auf, nicht aber deren Nichtvorhandensein. Sie bieten eine sinnvolle Erweiterung, nicht aber einen vollständigen Ersatz anderer Testmethoden wie Integrations-, System-, Last- oder Performancetests. Die durch die Testfallerstellung anfallenden Kosten sind meist günstiger als eine spätere Fehlersuche. A. Erweiterungsmöglichkeiten von NUnit NUnit kann durch Pakete erweitert werden, um auch Elemente einer grafischen Benutzeroberfläche testen zu können. Sie setzen allesamt auf NUnit auf und sind ebenfalls Open Source. • Für ASP.NET-Seiten steht NUnitASP [4] zur Verfügung. Das Rahmenwerk unterstützt die Abfrage von Webcontrols, unter Anderem von Buttons, Labels und TextBoxen. • Für Windows Forms Elemente steht die Erweiterung NUnitForms [5] zur Verfügung. B. Alternativen zu NUnit Ein weiteres Rahmenwerk ist CSUnit [6], das ähnlich ist wie NUnit. Allerdings ist dessen Umfang an Assertbefehlen etwas kleiner. Ein Tutorial sowie eine Anleitung zum Umstieg von NUnit zu CSUnit findet man auf den Herstellerseiten. Um Unit Tests fest in Visual Studio zu integrieren empfiehlt sich das kommerzielle Produkt TestDriven.NET [7] oder das freie Rahmenwerk VSNUnit [8]. In Visual Studio Team System ist ein Komponenten- und Modultestframework bereits integriert. Dies zeigt, dass auch Microsoft die Bedeutung von testgetriebener Entwicklung erkannt hat und als Verkaufsargument“ einsetzt. Voraussichtlich ” werden alle kommenden Visual Studio Versionen die testgetriebene Entwicklung unterstützen. L ITERATUR [1] K. Beck, Test-driven development : by example, 9th ed. Boston, Mass. [u.a.] : Addison-Wesley, 2006. [2] ——, Extreme Programming Explained, 2nd ed. Boston, Mass. [u.a.] : Addison-Wesley, 2006. [3] M. C. Two, C. Poole, J. Cansdale, and G. Feldman, “NUnit Rahmenwerk,” 2006. [Online]. Available: http://www.nunit.org/ [4] B. Knowles, J. Shore, and L. Khatskevitch, “NUnitASP,” 2005. [Online]. Available: http://nunitasp.sourceforge.net/ [5] L. T. Maxon, “NUnitForms,” 2004. [Online]. Available: http: //nunitforms.sourceforge.net/ [6] J. W. Anderson, P. Lawson, M. Renschler, and M. Lange, “csUnit,” 2006. [Online]. Available: http://www.csunit.org/ [7] J. Cansdale, “TestDriven.NET,” 2002-2006. [Online]. Available: http://www.testdriven.net/ [8] J. Gehtland, “VSNUnit,” 2004. [Online]. Available: http://sourceforge. net/projects/vsnunit/ [9] J. W. Newkirk and A. A. Vorontsov, Test-Driven Development in Microsoft .NET. Microsoft Press, 2004. [10] F. Westphal, “Testgetriebene Entwicklung,” 2002. [Online]. Available: http://www.frankwestphal.de/TestgetriebeneEntwicklung.html [11] B. Rumpe, “Extreme Programming - Back to Basics?” 2003. [Online]. Available: http://www4.in.tum.de/publ/papers/Rum01.pdf [12] D. Wells, “Extreme Programming,” 2006. [Online]. Available: http: //www.extremeprogramming.org/ [13] P. Provost, “Test-Driven Development in .NET,” 2003. [Online]. Available: http://www.codeproject.com/dotnet/tdd in dotnet.asp [14] W. Stott and J. Newkirk, “Test-Driven C# - Improve the Design and Flexibility of Your Project with Extreme Programming Techniques,” 2004. [Online]. Available: http://msdn.microsoft.com/msdnmag/issues/ 04/04/ExtremeProgramming/default.aspx [15] A. Hunt and D. Thomas, Pragmatic Unit Testing in C# with NUnit. The Pragmatic Bookshelf, Raleigh, 2004. [16] B. Hamilton, NUnit Pocket Reference. O’Reilly, Cambridge, 2004.