3.4 Code organisieren
3.4.1 Testcode vs. Produktionscode
Testcode hat einen spezifischen Zweck, er soll die Qualität des Produktionscodes sicherstellen, diesen aber nicht außerhalb der eigentlichen Testumgebung beeinflussen. Deshalb sollte Testbarkeit nie der einzige Grund für eine Designentscheidung sein.
Beispielsweise könnte es als sehr einfach erscheinen, private Klassenbestandteile durch eine Änderung der Sichtbarkeit auf public öffentlich sichtbar und somit einfach zugänglich für Tests zu machen. Dies stellt jedoch einen tiefen Eingriff in das gesamte Design dar, bricht es doch die Kapselung der inneren Logik auf, was zu größeren Abhängigkeiten innerhalb des Gesamtsystems der Applikation führen kann.
Um dies zu vermeiden, sollten sich Tests also zunächst wie jeder andere Konsument einer Funktionalität verhalten und möglichst keine Voraussetzungen an den zu testenden Code stellen, die über die Schnittstellenbeschreibung hinausgehen. Vielmehr sollten sie auch physisch vom Produktionscode getrennt und in eigenen Projekten verwaltet werden. Auf diese Weise wird von vornherein eine Vermischung mit der eigentlichen Applikation vermieden und es kann nicht versehentlich geschehen, dass Testcode in die tatsächliche Produktionsumgebung übernommen wird, wo er im ungünstigsten Fall selbst zu Fehlern führt und die Wartbarkeit behindert.
3.4.2 Separierung von Tests
Neben der reinen Separierung von Test- und Produktionscode empfiehlt es sich, auch die einzelnen Testarten, wie sie in Kapitel 1.1 beschrieben wurden, voneinander zu trennen. Auf diese Weise kann wesentlich besser auf ihre Besonderheiten in Bezug auf Ausführungsgeschwindigkeit und Ressourcenbedarf eingegangen werden, was darüber hinaus noch durch die Einführung einzelner Projektmappen unterstützt wird.
Dies erlaubt beispielsweise eine Solution, die nur von den Entwicklern verwendet wird und neben dem tatsächlichen Produktionscode auch die Unit Tests enthält. Auf diese Weise können die wichtigsten Tests parallel zur Programmierung ausgeführt werden, ohne lange Wartezeit auf ihr Ergebnis. Eine weitere Projektmappe, die neben Produktionscode und Unit Tests auch Integrations- und Systemtests enthält, wird dann nur von spezifischen Personen editiert und gezielt nach jedem Check-in oder einmal am Tag in dafür vorbereiteten Testumgebungen ausgeführt. Somit kann garantiert werden, dass die Applikation in Gänze und ohne Zeitdruck geprüft wird. Darüber hinaus ist abgesichert, dass alle notwendigen Ressourcen, wie zum Beispiel Datenbanken, auch bereitstehen, wodurch falsche Ergebnisse vermieden werden, die ggf. auf einen fehlerhaften Testaufbau zurückzuführen wären.
Unit Tests organisieren
Unit Tests müssen möglichst schnell auf jedem Entwicklerrechner und hohe ohne aufwändigen Testaufbau ausführbar sein. Da sie vor allem die Funktionalität einzelner Klassen testen, können und sollten sie auch mit genau jenen Klassen verteilt werden, falls diese in andere Programme übernommen werden. Aus diesem Grund bietet es sich in größeren Applikationen an, zu jedem Projekt, das zu testende Klassen enthält, auch ein eigenes Projekt für die dazugehörigen Unit Tests bereitzustellen. Dieses kann dann einem einfachen Namensmuster folgen:
[Projektname].UnitTests
Im Beispielszenario dieses Buches existiert demnach zum Projekt Twitter.Core, das die eigentliche Geschäftslogik enthält, ein Testprojekt mit dem Namen Twitter.Core.UnitTests. Sollte die Logik der Applikation also weiterverwendet werden, kann sie ohne Weiteres mit den dazugehörigen Tests übertragen werden. Darüber hinaus verringert diese Aufteilung das Risiko eines sehr großen Testprojekts mit vielen Abhängigkeiten, das seinerseits schwer zu deployen und zu warten ist.
Systemtests organisieren
Systemtests, wie zum Beispiel Coded UI Tests, testen nicht nur einzelne Teile, sondern die gesamte Applikation einschließlich ihrer Benutzeroberfläche. Auf diese Weise haben sie häufig Abhängigkeiten zu einer Vielzahl unterschiedlicher Projekte. Wenn als beispielsweise geprüft werden soll, ob ein Tweet auf Knopfdruck zu Twitter gesandt wird, so muss nicht nur auf das Projekt mit dem Fenster der Applikation zugegriffen werden, sondern auch auf die eigentliche Verarbeitungslogik und den dazugehörigen Web Service.
Dies bedeutet, dass Systemtests immer von dem System abhängig sind, das sie testen, und daher lohnt es sich, diese gemeinsam in einem einzelnen Projekt für genau dieses System zu hinterlegen. Abgeleitet von den Unit Tests, kann hierbei das Namensmuster beispielsweise wie folgt aussehen:
[Programmname].SystemTests
Im Beispielszenario würde es daher TwitterClient.SystemTests heißen.
Integrationstests organisieren
Im Gegensatz zu Unit und Systemtests, ist der Aufbau bei Integrationstests nicht ganz eindeutig, weil diese einen sehr unterschiedlichen Komplexitätsgrad aufweisen können. So gibt es beispielsweise Integrationstests, die nur prüfen, wie die Interaktion zweier Klassen mit inkorrekten Parametern aussieht. Dies kann verhältnismäßig einfach realisiert werden, sollte aber nicht mit Unit Tests vermischt werden, falls es sich um Klassen aus unterschiedlichen Assemblies handelt.
Offensichtlicher wird es hingegen, sobald ein Test auf Datenbanken, Web Services oder das Dateisystem zugreifen muss. Dieser bezieht sich dann noch immer nicht auf das Gesamtsystem, weist aber ebenfalls noch einen sehr hohen Aufwand bei der Bereitstellung einer Testumgebung auf, weshalb er eindeutig nicht in einer Unit-Test-Bibliothek untergebracht werden sollte.
Daraus ableiten lässt sich, dass nicht nur die Trennung zwischen Unit und Systemtests sinnvoll ist, sondern auch Integrationstests einzeln berücksichtigt werden sollten. Jene mit Unit Tests zu vermischen, um beispielsweise das Übertragen der Daten an Twitter im gleichen Projekt wie die Validierung der Eingabedaten zu prüfen, mag im ersten Moment einfacher sein, führt aber dazu, dass bei jeder Ausführung auch der Web Service zur Verfügung steht, was nicht immer garantiert werden kann, und unter Umständen zu fehlgeschlagenen Tests führt. In diesen Fällen sollte dann eher auf Testisolierung zurückgegriffen werden, wie es in Kapitel 4.4.1 beschrieben wird, oder die Tests in das Projekt der Systemtests übertragen werden.
Bei größeren Integrationstests, wie zum Beispiel Last- oder Webleistungstests, bietet es sich darüber hinaus auch an, ein eigenes Projekt anzulegen, dessen Name dann eindeutig auf den Zweck der darin enthaltenen Prüfungen hinweist:
DataBase.IntegrationTests oder Webservice.IntegrationTests.
3.4.3 Tests kategorisieren
Neben der Separierung in unterschiedliche Projekte können Tests auch auf einem anderen Weg organisiert werden. Dies geschieht über Attribute, die den einzelnen Testmethoden vorangestellt werden. Zur Auswahl stehen hierbei TestCategory, Owner, Priority und TestProperty. Auf diese Weise wird es möglich, Tests gezielt zu beschreiben und über den Test Explorer und den Build-Server auszuwerten.
[TestMethod]
[TestCategory("DataBaseIntegrationTest")]
[Owner("Mike")]
[Priority(1)]
[TestProperty("Duration","long running")]
public void MethodName()
{
// test code
}
Listing 3.2: Testattribute zur Kategorisierung
Damit der Test Explorer die Attribute auch wirklich auswerten kann, muss zunächst noch ein Update für Visual Studio installiert werden, da er in der Auslieferungsversion nicht über die notwendige Funktionalität verfügt. Nachdem dies getan wurde, enthält das in Abbildung 3.2 gezeigte Drop-down einen Eintrag „Merkmale“. Mit diesem werden die Tests auf die gleiche Weise im Test Explorer gruppiert, wie es im Code hinterlegt wurde, wobei jedes Attribut in einer eigenen Gruppe resultiert. Sollten diese Kategorien dabei nicht ausreichen, können eigene mit dem Attribut TestProperty definiert werden, indem jenen als erster Parameter der Name der Kategorie und als zweiter der entsprechende Wert übergeben wird.
Abbildung 3.2: Kategorien im Test Explorer
Darüber hinaus können Kategorien auch durch ein Build-System ausgewertet werden, wodurch es möglich wird, nur Tests einer bestimmten Art auszuführen. Dafür muss das Kommandozeilenprogramm für jeden Testdurchlauf mit dem Parameter /category: gefolgt vom Namen der entsprechenden Kategorie gestartet werden.
Beispiel: mstest /testcontainer:[projectname].dll /category:" DataBaseIntegrationTest"
3.4.4 Tests ignorieren
Listing 3.3 zeigt, wie Tests unter Verwendung des Ignore-Attributs von jeder Art Ausführung ausgenommen werden. Das Attribut kann dabei sowohl auf Testmethoden als auch Testklassen angewendet werden, wobei in letzterem Fall alle Methoden der Klasse betroffen sind.
[TestMethod]
[Ignore]
public void MethodName()
{
// test code
}
Listing 3.3: Ein ignorierter Test
Hinweis: Tests, die mit Ignore gekennzeichnet wurden, werden in jeder Hinsicht nicht vom Testframework berücksichtigt. Dies kann gegenüber der Verwendung von Assert.Inconclusive und Assert.Fail zu einem erheblichen Geschwindigkeitsgewinn beitragen, da in diesem Fall auch keine Testdaten angelegt sowie keine...