Software-Bugs sind allgegenwärtig: Von der Ariane-Explosion (1996) über den Mars Climate Orbiter (1999, 125 Millionen US-Dollar) bis zu den 63.000 Bugs in den 10 Millionen Codezeilen von Windows 2000. Im Jahre 1999 wurde bekannt, dass 1983 ein Bug in einem Programm, das falsche Raketenalarme auf Grund von Wolkenreflektionen filtern sollte, fast einen Nuklearkrieg ausgelöst hätte. Intel gab an, es seien nicht mehr als 80 bis 90 Fehler im Pentium-Prozessor. Durchschnittliche Programme besitzen 25 Fehler und gute zwei pro 1000 Zeilen. Kritische Systeme wie das Space Shuttle besitzen weniger als einen Fehler pro 10000 Zeilen [Bugs].
Um die Qualität ihrer Software zu verbessern, sollten Programmierer schon während der Entwicklungsphase mit einem Testing Framework ihren Code ständig überprüfen. Dagegen wird oft eingewendet: Tests schreiben dauert zu lange, ich komme nicht weiter. Berücksichtigt man allerdings, dass bei konsequenter und ständiger Kontrolle des Codes die Fehler sofort nach ihrer Entstehung gefunden werden, sieht die Sache schon wieder anders aus: Nächtelange Debug-Sessions fallen weg.
Test-First-Programmierung und Unit-Tests
Wir unterscheiden zwei Arten von Tests: lokale Unit-Tests, die eine einzelne Klasse oder Methode kontrollieren, und globale Akzeptanz-Tests, mit denen ein von außen sichtbares Verhalten überprüft wird. Letztere werden durch den Nutzer des Systems vorgegeben, erstere sollten vom Programmierer bzw. seinem Partner selbst erstellt werden, am besten, bevor der zugehörige Code geschrieben wird. So ist sichergestellt, dass die Tests auch vorhanden sind, wenn sie gebraucht werden: um dem Programmierer zu sagen, wann das aktuelle Problem gelöst ist.Außerdem entsteht das Sicherheitsnetz der Unit-Tests nur dann, wenn wirklich jede benötigte Funktion durch mindestens einen Test kontrolliert wird, denn wenn sich erst einmal ungetestete Funktionen im Code eingenistet haben, spiegeln die Tests nur noch eine trügerische Sicherheit wider.
Die Implementierung einer neuen Funktion läuft idealerweise so ab:
- Zwei Programmierer, A und B, holen sich den aktuellen Stand des Systems aus ihrem Versionsmanagement (CVS). Sie lassen die Testsuite laufen und stellen fest, dass alles im grünen Bereich ist.
- A und B entwerfen und implementieren einen Test für die neue Funktion und lassen die Testsuite laufen, die prompt Fehler anzeigt. Aber jetzt wissen die beiden, dass der Test auch wirklich aufgerufen wird. Je nach Sprache muss hierfür auch die neue Funktion zumindest dem Namen nach schon existieren.
- Nun wird die neue Funktion so weit aufgebaut, dass genau dieser Testfall korrekt bearbeitet werden kann.
- So entstehen abwechselnd immer mehr Tests und Produktionscode, bis alle externen Anforderungen an die Funktion erfüllt sind. Wenn dafür Änderungen an vorhandenen Programmteilen notwendig sind, werden sich einschleichende Fehler beim nächsten Testlauf, also jetzt gleich, von den Unit-Tests entdeckt. Da die Änderungen noch frisch im Gedächtnis sind, können die Fehler schnell behoben werden.
- Sind alle Tests im grünen Bereich und die angeforderten Fähigkeiten vollständig implementiert, werden die Quellcodes in das Versionsmanagement zurückgestellt und A und B widmen sich neuen Aufgaben.
(Perl-)Skript einen SMTP-Server simulieren [SMTP], der das Testsystem von einer echten Netzwerkverbindung und deren Unwägbarkeiten unabhängig macht. Ebenso können Anbindungen an Datenbanken oder das Internet simuliert werden, wenn sie nicht eigentlicher Gegenstand des Tests sind.
NUnit: Installation und Test
JUnit [JUnit] ist ein beliebtes Regression-Testing-Framework, das von Erich Gamma und Kent Beck entwickelt wurde und ein Open-Source-Projekt unter der IBM Public License ist. NUnit [NUnit] ist das .NET-Gegenstück von Philip Craig und vielen Helfern. Die Lizenz ist noch freier als die von IBM, aber da NUnit Dateien von JUnit benutzt, wird man sich wohl auf die Bedingungen der IBM-Lizenz einschränken müssen. Während diese Zeilen geschrieben werden, wird auf der XP-Mailingliste gerade NUnit 2.0 Beta 2 angekündigt.
Beispiele
Am schnellsten wird man NUnit beherrschen lernen, wenn man die Beispiele studiert:- C#: drei "failing" Unit-Tests in C# geschrieben
- VB.NET: drei failing Unit-Tests in VB.NET geschrieben. Das Beispiel zeigt, dass VB.NET keine Exception bei einer Division durch 0 abgibt, auch wenn es laut Anleitung eine Exception werfen sollte.
- Managed C++: die gleichen drei Tests. Das Verhalten im Debug Mode unterscheidet sich vom Verhalten im Release Mode.
- Visual J#: die gleichen drei Tests in J#.
- Money: das bekannte Geld-Beispiel von Kent Beck [TestInfected]. Wir werden den Code gleich aufgreifen.
- Money-port: das Beispiel zeigt, welche Veränderungen notwendig sind, wenn man mit ältereren NUnit-Versionen gearbeitet hat.
Listing 1
namespace bank{public class Account{private float balance;public void Deposit(float amount){balance+=amount;}public void Withdraw(float amount){balance-=amount;}public void TransferFunds(Account destination, float amount){}public float Balance{get{ return balance; }}}}
Test-Methoden
Nun schreiben wir dafür einen Test AccountTest. Die erste Methode, die wir testen werden, ist TransferFunds (siehe Listing 2). Durch das vererbbare Attribut [TestFixture] wird gezeigt, dass die Klasse Testcode enthält. Die Klasse selbst muss public sein und einen Default-Konstruktor haben; bezüglich der Superklasse gibt es keine Einschränkungen.Listing 2
namespace bank{using Nunit.Framework;[TestFixture]public class AccountTest{[Test]public void TransferFunds(){Account source = new Account();source.Deposit(200.00F);Account destination = new Account();destination.Deposit(150.00F);source.TransferFunds(destination, 100.00F);Assertion.AssertEquals(250.00F, destination.Balance);Assertion.AssertEquals(100.00F, source.Balance);}}}
Wenn der Code in die Datei bank.dll kompiliert ist, starten wir das NUnit-GUI und laden über File | Open die Datei bank.dll. Links wird die hierarchische Teststruktur gezeigt, rechts ist Raum für Statusmeldungen. Sobald man auf Run drückt, werden die Tests ausgeführt, wir waren hier leider nicht erfolgreich. Das Errors and Failures-Feld zeigt uns: TransferFunds : expected <250> but was <150> und im Stack Trace finden wir die Stelle, an der der Fehler ausgelöst worden ist: at bank.AccountTest.TransferFunds() in C:\nunit\BankSampleTests\AccountTest.cs:line 17.
Aber das war ja auch zu erwarten, da wir die TransferFunds-Methode noch nicht implementiert haben. Wir gehen zurück in die IDE, lassen aber das NUnit-GUI offen und geben etwa Folgendes ein:
public void TransferFunds(Account destination, float amount){destination.Deposit(amount);Withdraw(amount);}
Unsere Konten erhalten nun mit einem Mindeststand eine weitere Einschränkung:
private float minimumBalance = 10.00F;public float MinimumBalance{get{ return minimumBalance; }}
namespace bank{using System;public class InsufficientFundsException : ApplicationException{}}
[Test][ExpectedException(typeof(InsufficientFundsException))]public void TransferWithInsufficientFunds(){Account source = new Account();source.Deposit(200.00F);Account destination = new Account();destination.Deposit(150.00F);source.TransferFunds(destination, 300.00F);}
Mit dem folgenden Abschnitt korrigieren wir das Fehlverhalten:
public void TransferFunds(Account destination, float amount){destination.Deposit(amount);if(balance-amount<minimumBalance) throw new InsufficientFundsException();Withdraw(amount);}
[Test]public void TransferWithInsufficientFundsAtomicity(){Account source = new Account();source.Deposit(200.00F);Account destination = new Account();destination.Deposit(150.00F);try{source.TransferFunds(destination, 300.00F);}catch(InsufficientFundsException expected){}Assertion.AssertEquals(200.00F,source.Balance);Assertion.AssertEquals(150.00F,destination.Balance);}
public void TransferFunds(Account destination, float amount){if(balance-amount<minimumBalance) throw new InsufficientFundsException();destination.Deposit(amount);Withdraw(amount);}
[Test][Ignore("Need to decide how to implement transaction management in the application")]public void TransferWithInsufficientFundsAtomicity(){ gleicher Code wie oben }
Tests refaktorieren
So langsam sollten wir das Durcheinander in unserem Code etwas aufräumen! Alle verwendeten Test-Methoden haben ein gemeinsames Test-Objekt, dessen Initialisierung in eine Setup-Methode ausgegliedert werden kann, die von allen Tests verwendet wird (siehe Listing 3).Listing 3
namespace bank{using System;using Nunit.Framework;[TestFixture]public class AccountTest{Account source;Account destination;[SetUp]public void Init(){source = new Account();source.Deposit(200.00F);destination = new Account();destination.Deposit(150.00F);}[Test]public void TransferFunds(){source.TransferFunds(destination, 100.00f);Assertion.AssertEquals(250.00F, destination.Balance);Assertion.AssertEquals(100.00F, source.Balance);}[Test][ExpectedException(typeof(InsufficientFundsException))]public void TransferWithInsufficientFunds(){source.TransferFunds(destination, 300.00F);}[Test][Ignore("Need to decide how to implement transaction management in the application")]public void TransferWithInsufficientFundsAtomicity(){try{source.TransferFunds(destination, 300.00F);}catch(InsufficientFundsException expected){}Assertion.AssertEquals(200.00F,source.Balance);Assertion.AssertEquals(150.00F,destination.Balance);}}
Auf einen Blick
- Die Idee des Unit-Tests wurde mit dem Extremen Programmieren wiederentdeckt.
- Alle Unit-Tests müssen ständig zu 100 Prozent funktionieren.
- NUnit ist ein kompaktes Framework zum Unit-Testen mit .NET.
- Tests können nur zeigen, dass es Fehler gibt, nicht, dass es keine Fehler gibt.
Zum Schluss
Unit-Testen ist nur eine der zahlreichen Arbeitstechniken, die von agilen Entwicklungsmethodologen propagiert werden. Erst aus dem Zusammenspiel mehrerer Komponenten entsteht ein tragfähiges Ganzes, das die effiziente Erzeugung stabiler Applikationen ermöglicht. Wir empfehlen [EmbraceChange], um mehr über Extremes Programmieren zu erfahren. Dr. Armin Roehrl (armin@approximity.com) und Stefan Schmiedl (s@xss.de) leiten gemeinsam die Softwareschmiede Approximity. Seit Stefan Armin auf x-Units gebracht hat, ist jeder Tag ohne einen neuen Unit-Test ein verlorener Tag.Links und Literatur
- [JUnit] JUnit: www.junit.org/
- [NUnit] NUnit: nunit.sourceforge.net/
- [TestInfected] junit.sourceforge.net/doc/testinfected/testing.htm#
- [EmbraceChange] Kent Beck, Extreme Programming Explained: Embrace Change, Addison-Wesley, 1999
- [Bugs] Collection of Software Bugs, Prof. Thomas Huckle: wwwzenger.informatik.tu-muenchen.de/persons/huckle/bugse.html
- [SMTP] A Fake SMTP Daemon Written in Perl, John Brewer: www.jera.com/tools/fakesmtpd/


