Konrad Schreiber, Bartosz Boron, und Maurice Wendt

Voraussichtliche Lesedauer: 10 Minuten

Techblog

Fokus auf das Produktivsystem

Testen, worauf es wirklich ankommt Moderne Softwaresysteme funktionieren unter anderem deshalb so gut, weil während der Entwicklung systematisch und automatisiert getestet wird. Spätestens nach diesem Satz ist klar, warum Qualitätssicherung ein wichtiges Thema für die Softwareentwicklung ist. In der Testpyramide stellen ausgiebige Unit-Tests die Basis dar, dann werden technische Komponenten in Komponenten-, Integrations- und Systemtests…

Techblog

Testen, worauf es wirklich ankommt

Moderne Softwaresysteme funktionieren unter anderem deshalb so gut, weil während der Entwicklung systematisch und automatisiert getestet wird.

Spätestens nach diesem Satz ist klar, warum Qualitätssicherung ein wichtiges Thema für die Softwareentwicklung ist. In der Testpyramide stellen ausgiebige Unit-Tests die Basis dar, dann werden technische Komponenten in Komponenten-, Integrations- und Systemtests getestet und schließlich erfolgen punktuell Akzeptanztests.

Üblicherweise ist diese Abfolge Teil einer automatisierten Deployment-Pipeline, und die einzelnen Phasen erfolgen auf verschiedenen, eigens dafür eingerichteten Umgebungen. Die notwendigen Testfälle werden mit fachlichem Input erzeugt und automatisiert ausgeführt.

Am Ende steht das Deployment auf der Produktivumgebung – das Team ist sich zu diesem Zeitpunkt sicher, dass die neue Softwareversion einwandfrei funktionieren wird.

Es ist kein Geheimnis, dass dieser Idealfall nicht immer so eintritt. Die Gründe dafür sind vielfältig. Oft läuft beispielsweise auf dem Produktivsystem dann doch etwas anders, als auf den sorgfältig aufgebauten Testumgebungen. Es stellt sich also die Frage, ob man nicht noch mehr Fehler vermeiden kann, wenn man es schafft, die Tests näher an das eigentliche Produktivsystem heranzutragen.

You built it, you run it. You should test it!

Das Konzept “Testing in Production” taucht seit 2017 häufiger in der Softwarebranche auf – genau aus diesem Grund. Der Name legt es nahe: Tests sollen tatsächlich auf dem Produktionssystem ausgeführt werden. Da die Betriebsverantwortung beim Team liegt, sollte das Team bis zum Deployment – und darüber hinaus – größtmögliches Vertrauen in sein System haben. Dieses Vertrauen wird am besten über Tests aufgebaut, und zwar so nah am Produktivsystem wie möglich. Tests auf Produktion laufen zu lassen, sollte also Ziel eines modernen Testkonzepts sein.

Wir wollen in diesem Artikel drei konkrete Möglichkeiten beschreiben, mit denen wir in drei verschiedenen Projekten gute Erfahrungen gesammelt haben. Die beschriebenen Anwendungsfälle laufen teilweise auf Produktion, teilweise produktionsnah.

Happy-Flow-Smoke-Tests

Unserem ersten Anwendungsfall könnte man als „happy-flow smoke testing“ bezeichnen. Hier wird das ganze System als Blackbox gesehen; man versucht mithilfe von fachlichen Happy-Flow-Szenarien, die Core-Funktionalität aus Sicht des Users zu testen.

Die fachliche Dimension ist hierbei besonders wichtig. Auf Unit-Ebene werden (hoffentlich) bereits alle Services getestet. Die Integration zwischen mehreren Systemen ist bestenfalls auch schon technisch abgesichert. Ein fachlicher Systemtest zeigt Fehlverhalten des Systems vor der Auslieferung – also ehe 80 Prozent der User den Fehler bemerken. Er zeigt nicht unbedingt, wo genau ein Fehler liegt; bei positivem Verlauf verschafft er dem Team jedoch die notwendige Sicherheit, um das Risiko des Live-Gangs einzugehen. Daher kann man diese Art Test als Akzeptanz-Test verwenden, bevor man zum Beispiel auf PROD deployen möchte.

Beim Aufbau eines solchen Tests ist es wichtig, dass er jederzeit und bestenfalls auf allen Umgebungen – sei es TEST, STAGE oder sogar PROD – ausgeführt werden kann. Es muss also vermieden werden, auf einen Zustand des Systems zu vertrauen; besser ist es, sich lieber den benötigten Zustand selbst jedes Mal neu aufzubauen. Ein solches Test-Szenario besteht also oft aus vier (Gherkin-)Phasen:

  • Zustand aufbauen (z.B. einen Test-User registrieren)
  • Core-Features triggern (z.B. User-Login & Logout durchführen)
  • Systemzustand validieren (z.B. Anzahl der Login Versuche des Users abfragen)
  • Zustand abbauen (z.B. User deaktivieren/entfernen)

