Von Tests und Komponenten

Hallo Spaß-Coder.

Wer von euch liefert gerne großartige Qualität ab? Wo beginnt großartige Qualität des gesamten Systems? Was ist Voraussetzung dafür, dass eine Anwendung – bestehend aus vielen kleinen Bausteinen – unseren eigenen sowie den Anspruch unserer Kunden an eine hohe Qualität erfüllt? Wie wäre es, wenn wir das gesamte System testen? Wie wäre es, wenn jede einzelne Komponente des Systems getestet ist? Wenn jeder Baustein für sich funktioniert, funktioniert dann auch das gesamte System?

 

Komponententests (Unit Tests)

Die kleinste Einheit in einem System, nennen wir sie Methode, hat eine definierte Schnittstelle mit Ein- und Ausgaben und nach Anwendung der SOLID-Prinzipien eine beherrschbare Komplexität. Daher ist es vielleicht eine gute Idee, hier mit den Tests zu beginnen.

Der Test von Komponenten hat zum Ziel, einen isolierten Baustein (z.B. eine Methode) losgelöst vom Rest der Anwendung zu testen und die Korrektheit abzusichern. Dieser isolierte Baustein wird auch als SUT – „system under test“ oder „subject under test“ bezeichnet [2]. Erstellen wir eine neue Klasse mit einzelnen Methoden, ist es leicht die Methoden isoliert zu betrachten. Schwieriger wird es, wenn nachträglich Komponententests für ein bestehendes System hinzugefügt werden. Dabei besteht die Herausforderung, zu testende Einheiten vom Rest des System zu isolieren und somit für sich zu prüfen. Welche Methoden und Techniken hierfür bereitstehen, werden wir uns in einem späteren Artikel ansehen (siehe auch [1]).

Sobald die Isolation nicht gegeben ist und zwei oder mehr von einander abhängige Komponenten zusammen getestet werden, sprechen wir von Integrationstests, zu denen ein eigener Artikel erscheinen wird. Das bedeutet auch, dass jede Abhängigkeit zur Umwelt des SUT für den Komponententest zu entfernen ist. Also Zugriffe auf das Dateisystem, Netzwerke oder Datenbanken. Diese Abhängigkeiten sind für den Unit Test aufzulösen, da wir keine direkte Kontrolle über ein Netzwerk haben und wir auch gar nicht das Netzwerk, sondern unsere Komponente testen wollen. Doch wie kann ich eine Abhängigkeit zu einem Dateisystem temporär für die Ausführung der Tests auflösen?

Dafür gibt es ein paar nützliche Techniken, von denen nachfolgend einige vorgestellt werden.

Fake

Ein Fake ist ein Objekt, welches sich in Teilen so verhält wie die Abhängigkeit zu unserem SUT, jedoch nur die Implementierungen enthält, welche wir für einen bestimmte Test benötigen. Dies kann sinnvoll sein, wenn die Abhängigkeit weitere Abhängigkeiten hat, schwer zu erstellen oder langsam ist.

Stub

Ein Stub ist ein Objekt, welches die Abhängigkeit ersetzt und unserem SUT genau die für einen bestimmten Test festgelegten Daten zurück gibt, unabhängig von den Eingabeparametern. Dies ist eine sehr einfache Methode, Abhängigkeiten für einen Test loszuwerden.

Mock

Ein Mock ist ein Objekt, welches die Abhängigkeit ersetzt und unserem SUT je nach Eingangsparameter die von uns festgelegten Daten zurück gibt. Mocks werden häufig mit Hilfe von entsprechenden Frameworks verwendet, die es deutlich vereinfachen, Mock-Objekte zu parametrieren und zu erzeugen.

Die Definition von Stub und Mock unterscheiden sich je nach Quelle. Die oben genannten Definition entstammt der Wikipedia [3] und entspricht der üblichen Verwendung von Mock-Frameworks [siehe auch [4]). Roy Osherove schreibt in [2], dass das Mock-Objekt selbst nach Aufruf des SUT geprüft wird, da dies alle Aufrufe aufzeichnet und somit den Test validiert. Wir verwenden in den meisten Fällen die erste Definition, wenn wir mit Mock-Objekten arbeiten.

Wie bereits angesprochen, können diese Techniken deutlich leichter eingesetzt werden, wenn die Kopplung unseres SUT zu seinen Abhängigkeiten gering ist und SOLID angewendet wurde. So kann beispielsweise eine Fabrikklasse ein spezielles Objekt für den Test bereitstellen, wenn das DIP angewendet wurde.

Der grundsätzliche Aufbau eines Unit Tests ist immer gleich:

  1. Initialisieren des Ausgangszustandes; es werden alle für den konkreten Test erforderlichen Bausteine erzeugt
  2. Aufruf der zu testenden Methode
  3. Prüfung des Ergebnisses; hier wird mit Zusicherungen (assertions) gearbeitet, die das tatsächliche gegen das gewünschte Verhalten prüfen

 

