Test Driven Development

Die invertierte Testpyramide

Im Lebenszyklus einer Software nimmt die Wartungsphase bekanntlich den längsten Zeitraum ein. Damit der Code wartbar ist und sich nicht zu einem Lock-In für die Stakeholder entwickelt, muss er einer kontinuierlichen Qualitätssicherung unterzogen werden. Diese Qualitätssicherung ist sinnvollerweise automatisiert, denn manuelle Tests sind aufwändig und fehleranfällig.

Test Driven Development, kurz TDD, benennt das automatisierte Testen vor der Erstellung der Komponenten, anstatt im Nachgang. Dies mag zwar auf kurze Sicht aufwändiger sein, auf lange Sicht spart es jedoch viele Ressoucen ein. So geht laut Kent Beck ein Anti-Pattern mit der Vernachlässigung der Testautomation einher. Er nennt es den „no time for testing“-Teufelskreis: „The more stress you feel, the less testing you will do. The less testing you do, the more errors you will make. The more errors you make, the more stress you feel“. Dieses Zitat spricht für sich: Mehr Zeitdruck bedeutet weniger Tests, daraus resultieren Fehler, woraus wieder Zeitdruck entsteht.

Aus diesem Grund beschäftige ich mich in diesem Artikel mit den Vor- und Nachteilen des Test Driven Developments und den Möglichkeiten, welche diese Art der Test-Skalierung für die Softwareentwicklung mit sich bringt.

Der Aufbau von Softwaretests

Softwaretests lassen sich unterteilen in Unit-, Integrations- und E2E-Tests. Die Testpyramide greift diese Einteilung auf und ergänzt sie um die Faktoren Anzahl und Aufwand. Das Fundament der Pyramide bilden viele schnelle Unittests, in der Spitze gibt es wenige langsame E2E-Tests. Das Bindeglied bilden die Integrationstests.

Tests dienen auch der Dokumentation der Semantik der Software. Sie beschreiben das zu erwartende Verhalten und dienen der Verifikation der korrekten Funktion. Das Verhalten der Software wird entweder bei der Implementierung neuer Funktionalität bewusst oder beim Refactoring unabsichtlich geändert. Damit letzteres nicht passiert bedarf es einer Abdeckung der Codebasis mit Unit-Tests.

Auf die Frage wieviel Testabdeckung sein sollte antwortet Martin Fowler folgendermaßen: „The best measure for a good enough test suite is subjective: How confident are you that if someone introduces a defect into the code, some test will fail?“. Diese Sicherheit, die Tests bieten, ist die Voraussetzung für Refactoring.

Refactoring als Verbesserungsmöglichkeit

Refactoring besteht aus vielen kleinen Änderungen der Codestruktur, nicht jedoch des Verhaltens der Software. Dieses kontinuierliche Refactoring erhält die Wartbarkeit und Flexibilität des Systems. Es setzt allerdings ein schnelles Feedback durch das Testsystem voraus. Dieses schnelle Feedback ist nur dann gegeben, wenn es genügend Unit-Tests gibt. E2E-Tests hingegen sind langsam und eignen sich nicht als Feedback-Geber.

Die invertierte Testpyramide

Wenn man sich dennoch auf E2E-Tests verlässt handelt es sich um ein Anti-Pattern. Ich nenne es die invertierte Testpyramide. Dieses Anti-Pattern besteht darin, dass ein Softwareprojekt statt durch Unittests durch langsame E2E-Tests getragen wird. Dadurch wird der Entwicklungsprozess insgesamt verlangsamt und das notwendige Refactoring der Software gerät aus dem Blick. Statt dessen konzentrieren sich erhebliche Bemühungen darauf, neue E2E-Tests zu schreiben und die bestehenden Tests zu warten.

Das Fehlen von Tests erschwert Refactoring, was wiederum zu schwer wartbarem Code führt. Dieser Code ist zumeist schwer testbar, weil grundlegende Voraussetzungen für das Testen oft fehlen. Damit ein Softwaresystem gut testbar ist, müssen die Komponenten lose gekoppelt sein. Fehlen von Dependency Injection, Missachtung des Law of Demeter oder die übermäßige Verwendung von statischen Methoden führen zu einer starken Koppelung der Komponenten. Das heißt, dass das Fehlen von konsequentem TDD bei größeren Softwareprojekten fast zwangsläufig in die Sackgasse führt.

Das Durchbrechen des Missing-Tests-Teufelskreises

Erst durch den „Test first“-Ansatz wird aus dem Teufelskreis, in dem sich die technischen Schulden zunehmend erhöhen, ein inkrementeller Entwicklungsprozess. So wie vor der Implementierung getestet wird, so kommt vor der Testimplementierung das Testergebnis. Kent Beck unterscheidet zwischen dem Ergebnis des Tests und dem Test-Algorithmus und empfiehlt, beim Unit-Test mit der Assertion anzufangen. Aus dem erwarteten Ergebnis ergibt sich die Methode. Soll also beispielsweise getestet werden, ob ein Token einen bestimmten Wert enthält, fängt man mit dem erwarteten Wert an und arbeitet in umgekehrter Reihenfolge über die Decodierung des Strings bis zur Erzeugung des Tokens.

Test Driven Development als Teil der agilen Softwareentwicklung

Zur agilen Softwareentwicklung gehört Test Driven Development genauso wie Refactoring. Sie ist sogar auf Dauer ohne nicht möglich. Aus den Anforderungen der Stakeholder, die ein Projekt hat, ergeben sich kontinuierliche Änderungen an der Software. Erst Tests und Refactoring ermöglichen die Anpassungsfähigkeit der Software.

Martin Fowler fasst diesen Gedanken folgendermaßen zusammen: „A healthy code base maximizes our productivity, allowing us to build more features for our users both faster and more cheaply. To keep code healthy, pay attention to what is getting between the programming team and that ideal, then refactor to get closer to the ideal“.

Artikel von Mischa Siebert, Consultant bei objective partner