Test First spart Zeit! — Warum es länger dauert, Unit-Tests nach dem Code zu schreiben

Unit-Tests zuerst! Dann entsteht schneller besserer Code! [ Foto von luis gomes auf pexels]

„’Test first‘ machen wir nicht! Unit-Tests? Haben wir natürlich, aber wir schreiben sie hinterher.“ Ich weiß nicht mehr, wie oft ich diesen oder einen ähnlichen Satz schon gehört habe. Ich bin immer wieder erstaunt, wie unüblich es zu sein scheint, Tests, insbesondere Unit-Tests zuerst zu schreiben. Dabei gibt es so viele gute Gründe, sie zuerst zu schreiben.

Die Vorgehensweise ‚Test First‘, also ZUERST einen — zunächst fehlschlagenden — Test zu schreiben und erst anschließend den Code, der den Test erfolgreich macht, ist in der Praxis so vorteilhaft, dass sie sogar einen eigenen Namen hat, nämlich Test-Driven-Development, kurz TDD.

Und weil TDD immer wieder erwähnt, aber erstaunlich selten praktiziert wird, möchte ich in loser Folge nun eine Reihe guter Gründe dafür darstellen, warum es viel vorteilhafter ist, zuerst die Tests und dann den Code zu schreiben, anstatt andersherum vorzugehen. 

Auch wenn es dabei an der ein oder anderen Stelle mal etwas technischer wird, versuche ich, den roten Faden auch für Nichttechniker nachvollziehbar zu halten. Einfach, weil es für alle in einem Team, das ein Software-Produkt erstellt, wichtig ist, ein grundlegendes Verständnis der Erfolgsrezepte bei der agilen Erstellung von Software zu haben.

Die Frage ist also: Test first oder Code first? Unit-Tests führen Code aus, um sicherzugehen, dass er tut, was die Entwickler erwarten. Daher scheint es auf den ersten Blick völlig egal zu sein, ob man diese Tests schreibt, bevor oder nachdem man den Code erstellt. Tatsächlich gibt es eine ganze Reihe guter Gründe, zuerst den Test zu schreiben. In diesem Artikel beginnen wir mit Grund #1.

Test First verhindert untestbaren Code

Warum ist das so? In TDD muss sich jedes Stückchen Code erstmal seine Existenzberechtigung verdienen. Und zwar in Form eines fehlschlagenden Tests. Daher ist es ganz offensichtlich, dass so kein ungetesteter, also auch kein untestbarer Code entstehen kann.

Wobei es eigentlich gar nicht um wirklich untestbaren Code geht. Denn praktisch jeden Code kann man irgendwie testen. Nur ist das oft so aufwändig und kompliziert, dass es entweder gleich ganz unterbleibt, und die Developer erklären dann “Der Code ist untestbar!”

Oder die Unit Tests, die diesen Code testen, sind lang, schwer verständlich, aufwändig zu warten und enthalten oft ihrerseits Fehler. Das ist eine Situation, die man tatsächlich sehr häufig antrifft. Wenn Code ohne Rücksicht auf Testbarkeit entsteht, sind nachher alle Unit Tests entsprechend “hässlich”.

Developer, die Unit Tests in solchen Umgebungen kennen lernen, haben dann oft die Vorstellung, dass Unit Tests so aussehen müssen und daher immer so aufwändig zu schreiben und zu pflegen sind. Und gleichzeitig üben sie sich täglich darin, schlecht testbaren Code zu schreiben.

Das passiert einem leider schneller als einem lieb ist. Hier ein paar typische Muster und Beispiele:

Das „wuchernde SUT“

Als Tester möchte ich mein System-under-Test (SUT) so festlegen, dass es möglichst ausschließlich die zu testende Einheit selbst und sonst nichts enthält. Je größer das SUT, desto mehr Zustände, Interaktionsmöglichkeiten und mögliche Abläufe gibt es — Komplexität, die das Testen erschwert.

Wie entsteht so etwas? Zwei Beispiele: 

  • In deiner Methode verwendest du eine non-virtual oder finale Dependency. Und schon ist es unmöglich, in einem Test statt des Originals einen Platzhalter einzusetzen, dessen Verhalten du als Test-Autor festlegen kannst. Die Dependency gehört nun mit all ihren Zuständen, möglichen Interaktionsfolgen und vor allem ihren eigenen Abhängigkeiten untrennbar zum SUT. Benötigt die Dependency eine funktionierende Datenbank-Verbindung, sind Tests ohne Datenbank nicht mehr möglich.
  • Du erkennst eine unabhängige Teilfunktionalität und lagerst sie in eine Helper-Methode aus. Da die Methode keinen eigenen State hat, machst du sie static. Diese Methode selbst ist super einfach zu testen. Aber ihre Aufrufe nicht mehr. Denn der Code in der Heller-Methode gehört nun IMMER mit zum SUT und muss jedesmal mitgetestet werden.

In beiden Fällen „wuchert“ das SUT. Damit meine ich, dass es für den Test unbeeinflussbar größer als nötig wird. 

Das unhandliche Interface

Auch das entsteht ruckzuck. Hier ein paar Beispiele aus meiner Erfahrung:

  • In einem Pfad wirft der Code eine Exception, mit der der aufrufende Code umgehen muss. Oder schlimmer noch, explizit fangen, werfen, weiterwerfen oder ignorieren muss. Ruckzuck wird aus einem Test, der eigentlich nur eine Zeile lang sein müsste, einer, der 10x so lang ist.
  • In manchen Fällen kommt null zurück, kein Objekt. Und jeder Aufrufer muss damit umgehen.
  • Die Methode liefert keinen plausiblen return-Wert. Und wie findet man jetzt einfach heraus, ob das Objekt die gewünschte Zustandsänderung erfahren hat? Indem man mehr Code schreibt. Manchmal viel mehr.

Code mit „Prozedural-Akzent“

Viele Entwickler entwerfen in meiner Erfahrung unwillkürlich, wenn sie den Code zuerst schreiben, prozeduralen Code. So gestalteter Code geht meist Schritt für Schritt durch ein Szenario und enthält if-Anweisungen für unterschiedliche Fälle. Wenn diese Art von Code in einer objektorientierten Sprache geschrieben wird, enthält er oft eine Reihe von Code-Smells, auf gut deutsch: er „müffelt“. In einer funktionalen Sprache wirkt er seltsam aufgebläht und ebenfalls unpassend. Der Code klingt, als hätte er einen Akzent. Und er ist unnötig schwer zu testen.

Bei einem ‚Test first‘-Vorgehen landet dieser prozedurale Code im Test. Und das ist großartig! Genau da gehört er hin. Ein Test beschreibt ein Szenario. Der eigentliche Code hingegen ist nun frei, die Abstraktionen und das Potenzial der Programmiersprache auszunutzen, stateless functions, lambdas, mix-ins, contracts, was auch immer.

Fazit

Wenn wir erst den Code und dann den Test schreiben, brauchen wir viel länger als wenn wir einem Test First Ansatz folgen und erst den Test und dann den Code schreiben. Ganz einfach deshalb, weil im ersten Fall schlecht testbarer Code entsteht, für den es signifikant länger dauert, einen Test zu schreiben. Und wenn wir in Abständen von wenigen Tagen Software-Stände erzeugen wollen, die an Kunden auslieferbar sind, können wir uns eine solche Zeitverschwendung nicht leisten.

Außerdem erzeugen wir mehr, schlechter verständliche und dadurch aufwändiger zu wartende Tests, gegebenenfalls sogar schlechteren Code.

Wenn du konkret lernen möchtest, wie man stattdessen erfolgreich mit Scrum Software entwickelt, lade ich dich herzlich ein, unseren Advanced Certified Scrum Developer Kurs zu besuchen. Dort sprechen wir über ‚Test first‘ und viele andere relevante Aspekte der Code-Entwicklung in agilen Umfeldern und vor allem – wir wenden diese Konzepte an, indem wir gemeinsam Software entwickeln.

Wie ist es bei euch? Schreibt dein Team gut lesbare Unit-Tests? Und entstehen sie vor oder nach dem Code? Warum macht ihr das so? Wie zufrieden seid ihr damit?

Ähnliche Beiträge

Schreibe einen Kommentar

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