Im Testen macht euch so schnell keiner was vor. Eure Systeme und Geschäftsprozesse sind perfekt abgedeckt. Eure Testautomatisierung ist auf Kurs und performt wie ein Einser. Eure Berichte sind eine Augenweide, glänzen in der Sonne und der Status ist grün wie eine saftige Wiese. Alles ist gut.
Aber dann geht ihr mit Kollegen auf ein Bier. Ihr plaudert über eure tägliche Arbeit. Und zumindest in Wien (Österreich) ist es üblich, fast schon verpflichtend, sich in einem solchen Rahmen zu öffnen, den Arbeitstag zu teilen und ... zu sudern (=raunzen, meckern, sich beschweren). Und dann bemerkt ihr, dass der Weg vor euch doch noch weiter ist als gedacht.
Jeden Morgen müssen fehlgeschlagene Testfälle analysiert und gefiltert werden. Einige (prozentuell wenige, aber in absoluter Zahl dann doch viele) fehlerhafte Tests halten das Team auf Trab – wichtige und kritische Tests, die sich einfach nicht stabilisieren lassen wollen. Änderungen am Produkt und an den Schnittstellen führen dazu, dass Tests plötzlich nicht mehr funktionieren und angepasst werden müssen. Das Team muss immer wieder refactoren und redundante Tests und überflüssigen Code einer geordneten Entsorgung zuführen.
Kurzum, es scheint, dass euch so schnell nicht langweilig wird.
Dieser Artikel führt durch einige Problemstellungen der Testautomatisierung, die euch wahrscheinlich bekannt vorkommen. Aber: Für Probleme gibt es Lösungen. Wenn man die Ärmel aufkrempelt und einfach mal macht. Wir zeigen dazu mögliche Lösungsansätze auf.
Für viele dieser Probleme hat Nagarro bereits ausgereifte und praxiserprobte Lösungen entwickelt (mehr dazu unter "AI4T") – aber darum geht es in diesem Artikel nicht.
„Häferlgucken“: Was köchelt denn da?
Innovation ist wie Kochen – es geht darum, Feedback zu nutzen und sich zu verbessern. Das geht am besten, wenn man keine Angst vor Experimenten hat und mutig neue Dinge ausprobiert.
Wir haben diese Gerichte schon viele Male zubereitet – ihr müsst also nicht bei Null anfangen! Und um das Ganze abzurunden, stellen wir sogar ein Basisrezept zur Verfügung, damit ihr direkt loslegen könnt.
== Rezept #1 ==
Von "Oh Gott – all diese fehlgeschlagenen Testfälle, jeden verdammten Tag..." zu "Wir können uns auf die Fehlerursachen konzentrieren!"
Ihr kommt zur Arbeit (egal ob im Büro oder remote). Ihr habt "Nightly Run Duty". Einige Tests in eurem Umfeld sind fehlgeschlagen.
Schritt 1: Ihr setzt euch hin und beginnt mit der Sichtung. Nach etwa zwei Stunden habt ihr sieben verschiedene Ursachen identifiziert und beginnt endlich daran zu arbeiten.
Schritt 2: Ihr korrigiert Testscripts und Umgebungskonfigurationen und protokolliert die tatsächlichen Probleme.
Wäre es nicht großartig, wenn ihr Schritt 1 überspringen könntet? Nehmen wir an, ihr habt 3000 automatisierte Systemtestfälle. Und etwa 4 % davon schlagen fehl. Das bedeutet 120 fehlgeschlagene Tests, die analysiert werden müssen. Jeden einzelnen Tag. Das ist eine Menge Arbeit.
Aber ihr habt auch eine Menge historischer Daten! Eure automatisierten Tests und die Systeme, die ihr testet, erzeugen Protokolldateien. Ihr wollt die Ursache der Fehler herausfinden, und tut das auch, beinahe jeden Tag – wenn ihr diese Ursachen pro Testfall also irgendwo festhaltet, haben diese Fehler danach ein "Etikett", das aufzeigt, welcher Fehler welche Ursache hatte.
Und das ist glücklicherweise genau das, was man zum Trainieren eines Algorithmus für maschinelles Lernen braucht.
Von nun an erhaltet ihr also morgens ein fehlgeschlagenes Testergebnis und eine (in der Regel kurze) Liste an wahrscheinlichen Ursachen für die einzelnen Fehler. So könnt ihr gleich von Beginn an an den richtigen Stellen nachsehen oder den Fehler einem bestimmten Teammitglied mit besonderen Fähigkeiten zuweisen. Und, wenn ihr eine Fehlerursache behoben habt, könnt ihr alle Tests mit der gleichen Ursache in der Analyse überspringen.
Das ist doch eine verlockende Zukunftsvision, oder?
== Rezept #2 ==
Von "Warum werden hier laufend Dinge geändert?" zu "Unsere Automatisierung ist immun".
Systeme ändern sich in einer agilen Welt (und sogar in einer "klassischen" Welt). Sehr oft. Sowohl die Benutzeroberfläche als auch das Backend. Tester verbringen ihre Zeit hauptsächlich mit der Wartung von Tests oder ihrer Anpassung, damit sie weiterhin zum zu testenden Produkt passen.
Stellt euch also vor, euer Automatisierungsframework könnte, wenn eine Interaktion mit dem zu testenden System fehlschlägt, automatisch prüfen was los ist. Und zwar, ob es sich um eine wahrscheinlich beabsichtigte Änderung handelt – und es könnte in diesem Fall den Test sogar automatisch korrigieren!
Nehmen wir an, eine Schaltfläche "OK" in eurem Produkt heißt ab sofort "Bestätigen". Ein Mensch würde diese Änderung wahrscheinlich zur Kenntnis nehmen, vielleicht irgendwo einen Vermerk eintragen, sie aber weitgehend ignorieren. Höchstwahrscheinlich würde er den Testfall aber nicht als „fehlgeschlagen“ markieren. Aber eure Testautomatisierung wird hier möglicherweise darüber stolpern, aufgeben und den Test fehlschlagen lassen.
Und das zieht einen Rattenschwanz nach sich:
* Analysieren des fehlgeschlagenen Testfalls
* Manuelle Validierung der Änderung (Einloggen, Navigieren usw.)
* Suche nach den entsprechenden Automatisierungskennungen
* Ändern der Kennungen auf die neuen Werte
* Übertragen dieser Änderungen
* Erneutes Ausführen des Tests
Das alles kann locker 15 Minuten in Anspruch nehmen. Nur für eine triviale Wort-Substitution. Das ist frustrierend. Wir wollen zwar über diese Änderung Bescheid wissen, aber wir wollen den Test nicht abbrechen. Stellt euch vor, diese Änderung befindet sich an einer Schlüsselposition eurer Testsuite – sie könnte möglicherweise Hunderte von Tests an der Ausführung hindern!
Wenn euer Framework nun eine Möglichkeit hat zu erkennen, dass "OK" und "Bestätigen" Synonyme sind, kann es den Test fortsetzen, anstatt einfach abzubrechen. Dazu sind noch einige andere technische Umstände zu validieren, um sicher zu sein, dass "Bestätigen" dieselbe Schaltfläche ist, die früher "OK" war.
Es ist sogar möglich die Kennung automatisch zu aktualisieren, wenn sich das Framework sicher ist. Natürlich wird der Entwickler über diese Änderung informiert, aber einen kurzen Blick auf die Änderungen zu werfen und zu entscheiden, ob die Änderung valide ist oder nicht, ist deutlich schneller als der oben beschriebene Workflow.
== Rezept # 3 ==
Von "KI im Testen? Das heißt große Datenmengen, richtig?” zu "Wir haben bereits alle Daten die wir brauchen".
Maschinelles Lernen erfordert Tausende von Datenpunkten, richtig? Vielleicht sogar Millionen?
Ja und nein. Nehmen wir unser Beispiel der ersten Delikatesse – das Ermitteln der Ursachen für fehlgeschlagene Testfälle anhand von Log-Files. Da Log-Files recht klar aufgebaut und in ihrer Form recht deterministisch sind, sind sie für einen ML-Algorithmus "leicht" zu erlernen – gegenüber der Komplexität der „menschlich“ gesprochenen oder geschriebenen Sprache sind die Strukturen hier deutlich einfacher gestrickt.
Dies bedeutet, dass wir ML-Algorithmen verwenden können, die ebenfalls auf der einfacheren Seite (relativ gesehen) sind. Das bedeutet aber auch, dass wir nicht so viele Daten benötigen, wie es bei vielen anderen Anwendungsfällen notwendig wäre.
Anstelle von Zehntausenden von fehlgeschlagenen Testfällen, die mit den Grundursachen gekennzeichnet sind, können wir mit nur etwa 200 Fällen einen guten Startpunkt erreichen. Wir müssen zwar sicherstellen, dass die gewählten Fälle die meisten der betrachteten Ursachen abdecken, aber das ist viel weniger Arbeit, als man erwarten würde.
Hinzu kommt, dass bei der Testautomatisierung von vornherein bereits eine Menge Daten anfallen (Ausführungsberichte, Log-Files der automatisierten Tests, Log-Files der zu testenden Systeme, Infrastruktur-Logs und vieles mehr) – das bedeutet, dass wir bereits einen riesigen Datenpool vor uns haben. Und dabei haben wir die Logs aus der Produktion noch gar nicht berücksichtigt.
Aus all diesen Daten lassen sich viele wichtige Erkenntnisse gewinnen – bisher oft ungenutztes Potenzial. Werft also am besten einen Blick auf eure Probleme, schaut euch die Daten an, die ihr habt, und seid kreativ! Es gilt einen verborgenen Schatz inmitten des Ozeans von Informationen da draußen zu heben.
== Rezept #4 ==
Von "Synthetische Daten machen zu viel Arbeit, und Produktionsdaten sind schwer einzuschätzen und bergen rechtliche Probleme" zu "Wir kennen die Muster der Produktionsdaten und wenden diese an".
Viele Unternehmen kämpfen mit Testdaten. Einerseits werden synthetische Testdaten entweder in Blöcken generiert, was sie eher "langweilig" macht, oder sie werden mit viel Aufwand manuell erstellt.
Andererseits können Produktionsdaten wiederum auf viele andere Arten Kopfzerbrechen bereiten – sie sind nicht nur eine rechtlich relevante Geschichte (echte Anonymisierung ist leider keine leichte Aufgabe), sondern ein bisschen eine geschlossene Wundertüte, von der man nie weiß, was sie wirklich enthält.
Wie wäre es also, wenn ihr eure Testdaten nicht anonymisiert, sondern sie dazu verwendet, vollständig synthetische Datensätze zu replizieren? Daten, welche dieselben Eigenschaften wie eure Produktionsdaten aufweisen und gleichzeitig Kombinationen hinzufügen, um zusätzliche Konstellationen abzudecken?
Im Idealfall verwendet ihr hier eine " Explainable AI" – in dem Sinne, dass sie Datentypen, Strukturen und Muster in den Daten aus der Produktion lernt, aber gegenüber klassischem Machine Learning eine Besonderheit aufweist: Anstatt für sie "blind" neue Daten zu generieren, stellt sie ein für euch lesbares Regelmodell dieser Daten bereit. Mit diesem Modell könnt ihr dann so viele Testdaten generieren, wie ihr braucht – vollständig synthetisch, aber mit allen relevanten Eigenschaften, Strukturen und Beziehungen aus euren Produktionsdaten. Das Modell kann auch entlang eurer Testziele verfeinert und weiter ausgearbeitet werden, um die von euch benötigte Abdeckung sicherzustellen. Darüber hinaus können gelernte Regeln korrigiert, unnötige Regeln entfernt und neue Regeln hinzugefügt werden.
Jetzt könnet ihr nach Herzenslust nützliche Daten generieren!
== Rezept #5 ==
Von "Was tut unsere Testautomatisierungs-Suite eigentlich?" zu "Ich habe unsere Portfolio-Struktur komplett im Blick".
Testabdeckung auf höheren Testebenen zu berechnen, bereitet immer Kopfzerbrechen. Codeabdeckung sagt auf dieser Ebene oft nichts Nützliches aus. Traceability zu Anforderungen und User-Stories ist zwar schön, aber auch auf diesem Level bekommt man keinen echten Überblick darüber, was das gesamte Portfolio tatsächlich tut.
Um diesen Überblick zu bekommen, müsst ihr euch nicht nur durch die (hoffentlich sauber und klar strukturierte) Ordnerstruktur wühlen, sondern auch Unmengen von Textdateien lesen, sei es Automatisierungs-Code oder andere Formate wie Gherkin Feature-Files.
Jeder Testfall in einem schlüsselwort- oder verhaltensgesteuerten Testautomatisierungskontext besteht aus einer Reihe von Testschritten auf einer definierten Schicht, die eine Aktion repräsentieren. Egal ob ein Page-Object-Model-basiertes Konzept oder ein API-basierter Ansatz verfolgt wird – so oder so, wenn eure Automatisierung entsprechend einem der typischen Automatisierungsdesigns strukturiert ist und eine Abstraktion der geschäftsrelevanten Aktionen aufweist, bestehen die Testfälle im Wesentlichen aus einer Reihe dieser Aktionen und dazu passenden Validierungen.
Betrachten wir nun die Testfälle als einen "Pfad" durch euer System, wobei jede Aktion einen Schritt oder einen "Knotenpunkt" auf dem Weg darstellt. Wenn wir diese Pfade für alle eure Testfälle überlagern, ergibt das einen Graphen. Dieser Graph hat jetzt die "Form" eures Testautomatisierungsportfolios! Ein kurzer Blick genügt, um eine Vorstellung davon zu bekommen, was das Portfolio so tut und abdeckt.
Jetzt können wir weitere Informationen hinzufügen: Welche Schritte sind bei der letzten Ausführung fehlgeschlagen (problematische Farbknoten in Rot)? Wie oft wird eine bestimmte Aktion während eines Testlaufs ausgeführt (größere Schrift & dickere Linien)?
Diese Graphen und die dazugehörigen visuellen Darstellungen werden recht schnell recht groß. Aber der Mensch ist erstaunlich gut darin, sie zu interpretieren und Auffälligkeiten zu erkennen. So könnt ihr euch schnell einen Überblick verschaffen und Redundanzen, Lücken und andere nützliche Dinge entdecken.
== Rezept #6 ==
Von "Wir lassen jedes Mal alles neu laufen, nur um auf Nummer sicher zu gehen" zu "Wir wählen den richtigen Test, für jede Änderung, und das jedes Mal".
Große automatisierte Testportfolios können stundenlang, wenn nicht sogar tagelang laufen. Parallelisierung und Optimierung können euch schon sehr weit bringen. Aber manchmal (immer?) sind die Ressourcen begrenzt – sei es bei Systemen, Daten oder sogar Hardware. Es wird sehr schwierig, wenn nicht sogar unmöglich, alle Tests jedes Mal durchzuführen. Also erstellt man typischerweise einen reduzierten Satz von Regressionstests oder sogar einen Satz von Smoke-Tests, um schnelle Ergebnisse zu erzielen. Aber jede Änderung ist anders. Ein reduzierter Testsatz könnte für diese eine Änderung sehr kritische Bereiche ignorieren!
Wie wählt ihr also Tests aus? Wie deckt ihr in der aktuellen Situation das größte Risiko in der kürzesten Zeit ab?
Ähnlich wie bei der Zubereitung eines guten Gerichts gibt es viele Aspekte, die in eine solche Auswahl einfließen:
- Was hat sich an dem Produkt verändert?
- Wie viele Personen haben welche Teile des Codes in welchem Zeitraum verändert?
- Welche Tests sind sehr gut darin, echte Probleme aufzudecken?
- Welche Tests sind in letzter Zeit durchgeführt worden?
- Wie lange dauert dieser Test?
Auch hier werdet ihr feststellen, dass die Antworten für die meisten dieser Fragen bereits in euren Daten vorhanden ist – in eurem Versionsverwaltungssystem, in euren Testberichten und so weiter. Wir müssen zwar noch definieren, was "Risiko" für eure spezielle Situation bedeutet (eine sehr, sehr wichtige Diskussion!), aber es ist recht wahrscheinlich, dass ihr die meisten Daten bereits habt, um eure Tests auf Basis dieser Informationen und eurer Risikodefinition einzustufen. Es gilt also, diese Diskussion zu führen und eine "intelligente Testfallauswahl" in eurer eigenen Umgebung zu implementieren.
== Rezept #7 ==
Von "Niemand wagt es Legacy Code anzugreifen" zu "Wir kennen die Risiken, die unser Code birgt".
Um gleich beim Thema "Risiko" zu bleiben – es ist uns noch etwas anderes aufgefallen:
Nachdem erfahrene Developer ausreichend Zeit mit einer Codebasis verbracht haben, wissen sie sehr gut, welche Codeänderung wie gefährlich sein kann.
Aber es gibt immer wieder systemkritischen Code, der von einem Kollegen geschrieben wurde, der vor vielen Jahren das Unternehmen verlassen hat, und/oder Code der viele Jahre alt ist. Es gibt Code von externen Partnern, die nicht mehr im Unternehmen aktiv sind. Es gibt große, gemeinsam genutzte Code-Basen, an denen vertikal aufgestellte Teams arbeiten, ohne dass jemand einen vollständigen Überblick über das große Ganze hat. Und es gibt neuere, vielleicht jüngere, weniger erfahrene Kollegen, die dazustoßen. Und wir dürfen auch nicht unter den Tisch fallen lassen, dass selbst erfahrene Leute Fehler machen. Wir gehen davon aus, dass jeder von uns solche Situationen gut kennt.
Es gibt viele Methoden, um diese Dinge abzufedern. Einige Beispiele sind Code-Qualitätsmessungen, Code-Abdeckungsmessungen, Versionierungssysteme und so weiter. Diese Tools und Metriken sagen euch, was zu tun ist, was zu korrigieren und zu verändern ist. Aber das passiert nicht immer "unmittelbar". Stellt euch vor, ihr ändert eine Codezeile – in der Regel schaut man nicht jedes Mal auf all diese Dinge und geht die gesamte Historie dieser einen Codezeile durch.
Wie wäre es also mit einem System, das all diese Datenpunkte integriert:
- Wird diese Zeile von Tests erfasst?
- Wie oft wurde sie in den letzten zwei Tagen von wie vielen Personen geändert?
- Wie komplex ist sie?
- Wie viele andere Teile des Systems hängen von ihr ab?
Wir können auch Expertenmeinungen einbeziehen, indem wir z. B. Kommentare im Code eingeben. Dann verwenden wir all diese Informationen, um einen "Code-Risiko-Indikator" zu erstellen und diesen neben der Klasse/Methode/Codezeile anzuzeigen. "0 – Go for it! Geringes Risiko". "10 – Denk‘ nochmal drüber nach und hol‘ dir ein zweites Paar Augen dazu". Wenn ihr draufklickt, werden euch die Gründe für diese Bewertung direkt in eurer IDE angezeigt.
Der Sinn dahinter ist in der ersten Linie nicht, das Risiko sofort zu beheben, obwohl der Indikator auch dafür verwendet werden kann. Das Hauptziel ist vor allem, Entwicklern ein Gefühl für das Risiko ihrer Änderungen zu vermitteln, bevor sie sie machen.
== Rezept #8 ==
Von "Model-basiert? Dafür haben wir keine Zeit!" zu "Modelle entstehen von selbst – und helfen uns".
Modellbasiertes Testen ist seit vielen Jahren ein Thema. Aber es scheint, dass dieses Thema nie wirklich "durchgestartet" ist. Ein Grund dafür könnte die Komplexität der notwendigen Modelle sein, verbunden mit der Tatsache, dass diese Modelle von Experten erstellt und gepflegt werden müssen. Abgesehen davon sind diese Modelle zwar sehr gut in der Lage, viele Tests zu generieren, aber sie tun sich in der Regel schwer, das konkrete Sollverhalten des getesteten Systems in einer ausreichenden Schärfe abzuleiten.
In den meisten Fällen wird modellbasiertes Testen also nicht konsequent angewandt, obwohl es viel Potenzial hat.
Wie können wir also diese Probleme entschärfen? Durch die automatische Generierung eines geeigneten Modells aus den bereits verfügbaren Informationen. Hierfür gibt es zahlreiche Methoden und Quellen, wie z.B. die Analyse von Anforderungsdokumenten, die Analyse von Code, Abhängigkeiten zwischen Systemen und Codebausteinen, und so weiter. Die Kehrseite dieser Methoden ist, dass das daraus entstehende Modell nicht "unabhängig" von diesen Quellen ist, sondern lediglich eine Variation der bereits bestehenden Informationen darstellt.
Das heißt aber nicht, dass sie nicht nützlich sein können.
Denkt an unser Rezept #5 zurück – die Erstellung eines Graphen aus unserer automatisierten Testsuite. Dabei handelt es sich um ein Zustandsübergangsmodell unserer Tests und damit auch des Produkts, das wir testen (denn die Struktur unserer Tests spiegelt den Workflow des Produkts wider). Und dieser Graph hat Potenzial.
Wir könnten beispielsweise eine Reihe miteinander verbundener Knoten markieren, um das System Testautomatisierungscode generieren zu lassen, der diese Testschritte nacheinander ausführt. Dieser Code kann dann recht rasch manuell zu einem kompletten Testfall vervollständigt werden. Oder wir könnten das System fragen: "Gibt es einen Testfall, der diese beiden Knoten verbindet?", um unsere Abdeckung zu erhöhen, oder "Gibt es zwei Tests, die beide diesen Pfad abdecken?", um redundante Tests zu entfernen.
Da dieses Modell nicht manuell erstellt wird und die Grundlage, aus der es generiert wird (automatisierte Tests), ohnehin mit unserem Produkt synchron gehalten wird, müssen wir auch keine zusätzliche Zeit für die Pflege dieses Modells aufwenden. Und es hat jede Menge Anwendungsfälle.
== Unser traditionelles Rezept ==
Die Zutaten:
* 5 lästige Alltagsaufgaben und nervige Dinge
* Ein Kühlschrank voller Daten, die ihr bereits auf Vorrat habt
* 1 daumengroßes Stück Wissen über Machine Learning
* 20g Kreativität
Die Koch-Anleitung:
* Gedanken fest durchrühren und über die nervigen Dinge raunzen
* Kreativität beigeben, gut mischen
* Wissen über ML aus der Verpackung nehmen
* Der kreativen Mischung hinzufügen, vorsichtig die Daten unterheben
* Ofen bei 200°C, für die Dauer eines MVP backen
* Während des Backens wiederholt den Fortschritt und den tatsächlichen Wert überprüfen
* Aus dem Ofen nehmen, ein wenig ruhen lassen
* Heiß servieren – am besten mit einer Prise praktischer Erfahrung garniert
Viel Spaß und guten Appetit bei diesem Gericht!
Wenn ihr Fragen zur Testautomatisierung und zur Verbesserung eures Softwaretestprozesses habt, zögert bitte nicht, uns unter aqt@nagarro.com zu kontaktieren.
Ladet hier eure kostenlose Infografik mit allen 8 Rezepten herunter!
Testing, Software Testing, Accelerated Quality and Test Engineering, AQT, Optimize, Innovate
Thomas Steirer
Testing, Software Testing, Accelerated Quality and Test Engineering, AQT, Optimize, Innovate