Ich beschäftige mich seit einer ganzen Weile mit [Unit-Tests] und würde theoretisch sofort zustimmen, dass es eine ganz tolle Sache ist. Meine (noch nicht besonders umfangreichen) praktischen Erfahrungen sind aber bislang eher ernüchternd. Das Schreiben von Unit Tests kostet m. E. definitiv Zeit. Studien sprechen von 30% mehr Entwicklungsaufwand. Vielleicht mache ich auch etwas falsch, aber nach meinem Gefühl landet man eher beim Faktor 2.
Was heißt Unit Testing für den Entwickler? Muss man sich damit abfinden, weniger Produktivcode zu schreiben? Gibt es etwas, dass den Aufwand langfristig kompensiert? Auch ohne Unit Tests bin ich bis jetzt z. B. nicht in Bug-Reports untergegangen.
Mein Verdacht: Die meisten Unit Testing Fans stammen aus dem Bereich der Bibliotheksentwicklung (im weitesten Sinn): nichttrivialer Code, hantieren mit eher überschaubaren Objekten (was die Anzahl der Member angeht). Soll universell (nicht nur im Zusammenhang mit bestimmtem Client-Code) einsetzbar sein, hat extrem hohe Anforderungen an Stabilität und Fehlerfreiheit (hier leiden die Massen, wenn nicht). Alles Eigenschaften, die platter Anwendungscode nicht hat.
Wenn ich jetzt z. B. einen Controller mit einer typischen Methode vor mir habe: Daten irgendwo abrufen, vielleicht trivial in ein ViewModel verpacken und ab an die Oberfläche. Das soll ich jetzt Unit-testen? Na gut, ist nicht besonders schwierig, geht aber ziemlich schlecht von der Hand. Wenn ich den Datenzugriff durch ein Test-Double ersetze muss ich häufig ziemlich breite Datenkontainer simulieren, sprich solche mit vielen Membern. Ist tippintensiv und ermüdend. Und was habe ich gewonnen? Dass dieser Code funktioniert hab ich vorher auch schon geglaubt.
Lohnt es wirklich, jede mehr oder weniger triviale Funktionalität zu unit-testen (s. Controller)? Ich meine, eine gut strukturierte Anwendung besteht letztlich nur aus Methoden, die im wesentlichen die Arbeit an Mitspieler delegieren.
Kommt man nicht mit einem Integrationstest (z. B. angefangen beim Controller bis ganz durch in die Datenbank) genauso gut weiter?
Klare Zielvorgaben
Erfolgreich Software zu entwickeln bedeutet, zielgerichtet vorzugehen. Diese Ziele sollten sich aus mit dem Business abgestimmten Akzeptanzkriterien ergeben. Ohne eine klare Zielvorgabe – wir meinen damit Tasks, nicht Projekt- oder gar Jahresziele – läuft der Entwickler Gefahr, sich bei der Arbeit zu verlieren. Er weiß insbesondere nicht, wann er mit einem Task fertig ist.
Es bietet sich an, Akzeptanzkriterien durch automatisierte Tests zu dokumentieren und zu überprüfen. So oder so müssen die Ziele definiert werden, bevor Produktionscode geschrieben wird. Das ist testgetriebene Entwicklung, egal ob man es so nennen will oder nicht.
Wenn das nachträgliche automatisierte Testen von Code als schmerzhafte Mehrbelastung empfunden wird, dann fehlte beim Schreiben des Codes die klare fachliche (nicht technische) Zielvorgabe. Nachträgliches Testen führt zu Problemen, weil nicht klar genug ist, wozu der Code eigentlich existiert. Wie soll man in einer solchen Situation auch sinnvoll Tests formulieren?
Die Hauptaufgabe eines Entwicklers ist nicht das Schreiben von Code, sondern das Verstehen eines Problems. Das Formulieren von Tests, also der Zielvorgaben, hilft, ein Problem Schritt für Schritt zu durchdringen. Die Tests treiben die Entwicklung also nicht als Selbstzweck, sondern tun dies, indem sie die notwendigen Denkprozesse anstoßen.
Muss man auch trivialen Code testen? Gerade hier passieren Fehler, weil niemand genau hinschaut. Und gerade hier sucht man Probleme zuletzt. Ein Test darf daher keinen Unterschied zwischen trivialem und komplexem Code machen; er soll schließlich nicht Implementierungsdetails testen, sondern Akzeptanzkriterien verifizieren.
Besser Integrationstests?
Wenn bei der Ausführung von Produktionscode alles wie erwartet abläuft, also nichts Außergewöhnliches passiert und kein Fehler auftritt, scheint ein Integrationstest ausreichend, zumal er mit weniger Aufwand geschrieben werden kann. Fehler im sogenannten Happy Path sind offensichtlich, da sie ein Feature sichtbar kaputt machen.
Je mehr Änderungen am Code über die Zeit gemacht werden, desto mehr werden Ausführungspfade durchlaufen werden, an die man heute noch gar nicht gedacht hat. Ein Integrationstest, der auf den Happy Path fokussiert, reicht jetzt nicht mehr aus, um die Änderungen gegen neue Fehler abzusichern. Je mehr unterschiedliche Ausführungszweige man testen muss, desto nützlicher werden Unit Tests. Sie sind schneller auszuführen und liefern ein präziseres Ergebnis als Integrationstests.
Zu Beginn der Entwicklung muss der Schwerpunkt beim Testen auf dem Happy Path liegen. Später ändert sich die Sichtweise und man denkt über Edge Cases und Sonderfälle nach. Hierbei geht es nicht nur darum, dass heutige Verhalten des Codes zu verifizieren, sondern zukünftige Änderungen, die heute noch nicht absehbar sind, abzusichern.
Wer die Produktivität eines Entwicklers an der Menge von geschriebenem Code misst, hat ein falsches Bild von Softwareentwicklung. Dies erkannte schon Bill Gates, der sagte:
Measuring programming progress by lines of code is like measuring aircraft building progress by weight.
Eine klare, fachliche Zielorientierung beim Entwickeln von Software führt dazu, dass weniger Produktionscode entsteht. Automatisiertes Testen unterstützt genau dies.