TDD vs. Test-first

Immer wieder erlebe ich Diskussionen um die Frage, wie man denn nun Software so richtig richtig testet. Die Erkenntnis, dass automatisierte Tests notwendig sind, scheint sich inzwischen durchgesetzt zu haben. Ich erlebe nicht mehr, dass Entwickler ernsthaft behaupten, automatisierte Tests wären Zeitverschwendung, zu kompliziert, in ihrem Projekt halt unmöglich oder was auch immer die Argumente früher waren. Automatisierte Tests sind offensichtlich von allen Entwicklern anerkannt, als wichtigster Beitrag zum Wert der Korrektheit.

Damit ist der erste Schritt getan. Nur: automatisierte Tests für sinnvoll halten und tatsächlich Tests regelmäßig schreiben, sind zwei Paar Schuhe. Natürlich sind automatisierte Tests im Umfeld von Legacy Code eine andere Herausforderung, als auf der grünen Wiese. Methoden und Klassen mit unklaren Zuständigkeiten, dazu noch voller Abhängigkeiten, lassen sich nicht leicht automatisiert testen. Doch auch auf der grünen Wiese tun sich Entwickler schwer. Da wird gerne über den Sinn oder Unsinn bestimmter Praktiken und deren Details diskutiert, statt es einfach zu tun. Und ganz offensichtlich fehlt ein allgemein anerkannter Prozess, in dem die Schritte beschrieben sind, die auf dem Weg von den Anforderungen zum Code durchlaufen werden müssen. „TDD“ mögen nun einige rufen. Joa, schon besser als einfach los kodieren. Konsequentes Zerlegen von Anforderungen und Entwurf sind Tätigkeiten, die ich in den allermeisten Teams nicht entdecken kann und trotzdem für essentiell halte. Aus gutem Grund habe ich gemeinsam mit meinem Kollegen Ralf Westphal mit Flow Design eine Vorgehensweise und Entwurfssprache entwickelt, mit der diese Lücke geschlossen wird.

Doch bleiben wir beim Thema automatisiertes Testen. Meine Beobachtung ist, dass es hier schon bei den Begriffen durcheinander geht. So lange nicht jedem an einer Diskussion beteiligten klar ist, welche Bedeutung Begriffe wie „Unit Test“, „TDD“ oder „Test-first“ haben, wird die Diskussion nicht produktiv sein, sondern sich in Missverständnissen verstricken. Ich möchte daher mit diesem Beitrag darstellen, welche Bedeutung die einzelnen Begriffe für mich haben. Gerne können wir dann andere Bedeutungen diskutieren, mit dem Ziel, uns einem Konsens zu nähern, wie die Begriffe gemeint sind. Danach können wir beginnen, über Sinn oder Unsinn bestimmter Details zu diskutieren.

Werte

Damit Software die Anforderungen des Product Owners korrekt umsetzt, muss sie getestet werden. Der Wert der Korrektheit wird also nur erreicht, wenn wir Software testen, ob nun automatisiert oder manuell. Vielleicht kommen wir in der Informatik irgendwann dahin, dass die Korrektheit auf andere Weise nachgewiesen werden kann. Derzeit geht an Tests kein Weg vorbei. Um auch dem Wert der Produktionseffizienz gerecht zu werden, müssen die Tests automatisiert werden. Daraus ergibt sich die Praktik des automatisierten Testens. Als Entwickler schreiben wir automatisierte Tests, um die Sicherheit zu erhöhen, dass die Anforderungen korrekt umgesetzt sind. Für die Automatisierung der Tests kommen Frameworks wie JUnit, NUnit, Mocha, o.ä. zum Einsatz. Ein automatisierter Test ist somit ein Stück Software, meist eine Methode, welches einen Teil der Implementation durchläuft und Annahmen über das Resultat trifft. Diese Annahmen werden automatisiert überprüft, so dass der Test Erfolg oder Misserfolg anzeigen kann. Durch die Automatisierung ist es möglich, die Tests wiederholt auszuführen, ohne dass dadurch nennenswerte Kosten entstehen. Dies ist einer der Unterschiede zum manuellen Testen. Manuelle Tests kosten sehr viel Geld und werden daher möglichst selten durchgeführt. Der zweite Unterschied ist, dass durch die Automatisierung der Tests sichergestellt ist, dass die Testfälle bei jedem Ausführen der Tests garantiert immer wieder dieselben Annahmen überprüfen. Manuelle Tests leiden häufig darunter, dass trotz Testanweisungen in schriftlicher Form, nicht alle Annahmen exakt überprüft werden. Es braucht sehr viel Disziplin und eine sehr gute Organisation, Tests manuell durchzuführen.