Konzentriert man sich auf die absoluten Core Features, so sind die Tests relativ stabil und müssen nicht oft angepasst werden. Man kann sie also oft ausführen und sich auf sie verlassen. Versucht man zu viele fachliche oder technische Features einzusetzen, steigt der Pflegeaufwand und die Abhängigkeit vom aktuellen State des Systems. Wichtig dabei ist allerdings, dass man tatsächlich regelmäßig ausliefert – also echtes Continuous Delivery umgesetzt hat; sonst ist die Einhaltung der kleinen Änderungs-Batches und damit die Verlässlichkeit in die Tests nicht gegeben.

Wie die einzelnen Aktionen in solchem Test-Szenario ausgeführt werden, ist dabei fast egal – man braucht oft keine Test-Frameworks oder Tools – sondern vielleicht ein paar ‘curl’-Befehle, die auf der ‘bash’ am Ende eines Deployment-Jobs von Jenkins ausgeführt werden. Wichtig ist nur, dass die Aktionen die echte API, die sonst von den Usern getriggert würde, ansprechen und nicht über irgendwelche Hintertüren Test-Funktionen ausführen. Sonst ist die Garantie des gleichen Verhaltens für den User nicht gegeben.

Tipp: Oft werden viel zu viele dieser Szenarien aufgebaut. Die Wartbarkeit kann sehr schnell komplex werden und die Laufzeit solcher Test-Phasen steigt erfahrungsgemäß. Gerade in komplexeren (Microservice-)Architekturen kann ein einfacher Happy-Flow, der Zustände an mehreren Stellen im System erzeugen muss, sehr komplex sein. Wenn man dann alle möglichen Permutationen der Zustände als eigene Test-Szenarien aufbaut, so ist der Aufwand für solche Tests oft größer als die Entwicklung der eigentlichen Features! Ein besseres Vorgehen ist, sich das System als Ganzen anzuschauen und die Use-Cases zu identifizieren, die häufig und wichtig sind – beziehungsweise auf die zu fokussieren, mit denen man letztendlich Geld verdient.

Mirroring

Beim Mirroring geht es darum, neu entwickelte Features mit Produktivdaten zu testen: Dazu verwenden wir eine eigene Umgebung mit Fokus auf Entwicklung, die jedoch mit echten Daten und echtem Input aus dem Produktivsystem gespeist wird. Auch hier gehen wir davon aus, dass neue Features korrekt umgesetzt sind und das technische Zusammenspiel der Komponenten funktioniert. Dazu braucht es konkret Tests von Fachlichkeit und Technik durch Unit- und Integrationstests.

Wir testen beim Mirroring, wie ein neues Feature mit dem täglichen Traffic auf dem System umgeht und wie unsere Infrastruktur reagiert. Hier kommt die „Mirror“-Umgebung ins Spiel: Die aktuelle Version der Anwendung wird dort unkompliziert vom Entwicklerteam deployed. Über Logging und Monitoring wird die Infrastruktur überwacht. Dabei sehen wir Metriken für „Mirror“ auf den gleichen Dashboards wie für das Produktivsystem.

Wir sind übrigens nicht die einzigen, die das gut finden. Bei Google wird das entsprechende Vorgehen „Dark Launch“ genannt – siehe hier. Der Unterschied zu unserem Vorgehen ist, dass der Dark Launch bereits auf dem Produktivsystem stattfindet, das Mirroring jedoch auf einer eigenen Umgebung.

Wenn man diese Form des Mirroring einsetzt, sollte man den Aufwand für Aufbau und Betrieb der Mirror-Stage bewerten. In unserem Fall war der Aufbau der zusätzlichen Umgebung mit einmaligem und überschaubarem Aufwand möglich. Der Betrieb kann jedoch Ressourcen binden: Kommt es zu Problemen, sollte ein frisches Deployment jederzeit möglich sein, um die Mirror-Umgebung wieder lauffähig zu machen. Ist das nicht der Fall, weil zum Beispiel eine Datenbank für den laufenden Betrieb bestimmte Einträge enthalten muss, müssten Probleme exakt so behoben werden, wie auf dem Produktivsystem. Hier gilt es den Aufwand, Produktiv- und Mirror-Umgebung parallel zu halten, gegen den Nutzen abzuwägen.

