Grundüberlegungen von Unit Tests
Sicherlich haben die meisten Programmierer schon etwas von Unit Testing gehört und wissen grob, um was es dabei geht. Die Erfahrung zeigt jedoch, dass dieser wichtige Teil der Softwareentwicklung oft nicht eingesetzt wird, weil er mit Vorurteilen behaftet ist – einerseits die Berührungsangst mit dem „Neuen“, andererseits die Meinung, die Erstellung solcher Tests sei mit engen Zeitplänen nicht vereinbar. Für Letzteres gibt es aber stichhaltige Gegenargumente.
Nehmen wir an, Sie erstellen eine Anwendung ohne Unit Tests und sparen dadurch zunächst Zeit. Im Lebenszyklus jeder Anwendung kommt es zu Änderungen – um sicherzustellen, dass die Applikation danach noch das gewünschte Verhalten aufweist, ist ein vollständiger manueller Test nötig. Je nach Komplexität kostet das viel Zeit, und unter Termindruck wird er oft nicht gründlich genug durchgeführt. So entstehen früher oder später vermeidbare Softwarefehler.
Zugegeben: Während der Entwicklung ohne Unit Tests spart man zunächst Zeit. Denkt man aber die Wartung mit ein, zahlt sich der Mehraufwand für Unit Tests bei langfristig eingesetzter, stetig erweiterter Software definitiv aus. Bei durchdachten Tests lassen sich sogar schon während der Entwicklung Fehler und Folgefehler vermeiden. Ein bekanntes Verfahren dazu ist Test Driven Development (TDD), bei dem Testroutinen vor der eigentlichen Funktionalität geschrieben werden.
Unit Tests – auf Deutsch „Komponententests“ – dienen zur maschinellen Verifizierung von Softwarekomponenten: eigene Funktionen prüfen, ob Klassenfunktionen korrekt arbeiten. Vor allem bei Änderungen erhält man so sofort Rückmeldung, ob der Code noch das gewünschte Verhalten aufweist. Ein verbreitetes, kostenloses und leicht zu verwendendes Werkzeug dafür ist NUnit.
NUnit Referenz
Attribute
Attribute machen Klassen und Methoden NUnit bekannt und erlauben diverse Einstellungen.
| Befehl | Beschreibung |
|---|---|
[TestFixture] |
Kennzeichnet eine Klasse als testbar. Diese Klassen müssen public sein und einen Standardkonstruktor besitzen. |
[Test] |
Kennzeichnet eine Funktion als Testfunktion. Diese müssen parameterlos und public sein. |
[SetUp] |
Initialisierungscode, der vor jedem Test ausgeführt wird – etwa zum Anlegen von Testdaten. |
[TearDown] |
Wird nach jedem Test ausgeführt, z. B. zum Aufräumen der Testdaten. |
[Ignore] |
Nimmt eine Test-Klasse oder -Methode temporär aus dem Testdurchlauf; wird in NUnit gelb markiert. |
[ExpectedException] |
Prüft, ob eine Testmethode die erwartete Exception wirft. Andernfalls gilt der Test als nicht bestanden. |
[TestFixture]
public class DataLayerSQLTest
{
private DataLayerSQL _data;
private BankAccount _account;
}
Assertions
Assertions prüfen, ob das Ergebnis einer Testroutine richtig oder falsch ist. Wird eine Assertion
ausgelöst, ist der Test fehlgeschlagen und erscheint in NUnit rot. Die Klasse
NUnit.Framework.Assert bietet dafür viele Methoden, unter anderem:
Assert.AreEqual– prüft, ob zwei Objekte identisch sindAssert.AreNotEqual– prüft, ob zwei Objekte ungleich sindAssert.Fail– löst einen Fehlschlag ohne weitere Prüfung ausAssert.IsFalse/Assert.IsNotNullAssert.IsInstanceOfType– prüft den Typ eines Objekts
Assert.AreEqual(account.Amount, 10.08); Assert.IsNotNull(getAccount, "object could not be found");
Praxis-Beispiel
Am Beispielprojekt „MBBankSample“ lässt sich der Einstieg gut nachvollziehen: Für eine wartbare, langlebige Anwendung bietet sich eine klassische 3-Schichten-Architektur an, deren Präsentationsschicht zusätzlich nach dem MVP-Pattern unterteilt ist. Die Schichten liegen in separaten Assemblies, was eine sauberere Abstraktion ermöglicht.
Für die Tests wird ein eigenes Projekt „UnitTest“ angelegt, das per Referenz Zugriff auf die zu
testende Anwendung sowie auf nunit.framework erhält. Als Ergebnis entsteht eine
DLL-Datei, die sich mit NUnit.exe laden und ausführen lässt.
Testen der Datenzugriffsschicht
Für die Klasse MBBankSample.DA.SQL.DataLayerSQL wird ein Ordner
„NUnitTest.DA.DataLayerSQL“ mit der Testklasse DataLayerSQLTest angelegt, versehen mit
dem Attribut [TestFixture]. Getestet werden alle öffentlichen Methoden des Interfaces
IDataLayer:
BankAccount GetAccount(string accountNumber); void UpdateAccont(BankAccount account); void CreateAccount(BankAccount account); void DeleteAccount(BankAccount account);
Für jede Methode empfiehlt sich mindestens ein Test für den regulären Funktionsumfang, einer mit ungültigen Parametern und ein Szenario, das einen logischen Fehler provoziert. Zwischen dem Erstellen einer Methode und dem dazugehörigen Test sollte möglichst nicht mehr als ein Tag vergehen, solange der Ablauf noch frisch im Gedächtnis ist:
[Test]
public void CreateNewAccount()
{
BankAccount account = new BankAccount();
account.AccountNumber = "11223344";
account.Amount = 100.01;
account.BankCode = "187654321";
account.CustomerNumber = "123B";
account.FirstName = "UnitTestFirstname2";
account.SurName = "UnitTestSurname2";
account.MinimalAmount = 10.99;
try
{
_data.CreateAccount(account);
BankAccount getAccount = _data.GetAccount(account.AccountNumber);
Assert.IsNotNull(getAccount, "New created bank object could not be found");
Assert.IsTrue(account.Equals(getAccount), "Objects aren't equal");
}
finally
{
_data.DeleteAccount(account);
}
}
[Test]
public void CreateNewAccountWIP()
{
try
{
_data.CreateAccount(null);
Assert.Fail("Create Account accept a null argument");
}
catch (Exception exp)
{
Assert.IsTrue(exp is ArgumentNullException, exp.GetType().ToString());
}
}
[Test]
[ExpectedException(typeof(DAException))]
public void CreateDoubleAccount()
{
BankAccount account = new BankAccount { AccountNumber = "11223344", Amount = 100.01 };
try
{
_data.CreateAccount(account);
_data.CreateAccount(account);
}
finally
{
_data.DeleteAccount(account);
}
}
CreateNewAccount prüft die normale Funktionsweise, CreateNewAccountWIP
übergibt ungültige Parameter, und CreateDoubleAccount provoziert einen logischen
Fehler durch das zweimalige Anlegen desselben Kontos.
Testen mit NUnit
Nach dem Kompilieren wird die entstandene DLL in NUnit.exe als Projekt geladen. Ein Klick auf
„Run“ führt alle mit [Test] markierten Methoden aus und wertet sie aus – schlägt eine
fehl, erscheint der Eintrag rot. So lässt sich nach jeder Änderung die Funktionsweise per Klick
überprüfen.
Hinweis: Die im Originalartikel verlinkten Beispieldateien (MBBankSample-Projekt, PDF) sind nicht mehr verfügbar. Bei Interesse am vollständigen Quellcode gerne über die Kontaktseite melden.