Automatisierter Test

Wird ein Testfall automatisiert, ist das Ergebnis ein automatisierter Test und nicht zwingend ein Unit Test. Auch wenn Testframeworks manchmal „Unit Test Framework“ genannt werden, ist dieser Begriff eigentlich falsch. Es müsste „Automated Test Framework“ o.ä. heißen. Die Frameworks dienen zur Automatisierung von Tests. Mit einem solchen Test können sowohl Unit Tests als auch Integrationstests und sogar Systemtests automatisiert werden. Alle drei sind also orthogonal zum Begriff automatisierter Test. Stattdessen beschreiben diese Begriffe, welche Ausschnitte eines Softwaresystems getestet werden. In der folgenden Abbildung ist ein Baum von Funktionseinheiten mit ihren Abhängigkeiten dargestellt.

Unit Test

Ein Unit Test ist dafür zuständig, eine Unit, oder auch Funktionseinheit, isoliert zu testen. Wird eines der Blätter getestet, handelt es sich um einen Unit Test, da die Blätter keine weitere Abhängigkeiten haben. Ein Blatt ist bereits eine freigestellte Einheit.

Integrationstest

Testet man dagegen einen der Knoten, handelt es sich zunächst um einen Integrationstest. Der Knoten wird inklusive seiner Abhängigkeiten getestet. Man testet damit also nicht eine Einheit in Isolation, sondern inkl. der Abhängigkeiten. Das Ziel der Integrationstests ist es herauszufinden, ob die Integration der Bestandteile korrekt funktioniert.

Systemtest

Eine spezielle Form von Integrationstest sind die Systemtests. Mit einem Systemtest wird das gesamte Softwaresystem inklusive aller Abhängigkeiten getestet. Insbesondere wird hier also durch die Ui hindurch getestet und es kommen die realen Ressourcen wie Datenbanken oder spezielle Hardware zum Einsatz. In Integrations- und Systemtests sind in ihrer reinen Form keine der Bestandteile durch Attrappen ersetzt.

Mit Interfaces und Attrappen zum Unit Test

Manchmal besteht der Wunsch, eine Funktionseinheit, die über Abhängigkeiten verfügt, isoliert zu testen. Die Herausforderung hier ist also, aus einem Knoten eine Unit zu machen. Der Knoten muss für einen Unit Test von seinen Abhängigkeiten befreit werden. Dazu werden Interfaces und Attrappen eingesetzt. Dadurch wird der Knoten isoliert und in einem Unit Tests kann nur der Bestandteil des Knoten getestet werden, ohne dass die Abhängigkeiten einen Einfluss ausüben. Doch hier gilt es nun aufzupassen: ein solcher Test ist kein Integrationstest mehr, da im Test nicht die realen Abhängigkeiten integriert werden, sondern Attrappen, die sich im Test möglicherweise anders verhalten, als die Originale, die sie ersetzen.

Ressourcentest