In unserem Fall überwiegen die Vorteile. Einmal aufgebaut sind die Möglichkeiten fantastisch: Man kann die Mirror-Umgebung eingeschränkt oder voll mit dem anfallenden Traffic versorgen. Da wir uns bei dem konkreten Fall in einem Streaming-Kontext befinden, können wir die anfallenden Daten auch „aufstauen“ und als Lastspitze abschicken. So kann ein Feature auf der Mirror-Umgebung im laufenden Sprint getestet werden.

Gerade wenn nichtfunktionale Anforderungen nicht klar oder nicht streng genug formuliert sind (oder sein können), erhöht der Mirror-Ansatz die Sicherheit durch kontinuierliches Monitoring der noch nicht produktiven Software-Version. Mögliche Bottlenecks der Infrastruktur oder gar echte Fehler im System erkennen wir so frühzeitig vor dem Rollout auf PROD.

Live-Testdaten

Happy-Flow-Smoke-Tests und Mirroring arbeiten nah am Produktiv-System. Mit Live-Testdaten gibt es einen Ansatz, der quer zu den beiden beschriebenen Verfahren gelagert ist. Auch sie lösen im besten Fall die Problematik unvorhersehbarer Systemzustände, die unter anderem die Happy-Flows zu lösen versuchen.

Für das fachliche Testing und Monitoring ist es wichtig, den definierten Output einer Schnittstelle prüfen zu können. Oft ist dies nicht möglich, insbesondere wenn wir nicht wissen, welche Daten in unser System fließen.

Um bekannte Daten im Produktivsystem zur Verfügung zu stellen, spielen wir Testdaten kontinuierlich und über die gleichen Schnittstellen in das System ein wie die Produktivdaten. Diese Testdaten werden gleichberechtigt zu den Produktivdaten gespeichert. Sie durchlaufen die gleichen Schritte und liefern dadurch Hinweise über fehlerhafte oder nicht verfügbare Komponenten im System. Das Gute an diesem Vorgehen: Wir schaffen sozusagen eine kontrollierte Versuchssituation – nur dass wir nicht die Umgebung kontrollieren, sondern die Daten. Aus dem Wissen um den Input und der Kontrolle des Output können wir auf Fehler in der Verarbeitung schließen.

Durch das Abfragen der Schnittstellen nach außen mit den definierten Testdaten kann das Nutzerverhalten kontrolliert simuliert werden; dadurch wird ein fachliches Monitoring durchgeführt. Es ist bekannt, welches Ergebnis ein Aufruf liefern sollte, wenn er mit diesen Testdaten durchgeführt wird und alle Komponenten des Systems korrekt funktionieren.

Eine Herausforderung, welche es jeweils individuell in Produktivsystemen zu lösen gilt, ist die Isolation der Testdaten für reale Benutzer. Obwohl diese Daten wie Produktivdaten behandelt werden, muss es einen Mechanismus geben, durch welchen diese Daten für reale Nutzer nicht sichtbar werden. Wenn die Daten selbst keine Verbindung zu den Produktivdaten haben, ist dies unproblematisch, da sie individuell angefragt werden. Sobald jedoch Suchanfragen auf die Gesamtdaten gestellt werden, müssen hier der Aufrufer identifiziert werden und ein weiterer Filter angewendet werden.

Von produktionsnah bis auf Produktion

Die drei beschriebenen Anwendungsfälle für Testkonzepte laufen nah am Produktivsystem oder sogar direkt auf dem System. Obwohl wir das Idealbild des „Testing in Production“ nicht voll erreichen, sind alle drei Vorgehensweisen für unser Kunden-Projekt notwendig und wertvoll. Interessant ist, dass der Impuls jeweils aus dem Entwicklerteam kam – aus der Motivation heraus, der Verantwortung für das eigene System gerecht zu werden. Schließlich baut und betreibt das Team das IT-System.

Fehler finden, bevor der Kunde sie sieht

Und um noch einmal eine Ebene höher zu diskutieren: Ein Problem mit der Software, das im Rahmen eines Tests auftritt, hat noch keinen Schaden verursacht. Wenn wir also in der Lage sind Fehler zu finden, bevor der Kunde sie sieht, können wir gegensteuern. Dann schlägt der Fehler auch nicht finanziell zu Buche. Wir arbeiten natürlich mit bestehenden Good Practices.

Daneben haben wir Innovationsbedarf: So wirkt zum Beispiel die Testpyramide nicht mehr ganz aktuell; unser Kollege Marcel Gehlen beschreibt statt dessen in seinem Blog das Test-Hourglass. Wir sind überzeugt, dass das Thema Testing in Production noch viel mehr Möglichkeiten bietet; unsere drei beschriebenen Use Cases werden sich sicher noch weiterentwickeln.


Über die Autoren

Konrad Schreiber, Bartosz Boron, und Maurice Wendt