In unserer fortlaufenden Reihe DevOps for the Win haben wir unseren Weg der DevOps-Transformation dokumentiert. In Kapitel 1 haben wir das DevOps-Dreieck vorgestellt: Tools, Architektur und Menschen in der Produktentwicklung, in dem wir die grundlegenden Elemente besprochen haben, die den DevOps-Erfolg antreiben. In Kapitel 2 haben wir die ersten Schritte unserer DevOps-Transformation untersucht und gezeigt, wie wir den Grundstein für sinnvolle Veränderungen gelegt haben.
In Kapitel 3 widmen wir uns nun der nächsten großen Herausforderung, auf die wir gestoßen sind: Design für Testbarkeit. Nachdem wir unsere Teamstruktur verbessert, unsere Definition of Done präzisiert und eine automatisierte Code-Analyse eingeführt hatten, wurde uns klar, dass der Hauptengpass in unserer Pipeline zum Software-Design – genauer gesagt zur Testbarkeit – geworden war. Die Arbeit in der Testphase staute sich an, was uns daran hinderte, schneller neue Versionen zu veröffentlichen. Dieses Kapitel befasst sich eingehend damit, wie wir diese Herausforderung gemeistert und unsere Fähigkeit verbessert haben, qualitativ hochwertige Software effizient bereitzustellen.
Der Testengpass
Der Hauptgrund für diese Verzögerung war, dass das Testen überwiegend manuell erfolgte und sich auf UI-Tests sowie Benutzer-Workflows konzentrierte. Da die Benutzeroberfläche in der Regel als Letztes fertiggestellt wurde, gab es kaum oder keine Automatisierung, und manuelle Tests waren der einzige gangbare Weg. Automatisierte UI-Testfälle (z. B. Selenium-Tests) wurden meist nach Abschluss der manuellen Tests geschrieben, hauptsächlich zu Regressionszwecken.
Diese Abhängigkeit von manuellen Tests führte zu:
- Erheblichen Verzögerungen bei Software-Releases.
- Die Notwendigkeit mehrerer Bereitstellungen, um parallele Tests durch mehr als ein QA-Teammitglied zu ermöglichen.
Dies unterstrich, wie wichtig es ist, den Code testbarer zu machen, um den Wertfluss zu optimieren. Dies erwies sich jedoch als schwieriger als erwartet.
Priorisierung von API- und Unit-Tests
Um diese Herausforderung zu bewältigen, konzentrierten wir uns auf zwei Hauptbereiche:
- Unit-Tests – Sicherstellen, dass einzelne Komponenten isoliert getestet werden können.
- API-Tests – Verringerung der Abhängigkeit von UI-basierten Tests und Datenbankabhängigkeiten.
API-Tests: Verlagerung nach links (Shifting Left) für schnelleres Feedback
Wir haben unsere API-Teststrategie mit folgenden wichtigen Verbesserungen neu definiert:
- Klar definierte APIs – APIs wurden so konzipiert, dass sie einfach und gut dokumentiert sind und früh in der Entwicklung zur Verfügung stehen.
- Verzicht auf die Datenbank als Integrationspunkt – Die Verwendung von Datenbanken für die Integration schuf Abhängigkeiten, die das Testen verlangsamten. Stattdessen wandten wir uns der API-basierten Integration unter Verwendung von REST über HTTP und GraphQL zu. Dies minimierte den Einrichtungsaufwand für Datenbanken und verbesserte die Testautomatisierung.
Diese Verlagerung reduzierte Verzögerungen erheblich, ermöglichte eine schnellere Automatisierung und Tests in einem frühen Stadium und zeigte, dass die Konzentration auf API-First-Designs sowohl die Testbarkeit als auch die Effizienz steigerte.
Unit-Testing: Ein Umdenken
Anfangs führten unsere Bemühungen im Bereich Unit-Tests zu einer raschen Erhöhung der Testabdeckung, doch wir stellten schnell Probleme fest:
- Einige Tests waren oberflächlich und wurden nur geschrieben, um die Ziele für die Codeabdeckung zu erreichen.
- Es fehlte ihnen an einer sinnvollen Validierung der Funktionalität, von Randfällen und Fehlerszenarien.
Um dem entgegenzuwirken, legten wir den Schwerpunkt auf:
- Die Schulung von Entwicklern beim Schreiben wertvoller Tests.
- Die Refarktorisierung von Code zur Verbesserung der Testbarkeit.
Herausforderungen beim Schreiben testbaren Codes
Das größte Hindernis für effektive Unit-Tests war die mangelnde Testbarkeit der Codebasis selbst. Zu den Hauptproblemen gehörten:
- Große, monolithische Funktionen.
- Eng gekoppelte Komponenten.
- Mangelhafte Trennung von Funktionsbereichen.
- Unzureichende Abstraktion.
Diese Probleme erschwerten es, einzelne Einheiten effektiv zu isolieren und zu testen. Um dem entgegenzuwirken, haben wir Folgendes getan:
- Bereitgestellte Richtlinien: Wir haben Best Practices zu folgenden Themen geteilt:
- Auswahl von Funktionen für das Unit-Testing.
- Die Refarktorisierung von Code zur Verbesserung der Testbarkeit.
- Entwurf und Einsatz von Mocks zur Erleichterung von Tests.
- Fokus auf schrittweise Verbesserungen: Entwickler wurden ermutigt, kleine, sinnvolle Änderungen vorzunehmen, um die Testbarkeit im Laufe der Zeit zu verbessern.
Langsame, aber stetige Fortschritte
Trotz dieser Bemühungen verlief der Fortschritt langsam, insbesondere bei älteren Produkten. Die bestehende Architektur schränkte die Anzahl der Unit-Tests ein, die wir schreiben konnten. Diese Maßnahmen dienten jedoch nicht nur der Verbesserung des aktuellen Codes, sondern waren eine Investition in die Zukunft:
- Die Stärkung der Fähigkeiten von Entwicklern beim Entwurf testbaren Codes stellte sicher, dass zukünftige Projekte nicht unter denselben Problemen leiden würden.
- Schrittweise Verbesserungen verhinderten Störungen und erhöhten gleichzeitig kontinuierlich die Testautomatisierungsabdeckung.
- Ein Umdenken half den Teams, die Testbarkeit nicht als Last, sondern als Notwendigkeit für einen nachhaltigen DevOps-Erfolg zu sehen.
Für Teams und Unternehmen, die eine DevOps-Transformation beginnen, ist die Testbarkeit ein kritischer Schwerpunktbereich. Sie hilft dabei, Engpässe in der Entwicklungsphase zu verringern. Es ist jedoch wichtig, die Erwartungen zu steuern – sofortige Ergebnisse sind möglicherweise nicht erzielbar, insbesondere im Umgang mit großen, gewachsenen Codebasen. Die Refaktorisierung solchen Codes ohne ausreichende Unit- und Regressionstests stellt eine große Herausforderung dar.
Lektionen aus dem Buch „The Pragmatic Programmer“
Für Szenarien, in denen der Start mit einer neuen Codebasis nicht machbar ist, bieten Andy Hunt und David Thomas in The Pragmatic Programmer: Your Journey to Mastery praxisnahe Orientierung:
- Auf schrittweise Änderungen abzielen: Konzentrieren Sie sich auf die Refaktorisierung kleiner, überschaubarer Teile der Codebasis.
- Komponenten entkoppeln: Reduzieren Sie Abhängigkeiten, um einzelne Einheiten leichter testbar zu machen.
- Monolithische Funktionen aufteilen: Unterteilen Sie große Funktionen in kleinere, gezieltere Einheiten.
- Modulares Design einführen: Machen Sie den Code durch verbesserte Modularität testbarer und wartbarer.
Die Botschaft hierbei ist klar: Sobald die ersten Engpässe behoben sind, sollte die Testbarkeit zu einem zentralen Schwerpunkt werden. Dies verringert Einschränkungen in der Testphase und ermöglicht eine schnellere Bereitstellung hochwertiger Software.
Ausblick
Unser Weg zur Verbesserung der Testbarkeit hat uns wertvolle Lehren über die Rolle des Software-Designs bei der Ermöglichung reibungsloser DevOps-Transformationen gelehrt. Durch die Fokussierung auf Testbarkeit haben wir Engpässe beseitigt, die Fähigkeiten der Entwickler verbessert und den Grundstein für den zukünftigen Erfolg gelegt. Auch wenn die unmittelbaren Ergebnisse langsam sein mögen, machen die langfristigen Vorteile in Bezug auf Qualität, Effizienz und Teamwachstum diese Investition lohnenswert.
Während wir unseren DevOps-Weg fortsetzen, wird die Erfolgsmessung zur nächsten großen Herausforderung. In Kapitel 4 beleuchten wir, wie wir KPIs und Dashboards eingeführt haben, um unsere Fortschritte zu verfolgen und weitere Verbesserungspotenziale zu identifizieren.
Seien Sie gespannt, wenn wir uns ansehen, wie datengestützte Entscheidungsfindung uns geholfen hat, unsere DevOps-Prozesse zu verfeinern und die Leistung zu optimieren!
