Unit Tests bei Vertec
Unit Tests zu schreiben gehört für uns als Softwareentwickler in einem agilen Entwicklungsprozess genauso zur täglichen Arbeit wie die Umsetzung konkreter Anforderungen an unser Produkt. Im Allgemeinen setzen wir dabei klassisches White Box Testing ein. Für einfache Klassen ohne Abhängigkeiten greifen wir auch auf TDD (Test-Driven Development) zurück. Für diese eignet sich TDD gut, mit den entsprechenden Tests schreibt sich der Produktivcode oft wie von selbst. Sobald Abhängigkeiten ins Spiel kommen wird das deutlich schwerer und die wir schreiben unsere Tests meist, nachdem zu testenden Code geschrieben ist. Des Weiteren setzen wir für unterschiedliche Anforderungen auf passende Testkategorien.
Die Testpyramide bei Vertec
Unsere Testpyramide kennt derzeit zwei wesentliche Stufen. Die Basis bilden einfache, klar abgegrenzte Unit Tests. Mit ihnen testen wir nach Möglichkeit nur eine einzelne Klasse mit gemockten Abhängigkeiten. Diese Tests sind entsprechend veränderlich und ihre Pflege aufwändig. Wenn sich durch neue Anforderungen neue Abhängigkeiten ergeben, müssen wir unter Umständen auch bestehende Tests überarbeiten. Aus der Historie gibt es zudem Klassen, bei denen die Umsetzung dieser Tests kompliziert ist. Wir arbeiten stetig daran, diese technischen Schulden abzubauen und konsequent für alle Ausführungspfade einer Klasse Unit Tests umzusetzen.
Darüber liegen eine grosse Anzahl kaum veränderlicher Integrationstests, welche bestimmte Anforderungen an unser Produkt als Ganzes testen. Da wir sehr hohen Wert auf Rückwärtskompatibilität legen, sind diese Tests, wenn einmal geschrieben, sehr langlebig.
Alle unsere aktuellen Tests laufen sowohl auf dem Build Agent im Rahmen unserer CI/CD (Continuous Delivery / Continuous Deployment) Pipeline als auch lokal auf den Entwicklerrechnern.
Mit Blick auf die Zukunft prüfen wir derzeit die Einführung einer weiteren, noch abstrakteren Stufe automatisierter Tests, deren Ziel es ist, an der äusseren Schnittstelle der Serverkomponenten von Vertec anzuknüpfen. Da Server und Client durch unser Sync Protokoll sehr eng kommunizieren ist das deutlich komplexer als beispielsweise einzelne Endpunkte einer Web API abzutesten.
Was wollen wir mit unseren Tests erreichen?
Die Ziele, die wir mit unserer Teststrategie verfolgen, sind breit gefächert. Natürlich geht es unter anderem darum, Fehler frühzeitig zu finden. Aber vor allem verleihen wir dem Produktivcode mit unseren Tests mehr Kontext. Genauso wie gut designter Code, sagen uns auch sinnvolle Tests etwas über den Zweck einer Implementierung. Sie helfen mir selbst oder meinem Kollegen in zwei Monaten zu verstehen, was ich mit meiner Umsetzung erreichen wollte. Aus diesem Grund verfolgen wir seit einiger Zeit eine Konvention zur Benennung unserer Unit Tests, mit der schon aus dem Namen des Tests ersichtlich wird, was die Voraussetzungen und das erwartete Ergebnis sind. Wird ein Test rot, kann der Entwickler so oft schon aus dem Namen des Tests ablesen, wo das Problem liegt - ohne erst umständlich den gesamten Testcode verstehen zu müssen.
Was ich persönlich am Vorhandensein guter Unit Tests besonders schätze, ist die Freiheit, die ich durch sie beim Refactoring erhalte. Methoden extrahieren, alte for-Schleifen in LINQ-Einzeiler umschreiben, Ketten von if-Statements in switch expressions überführen - ein Tastendruck nach jeder Änderung startet die Tests und zeigt mir, dass sich das Verhalten der Klasse nach aussen nicht geändert hat. So macht die Pfadfinderregel Spass.
Langfristig geht es bei all diesen Aspekten letztlich immer um eines. Qualität. Qualitativer Code ist wartbar, erweiterbar und verständlich und führt zu einem qualitativen Produkt.
Vorne hui, hinten pfui ...
Wer kennt es nicht: Im Produktivcode hält man sich an Prinzipien wie Don't Repeat Yourself, setzt Patterns ein wo es sinnvoll erscheint, legt jeden Namen einer Methode auf die Goldwaage - nur um dann in zehn Testmethoden fast identischen Code zur Erzeugung von Mocks zu schreiben.
Um das zu vermeiden, stellen wir an unseren neuen Testcode die gleichen Qualitätsansprüche wie an unseren Produktivcode. Über das Naming unserer Testmethoden habe ich oben schon geschrieben. Doppelten Code zur Erzeugung von Mocks reduzieren wir durch den Einsatz eines Builder Patterns. Unsere zu testenden Objekte werden von einem Builder erzeugt, der das Setup der Abhängigkeiten kapselt und so in allen Tests für eine Klasse wiederverwendet werden kann. Ausserdem durchlaufen die Unit Tests den gleichen Reviewprozess durch einen zweiten Entwickler wie unser produktiver Code.
Fazit
Automatische Tests sind ein integraler Bestandteil unseres Softwareentwicklungsprozesses, die genauso wie der Produktivcode ständiger Pflege bedürfen und regelmässig hinterfragt werden müssen. Helfen uns die Tests, so wie wir sie derzeit schreiben, noch weiter? Was können wir besser machen?
Steht Codequalität auch für dich an oberster Stelle? Dann schau dir unsere Vakanzen an oder bewirb dich spontan bei uns!