Fünf Gründe um mit Continuous Testing zu scheitern
Continuous Integration ist eine etablierte Technik in Software-Projekten, um kontinuierlich die Änderungen, die von verschiedenen Entwicklern vorgenommen werden, zusammenzuführen. Der Vorgang kann in den Kompilierungsvorgang und das Testen gegliedert werden. Dieser Artikel beschäftigt sich mit den Herausforderungen, die im Bereich des Testens in großen Projekten auftreten können.
In diesen Projekten läuft ein großer Teil der Tests automatisiert ab. Techniken wie das Test Driven Development, die nachweislich einen positiven Einfluss auf den Projektverlauf haben (vgl. [1]), werden angewendet. Die Testmenge, die zur Ausführung vorhanden ist, wächst aufgrund solcher Methoden kontinuierlich an. Idealerweise würde man alle automatisierten Tests, die vorhanden sind, mit jeder Änderung am Source-Code ausführen. So würde sichergestellt, dass bereits implementierte Funktionalität nicht durch Änderungen gebrochen wird. Hier auftretende neue Herausforderungen sind in der Fachliteratur zu Continuous Integration jedoch nicht berücksichtigt. Anfangs können alle Tests mit jeder Änderung durchlaufen werden. Allerdings wächst die Ausführungszeit für die Testsuite stetig an, bis zu Laufzeiten von mehreren Stunden. Dies führt zu viel zu hohen Rückmeldungszeiten für die Entwickler. Folgende fünf Faktoren können dazu führen, mit Continuous Testing zu scheitern.
Testlevel nicht richtig angewandt
Idealerweise ergibt die Verteilung der Tests auf verschiedenen Ebenen eine Pyramide aufsteigend von Unit-, zu Komponenten-, zu Integrations-, zu System-Tests (vgl. [2] und [3]). In vielen Projekten findet man jedoch eine inverse Pyramide (auch Test-Eiswaffel genannt (vgl. [4])) bei der die Mehrzahl der Tests sich auf einem sehr hohen Level, wie dem Integrations- oder System-Level, befinden. Schlimmstenfalls sind alle Tests auf dem UI-basierend oder manuell, wobei diese Tests jedoch den kleinsten Teil der Testsuite ausmachen sollten. Wieso hat das eine negative Auswirkung? Tests auf höheren Ebenen haben einige oder alle der folgenden negativen Merkmale:
- Hohe Laufzeit
- Hoher Analyseaufwand im Fehlerfall
- Hoher Wartungsbedarf (vor allem bei UI-Tests)
- Höhere Instabilität bei der Testausführung
Andererseits ist es nicht sinnvoll, wenn eine Testsuite nur aus Unit Tests besteht. Diese testen zwar sehr schnell, ob eine Änderung in einem lokalen Bereich die bestehende Funktionalität gebrochen hat, nicht aber, ob die einzelnen Teile der Software korrekt zusammenarbeiten. Deswegen ist es essenziell, die Anzahl der Tests auf den jeweiligen Ebenen in die Form einer Pyramide zu bringen. Jede dieser Ebenen deckt einen eigenen Aspekt ab, der getestet werden sollte, von der Funktionsfähigkeit einzelner Klassen bis zu der des gesamten Systems. Es wird zudem getestet, ob das System auch den nicht-funktionalen Anforderungen genügt.
Tests werden nicht kontinuierlich verbessert
Unit-Tests werden idealerweise kontinuierlich zusammen mit dem Produkt-Code angepasst und verbessert. Daher ist das Altern des Codes zumeist kein Problem. Sobald man sich aber den System-Level-Tests nähert, sieht man immer häufiger das Phänomen, dass der Test-Code nicht mehr betrachtet wird, sobald er der Source-Code-Verwaltung hinzugefügt wurde. Aber gerade auf den höheren Testebenen kann das mit der Zeit zu Problemen führen. Testsuiten, die zunächst in einigen Sekunden durchgelaufen sind, dauern auf einmal Minuten bis hin zu Stunden.
Zusätzlich kann sich in den Tests auch technische Schuld ansammeln, wie duplizierter Code oder mangelnde Refaktorisierungen. Dies führt dazu, dass die Testsuite immer schlechter zu warten ist. Man gerät dann in die Situation, dass es aufwändiger ist, die Tests anzupassen, als das Produkt zu implementieren. Deshalb muss fortlaufend an der Qualität der automatisierten Tests gearbeitet werden. Man kann beispielsweise in jeder Iteration die fünf langsamsten Tests analysieren, um herauszufinden, ob man sie beschleunigen kann.
Instabile Tests werden nicht gesondert behandelt
In jedem Projekt kommen Tests vor, die ein nicht deterministisches Ergebnis liefern. Der automatisch laufende Buildvorgang ist aufgrund dessen sporadisch grün oder rot. Damit schwindet das Vertrauen in das Continuous Integration System.
Dieses Problem darf nicht ignoriert werden. Betrachtet man die „Broken Window Theory“ (vgl. [5]) die besagt, dass man beginnt, etwas zu vernachlässigen, sobald die ersten Mängel daran auftauchen, führt das dazu, dass man anfängt die Ergebnisse des Builds zu ignorieren. Aussagen wie „Ich bin fertig, wenn 95 Prozent meiner Tests grün sind“ oder wie „Diese fünf Tests schlagen sporadisch fehl, aber alles funktioniert“ sind das Resultat. Aber was ist, wenn es einmal 94 Prozent statt 95 Prozent sind? Oder wenn anstatt fünf auf einmal sechs Tests fehlschlagen? Oder fünf komplett andere schlagen fehl und keiner bemerkt es, weil man sich nur auf die Anzahl der fehlschlagenden Tests konzentriert?
Man beginnt, etwas zu vernachlässigen, sobald die ersten Mängel daran auftauchen
In einem System, welches kontinuierlich automatisiert Tests ausführt, sollten deshalb instabile Tests gesondert mit einem eigenen Prozess behandelt werden. Martin Fowler hat dafür den Begriff der „Quarantäne“ (vgl. [6]) benutzt. Im Prinzip geht es darum, dass ein sporadisch fehlschlagender Test nicht in der normalen Testsuite enthalten sein sollte. Ähnlich einer hochansteckenden Erkrankung, bei der die infizierten Personen in einer Quarantäne-Einrichtung untergebracht werden müssen, sollte man auch diese Tests aus der normalen Ausführung entfernen, um zu verhindern, dass die Ergebnisse des Builds ignoriert werden. Die Tests müssen natürlich untersucht und anschließend entweder das Produkt repariert oder der Test korrigiert werden. Wenn das Problem nicht innerhalb weniger Stunden gelöst werden kann, sollte dies außerhalb des normalen Build-Prozesses erfolgen, um zu vermeiden, dass dieser nicht einen positiven Ausgang hat. Mit Einführung der Test-Quarantäne sollte zugleich überwacht werden, wie viele Tests in die Quarantäne verschoben wurden, um zu vermeiden, dass sich plötzlich keine Tests mehr in der normalen Testausführung befinden.
Werkzeuge sind zu komplex angelegt
Ein nächster möglicher Fehler ist, dass die Werkzeuge, welche für die Continuous Integration und das Continuous Testing benutzt werden, zu komplex angelegt sind. Sie sollten eine möglichst einfache Handhabung bieten, da sie von fast allen Entwicklern und Testern mehrmals am Tag benutzt werden. Das heißt jedoch auch, dass man die Werkzeuge nicht getrennt betrachten kann für die Bereiche „Kompilieren“ und „Test“. Diese Aspekte müssen Hand in Hand gehen, um das tägliche Arbeiten für alle Beteiligten möglichst einfach zu gestalten. Es sollte klar definiert sein, was von einem Test hinsichtlich seines Verhaltens erwartet werden kann und was wiederum Aufgabe der Infrastruktur sein sollte. So kann man gezielt entscheiden, welche Maßnahmen mittels der Werkzeuge zu treffen sind und was in der Verantwortung der Tests liegt. Typische Beispiele hierfür sind Konfigurationsänderungen, die ein Test vornimmt, um ausgeführt werden zu können. Der Test ist dafür verantwortlich, diese wieder zurückzusetzen (auch im Fehlerfall).
Keine Test-Architektur
In vielen Projekten wird die Test-Code-Entwicklung immer noch nicht als Software-Entwicklung angesehen. In komplexen Projekten jedoch, wie sie heutzutage entwickelt werden, ist auch die Test-Code-Entwicklung eine große Herausforderung und muss mit denselben Software-Entwicklungsmethoden angegangen werden wie die Produktentwicklung. Ist die Qualität des Produkt-Codes schlecht, bedarf es einer automatisierten Testsuite mit guter Qualität. So kann man nach und nach die Qualität des Produkt-Codes erhöhen und sich auf das Sicherheitsnetz der qualitativ hochwertigen Tests verlassen.
Wenn jedoch auch die Test-Code-Qualität nicht gut ist, wird die Behebung von Fehlern, die im Feld gefunden werden, viel Zeit kosten. So kommt es, dass ein hoher Teil der Kosten nach der eigentlichen Entwicklung, also erst in der Wartungsphase, aufläuft. Deswegen ist es wichtig, für den Test-Code eine Architektur mit klaren Regeln aufzusetzen und die Qualität des implementierten Codes zu messen. Im besten Falle ist die Architektur des Test-Codes in gleicher Weise beschrieben, wie die Architektur des Produkt-Codes, um zu gewährleisten, dass alle Projektbeteiligten diese Dokumentation auch in derselben Weise lesen können.
Fazit
Continuous Testing in großen Projekten ist nicht unmöglich. Aber es ist eine Herausforderung. Mit den oben genannten fünf Punkten wurde ein großer Teil der möglichen Schwierigkeiten behandelt. Dennoch sollte man die Herausforderung annehmen, da Continuous Testing die Entwicklung einer Software spürbar vereinfacht und die Angst vor Änderungen in einem bestehenden System reduzieren kann. Eine Investition in die Zukunft.
[1] msr-waypoint: Test Driven Development
[2] Martin Fowler: Test-Pyramide
[3] namics.com: Test-Pyramide
[4] watirmelon.com: Test-Eiswaffel
[5] Wikipedia: Broken-Windows-Theorie
[6] Martin Fowler: Test-Quarantäne