Manche Kollegen bezeichnen einen Test, der auf eine externe Ressource wie das Dateisystem zugreift, nicht mehr als Unit Test. Diese Definition halte ich für falsch! Selbstverständlich kann eine Funktionseinheit gleichzeitig eine Unit sein und einen Ressourcenzugriff durchführen. Eine Methode, die einen Dateinamen erhält und den Inhalt der Datei zurück liefert, ist eine Unit, da sie keine Abhängigkeiten zu anderen Methoden der Lösung hat. Wieso sollten automatisierte Tests dieser Methode nicht Unit Test genannt werden? Hier werden von den Kollegen zwei Aspekte vermischt. Einerseits die Betrachtung der Abhängigkeitsstruktur und andererseits der Zugriff auf externe Ressourcen. Beide Aspekte sind orthogonal, also voneinander unabhängig. Ich kann einen Ressourcenzugriff in einer Methode realisieren, dann ist diese Methode eine Unit und kann mit einem Unit Tests überprüft werden. Ein Ressourcenzugriff kann aber auch mit vielen voneinander abhängigen Methoden realisiert werden, dann kann ich auf der obersten Ebene einen Integrationstest ansetzen. Ich spreche daher zur Unterscheidung von einem Ressourcentest, wenn eine Funktionseinheit getestet wird, die auf eine externe Ressource zugreift. Eine Unit Test und auch ein Integrationstest kann also gleichzeitig ein Ressourcentest sein.

Damit haben wir nun folgende Begriffe zur Verfügung:

  • Automatisierter Test vs. Manueller Test – Aussage über die Automatisierung der Tests
  • Unit vs., Integrations- vs. Systemtest – Aussage über den Umgang mit Abhängigkeiten
  • Ressourcentest – Aussage über den Zugriff auf externe Ressourcen

TDD vs. Test-First

Es fehlen die beiden Begriffe TDD (Test Driven Development) und Test-first. Ich bin mir nicht sicher, ob diese Begriffe von einigen Kollegen synonym verwendet werden. Für mich gibt es einen wesentlichen Unterschied. Test-first beschreibt lediglich, dass jeweils zuerst ein automatisierter Test erstellt wird, bevor Funktionalität implementiert wird. Im Gegensatz dazu steht Test-after. Hierbei wird erst die Funktionalität implementiert und erst dann werden automatisierte Tests erstellt.

Mit Test Driven Development wird mehr beschrieben als lediglich Test-first. Auch bei TDD werden die Tests jeweils vor der Implementation erstellt. Allerdings soll darüber hinaus durch die Tests die Struktur der Software entwickelt werden. Vereinfacht gesagt beginnt man jeweils mit einem Test, implementiert gerade soviel, dass der Test grün wird und refaktorisiert dann ggf. um die Verletzung von Prinzipien zu eliminieren. Habe ich mich bspw. bei der Implementation wiederholt und dadurch das DRY Prinzip verletzt, extrahiere ich die Gemeinsamkeiten. Oder ich stelle fest, dass eine Methode mehr als eine Verantwortlichkeit hat und damit gegen das SRP verstößt. Auch hier extrahiere ich die Verantwortlichkeiten durch eine Refactoring Maßnahme um das Einhalten der Prinzipien wieder herzustellen. Jeweils im Anschluss an Refactorings können die Tests erneut ausgeführt werden und so sicherstellen, dass durch das Refactoring nichts kaputt gegangen ist.

Über die Wirksamkeit von TDD lässt sich streiten und ich habe dazu eine klare Meinung. Vor einer solchen Diskussion muss allerdings Klarheit über die Begriff herrschen. Test-First ist Bestandteil von TDD. TDD ergänzt den Refactoring Schritt und versucht, durch diese Vorgehensweise eine Implementation zu entwickeln, die die wichtigsten Prinzipien einhält.

Fragen

Fehlen wichtige Begriffe?

Gibt es andere Bedeutungen der Begriffe, die ich hier übersehen habe?

Nutzen sie gerne die Kommentarfunktion!

Schreibe einen Kommentar