Eine besondere Praktik  im Zusammenhang mit Komponententests ist das Test-Driven-Development (TTD), bei dem der Test-First-Ansatz verfolgt wird. Dabei wird zuerst der Komponententest erstellt und anschließend der Produktionscode implementiert. Diese Vorgehensweise ist nicht zwingend erforderlich für das Schreiben von Komponententests, hat jedoch einige Vorteile, die wir in einem weiteren Artikel noch beleuchten werden.

 

Die nachfolgenden Kriterien werden von guten Unit Tests erfüllt:

  • ein Komponententest sollte automatisiert und wiederholbar sein (siehe auch Testautomatisierung)
  • er sollte einfach zu implementieren sein
  • jeder sollte in der Lage sein, den Test auszuführen
  • er sollte auf Knopfdruck oder regelmäßig ablaufen
  • er sollte schnell laufen

In einigen Punkten unterstützen verschiedene Testframeworks die Arbeit, wie z. B. xUnit (also JUnit für Java). Konkrete Werkzeuge schauen wir uns im einem späteren Artikel genauer an. Die Notwendigkeit für ein schnelles Ausführen der Tests wird dann deutlich, wenn mit der Zeit einige hundert oder sogar tausend Tests existieren. Wenn jeder dieser Tests auch nur 10 Sekunden läuft, ist dies bei 1000 Tests eine Gesamtlaufzelt von 2,77 Stunden. Dabei würde das schnelle Feedback an den Entwickler, ob das erwartete Verhalten der Komponente mit dem tatsächlichen Verhalten übereinstimmt, uninteressant für kleine Programmierzyklen werden.

 

Unsere Erfahrung mit Komponententests

Häufig beginnen wir das Programmieren mit der Idee – „Lass uns mal einen Test schreiben.“. Wir haben dabei die Erfahrung gemacht, dass wir fokussierter Arbeiten, da wir genau ein bestimmtes Verhalten der gewünschten Komponente betrachten und dafür den Test schreiben. Das vereinfacht die Arbeit deutlich. Bei späteren Änderungen am  – durch Tests gesicherten – Code ist es ein gutes Gefühl von Sicherheit, anhand der automatisierten Komponententests direktes Feedback über unsere Änderungen zu bekommen: funktioniert die Änderung wie gewünscht? Welche Nebeneffekte hat die Änderung auf andere Teile der Anwendung? Uns fällt es einfacher, Tests direkt zu beginn schreiben wenn wir eine neue Klasse oder Methode entwickeln. Tests nachträglich in legacy code einzuführen birgt immer die Gefahr, dass der Produktionscode erst angepasst werden muss (SOLID), damit Tests überhaupt möglich sind.

Weiterhin sind es immer wieder kleine Erfolgserlebnisse eine Anwendung inkrementell wachsen zu sehen und dabei in kurzer Zeit grün auf weiß bestätigt zu bekommen, dass alle einzelnen Komponenten für sich funktionieren. Wenn jetzt noch alle richtig zusammengesteckt werden, steht einer hoch qualitativen Anwendung nichts mehr im Wege (Annahme: die Tests sichern das benötigte Verhalten des Kunden :-)).

Wie viel Testabdeckung ist wünschenswert, sinnvoll oder notwendig? Das Ziel von Komponententests ist bei nachträglicher Änderung des Codes eine Sicherheit zu haben, dass die Änderung frei von unerwünschten Nebeneffekten ist und somit kein anderes Verhalten des Systems negativ beeinflusst. Komponententests sind da einzusetzen, wo sie zu diesem Ziel einen sinnvollen Beitrag leisten.

 

Zusammenfassung

Jeder  von uns kann ein noch so großes Problem meistern und eine Lösung finden. Die Herausforderung dabei ist, das Problem in kleine, noch kleinere und kleinste Einheiten zu zerlegen, welche dann wieder beherrschbar werden. Wird nach und nach in kleinen Schritten jedes dieser kleinsten Problem gelöst, entsteht dadurch die Lösung des Gesamtproblems. Oder anders ausgedrückt – die längste Reise beginnt mit dem ersten Schritt.

 

Viel Spaß beim Anwenden.

Eure Spaß-Coder.

 

[1] Feathers, Michael C. Effektives Arbeiten mit Legacy Code: Refactoring und Testen bestehender Software. mitp Verlags GmbH & Co. KG, 2011.

[2] Osherove, Roy. The art of unit testing. mitp, 2010.

[3] https://de.wikipedia.org/wiki/Modultest, aufgerufen am 24.05.2015

[4] https://de.wikipedia.org/wiki/Mocking_Framework, aufgerufen am 24.05.2015

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert