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]

„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, 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: Tests zuerst oder Code zuerst? 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.

Grund #1: Es kann kein untestbarer Code entstehen

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.

In der Praxis ist es gar nicht so einfach, wirklich untestbaren Code zu schreiben. Sehr viel häufiger und praxisrelevanter ist der Fall, dass schwer oder aufwändig testbarer Code entsteht. Unit-Tests, die diesen Code testen, sind lang, schwer verständlich, aufwändig zu warten und enthalten oft ihrerseits Fehler. 

Developer, die Unit-Tests so kennenlernen, haben dann oft das Vorurteil, dass Unit-Tests immer so aussehen und immer so aufwändig zu schreiben und zu pflegen sind. Und gleichzeitig üben sie sich täglich darin, schlecht testbaren Code zu schreiben.

Das liegt näher, als man denkt, wie ich mit ein paar typischen Mustern und Beispielen illustrieren will.

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, constracts, was auch immer.

Fazit

Wenn wir erst den Code und dann den Test schreiben, brauchen wir viel länger als wenn wir erst den Test und dann den Code schreiben, und zwar ganz einfach deshalb, weil im ersten Schritt schlecht testbarer Code entsteht, für den es signifikant länger dauert, einen Test zu schreiben.

Und außerdem erzeugen wir mehr, schlechter verständlichen und dadurch aufwändiger zu wartende Tests, und vielleicht sogar auch schlechteren Code.

Wenn wir in Abständen von wenigen Tagen Software-Stände erzeugen wollen, die an Kunden auslieferbar sind, können wir uns Zeitverschwendung nicht leisten. 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 dieses 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?

Pascal Gugenberger
Pascal Gugenberger
Pascal ist Certified Scrum Trainer der Scrum Alliance und zugleich erfahrener Agile Coach mit einem soliden Software-Development Background. Seit über 15 begleitet er Menschen und Organisationen auf ihrem Weg zu mehr Agilität. Er brennt dafür, Menschen und Organisationen zu helfen in einer immer dynamischeren Welt ihr Potenzial zu entfalten. Mit menschengerechten Trainings und Perspektiven eröffnendem Coaching - mit Leidenschaft und Einfühlungsvermögen.

Noch mehr lernen

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht.

Ich freue mich, Dich kennenzulernen

Pascal Gugenberger

Pascal

Ich freue mich auf unser Gespräch!

Such Dir einfach einen Termin aus und wir quatschen.

Weiterleitung an Calendly, siehe Datenschutzerklärung.

Lust auf Input?

Newsletter

Melde Dich an und erhalte regelmäßig spannende Beiträge!