Shownotes & Links
Transkript
Lucas Dohmen: Hallo und herzlich Willkommen zu einer neuen Folge des INNOQ-Podcasts. Heute wollen wir über End-to-End-Testing von Microservice-Systemen sprechen und dafür habe ich mir einen Gast eingeladen, den Torsten. Hallo Torsten.
Torsten Mandry: Hallo Lucas.
Lucas Dohmen: Torsten, magst du dich mal kurz vorstellen: Wer bist du und was machst du bei INNOQ?
Torsten Mandry: Mein Name ist Torsten Mandry, ich bin Senior Consultant bei der INNOQ und bin dort hauptsächlich damit beschäftigt, Software-Systeme für unsere Kunden zu entwickeln. Das ist meist mit Java und Web-Technologien, ich bin überwiegend im Backend, manchmal auch im Frontend unterwegs. Ansonsten interessiere ich mich noch für Domain-driven-Design, Clean Code und schreibe gerne automatisierte Tests. Insbesondere Integrationstests und End-to-End-Tests.
Lucas Dohmen: Bevor wir jetzt bei Microservices einsteigen: Was verstehst du denn unter einem End-to-End-Test und warum findest du die wichtig, warum wollen wir die haben?
Torsten Mandry: Ein End-to-End-Test testet ein Software-System, das ich entwickle, als eine Einheit, als Ganzes. Das heißt alle Teile, alle Komponenten vollständig integriert. Und zwar optimalerweise so nah wie möglich an dem, wie es auch nachher in der Produktion läuft und von einem Anwender oder von angebundenen Systemen verwendet wird. Das heißt über die öffentlichen Schnittstellen, beispielsweise eine API oder UI.
Lucas Dohmen: Wenn wir jetzt über eine Browser-Anwendung sprechen: Würde das auch bedeuten, dass ein Browser zum Testen benutzt wird?
Torsten Mandry: Ganz genau. Das Ziel dabei ist sicherzustellen, dass die ganzen Einzelteile auch vernünftig zusammenspielen. Klar, für alle diese Einzelteile haben wir unsere Unit-Tests, aber ich glaube jeder hat schon mal lustige Bildchen im Internet gefunden mit “Unit-Tests versus Integrationstests”. Unit-Tests sind super, um festzustellen, dass eine Einheit für sich genommen funktioniert, aber letztendlich bringt mir das Ganze erst dann etwas, wenn hinterher alles zusammen funktioniert. Wenn die Anwendung von vorne bis hinten funktioniert und das tut, was der Anwender oder die Systeme, die meine Anwendung verwenden, haben wollen. Und genau das möchte ich halt über End-to-End-Tests feststellen. Optimalerweise habe ich das nicht im großen Detail und auch nicht viele, sondern nur wenige, nur eine Hand voll. Die finde ich aber besonders wertvoll, um am Ende sagen zu können: Ja, es funktioniert wirklich und jetzt kann ich es auf die Produktion loslassen.
Lucas Dohmen: Ein Beispiel dafür wäre vielleicht mal einen ganzen Buchungsprozess durchzuspielen: Als Kunde etwas in den Warenkorb zu legen, zu gucken, dass ich etwas bestellen kann und so weiter.
Torsten Mandry: Ganz genau. Bis hin zur Bestellung und den Outcome zu testen, dass der Kunde am Ende die Bestellbestätigungsseite sieht, vielleicht noch eine Email bekommt und am Ende noch eine Nachricht an ein angebundenes Drittsystem herausgeht.
Lucas Dohmen: Du sagst, man möchte das System als Ganzes betrachten. Datenbanken, Queues und was auch immer sollen alle mitspielen. Je komplexer so ein System wird, desto mehr muss ich tun, damit ich die Tests auch durchführen kann. Wie kann ich mir jetzt solche Tests in einem Microservice-System vorstellen?
Torsten Mandry: Bei einem Microservice-System kann ich End-to-End-Tests auf zwei Ebenen betrachten. Microservice-System heißt ja, dass ich mehrere Teilsysteme habe, mehrere Microservices, die jeder für sich schon ein vollständiges System sind. Die kann ich natürlich zum Einen auf Basis von Unit-Tests entwickeln, aber auch Integrations-testen und End-to-End-testen. Das ist jetzt erstmal nicht großartig anders als mit jedem anderen System auch. Die spannendere Ebene ist, dass in einem Microservice-System das Gesamtsystem aus mehreren dieser Microservices besteht. Das heißt im Grunde genommen - wenn man das mal von der Ebene aus betrachtet - ist ein Microservice dann eine Unit, eine Komponente eines Gesamtsystems. Wenn ich diese Analogie so ein bisschen weiter treibe, dann kann ich sagen: Okay, die ganzen Tests, die ich für meinen einzelnen Microservice habe, sind auf Ebene des Gesamtsystems dann quasi nur die Unit-Tests. Und wenn man dann aus der Entwicklung eines einzelnen Systems kommt und gewohnt ist, dass man am Ende ein paar End-to-End-Tests hat, ist es relativ naheliegend, dass man auf die Idee kommt: Hey, ich kann ja auch einen End-to-End-Test für mein Gesamtsystem machen. Alle Teilsysteme, alle Microservices zusammen testen und halt genau solche End-to-End-Szenarien durchspielen.
Lucas Dohmen: Um jetzt nochmal das Beispiel zu nehmen, über das wir gerade gesprochen haben: Sagen wir mal, wir haben ein Warenkorb- und ein Bestellsystem. Dann könnte ich das Warenkorb-System in Isolation End-to-End-testen, also wirklich im Browser den gesamten Prozess durchspielen, wie ich da etwas hinein lege und ich könnte in Isolation durchspielen, wie das Bestellen nachher funktioniert, dass die Mail rausgeht und was auch immer. Jetzt ist die Frage: Möchte ich auch die Gesamtstrecke haben, also den Übergang von dem Warenkorb- ins Bestellsystem, richtig?
Torsten Mandry: Zumindest möchte ich mir üblicherweise sicher sein, dass das funktioniert, denn in meinem Gesamtsystem bringt das Ganze erst dann etwas, wenn der Kunde von dem Warenkorb in den Bestellprozess reinkommt und man den Prozess vernünftig starten kann. Und wenn man in dem Kontext über End-to-End-Tests nachdenkt, dann wäre das genau so ein Szenario, das man wahrscheinlich testen würde.
Lucas Dohmen: Und ist es eine gute Idee, das zu tun oder erzeugt es irgendwelche Probleme, diesen Ansatz zu fahren?
Torsten Mandry: Wie bei so vielen Sachen: Es kommt darauf an. Es kommt darauf an, wann man das macht und wie man das macht. Vielleicht macht es Sinn, da nochmal einen Schritt zurück zu gehen und uns nochmal in Erinnerung zu rufen, warum man überhaupt auf die Idee kommt, ein Microservice-System oder Microservices zu machen. Da gibt es aus meiner Sicht im Wesentlichen drei Punkte bzw. Gründe: Der erste Punkt, den ich da sehe, ist: Mein Gesamtsystem ist zu komplex, als dass ich es als ein System mit einem Team alleine verstehen und entwickeln könnte. Das heißt ich will es modularisieren, ich will es zerschneiden, ich will da kleinere überschaubare Häppchen draus machen. Damit das funktioniert - und damit sind wir beim zweiten Punkt - damit dann mehrere Teams gemeinsam quasi dieses Gesamtsystem entwickeln können, müssen die das so weit wie möglich unabhängig voneinander tun. Sie sollen sich nicht gegenseitig auf den Füßen stehen, sondern müssen in der Lage sein, unabhängig voneinander zu agieren. Die beiden Punkte zusammen sprechen jetzt noch nicht zwingend für Microservices-Systeme, denn das kriege ich auch über andere Modularisierungsformen hin. Der dritte - und aus meiner Sicht ausschlaggebende - Punkt ist deswegen: Ich möchte diese einzelnen Services, diese einzelnen Teile auch unabhängig voneinander deployen können. Das heißt, wenn wir jetzt wieder bei dem Beispiel mit dem Warenkorb- und dem Checkout-System sind, ich möchte in der Lage sein den Warenkorb neu zu deployen, ohne dass an dem Checkout irgendetwas passiert oder halt umgekehrt. Und dieser letzte Punkt - das kriege ich eben mit anderen Modularisierungstechniken, wie irgendwelchen getrennten Packages oder getrennte Libraries/Subprojekten, nicht mehr hin. Da entscheide ich mich wahrscheinlich für Microservices. Und wenn wir jetzt wieder zurückkommen zu den End-to-End-Tests - da haben wir ja gerade gesagt, dass die das Gesamtsystem in Integration testen, das heißt mit allen Teilen. Im Kontext eines Microservice-Gesamtsystems heißt das alle Microservices zusammen als ein Ganzes. Und damit habe ich im Grunde wieder die vollständige Abhängigkeit. Das heißt diese Unabhängigkeit, die ich mir vorher teuer erkauft habe - denn Microservices kriege ich auch nicht geschenkt - hebe ich damit wieder ein ganzes Stück auf und teste am Ende doch wieder alles zusammen. Was noch dazu kommt: Wenn ich davon ausgehe, dass meine einzelnen Microservices von unterschiedlichen Teams entwickelt werden und ich darüber nachdenke, über alle Systeme zusammen eine Suite von End-to-End-Tests zu pflegen, dann heißt das auch, dass diese über alle Services gehen und damit dann auch von allen Teams zusammen entwickelt und gepflegt werden müssen. Denn jedes Team kann am besten entscheiden, was in seinem Bereich das Relevante ist, was in irgendeinem Szenario passieren kann. Auf der Ebene habe ich also auf einmal wieder eine ziemlich große Abhängigkeit zwischen den Teams.
Lucas Dohmen: Das heißt also diese extreme Unabhängigkeit zwischen den Services beißt sich mit den übergreifenden End-to-End-Tests, in denen du dann wieder eine Abhängigkeit erzeugst.
Torsten Mandry: Ganz genau, das geht im Grunde genommen in entgegengesetzte Richtungen, das eine hebt das andere wieder ein Stück auf. Erst recht, wenn ich dann noch irgendwie versuche den End-to-End-Test in meine Build- und Deployment-Pipeline zu integrieren. Das heißt ich versuche mögliche Probleme mit diesen Tests festzustellen, bevor ich irgendetwas in Produktion deploye. Dann bin ich in der Situation, in der ich diese Deployment-Pipelines miteinander gekoppelt habe. Ein unabhängiges Deployen, wie ich es eigentlich im Microservice-Kontext haben möchte, ist dann nicht mehr möglich.
Lucas Dohmen: Aber trotzdem ist es ja so, dass diese Systeme ein Gesamtsystem bilden, sonst wären es ja unabhängige Applikationen. Das heißt, dass wir zur Laufzeit diese Abhängigkeit haben, sie sind ja nicht vollständig unabhängig. Aber dir geht es um den Zeitpunkt, bevor es in Produktion geht?
Torsten Mandry: Ganz genau. Ich möchte im Extremfall mit meinem Service in der Lage sein, ihn in Produktion zu deployen, auch wenn mit irgendeinem anderen Service ein Problem existiert, dieser also tatsächlich nicht funktioniert. Ich möchte nicht durch ein woanders existierendes Problem auf meiner Seite im Deployment gehindert sein.
Lucas Dohmen: Das heißt, wenn der Test fehlschlagen würde, weil die neueste Version von dem anderen Team ein Problem hat, möchtest du trotzdem deployen können.
Torsten Mandry: Genau.
Lucas Dohmen: Okay, für mich klingt das so: Wir können jetzt doch nicht diese Art von End-to-End-Tests machen. Wie habt ihr das denn im Projekt gemacht, du hast mir erzählt, dass ihr dort auch ein bisschen eine Evolution durchgemacht habt.
Torsten Mandry: In meinem aktuellen Projekt haben wir halt genau diese Erfahrung gemacht. Das heißt wir sind genau diesen Weg gegangen, den ich geschildert habe. Vielleicht kurz der Kontext: Ich bin aktuell in einem Online-Shop-Projekt unterwegs. Da sind wir mit mehreren unabhängigen Teams, sogenannten Vertikalen unterwegs, die - wie es immer so schön heißt - entlang der Customer Journey geschnitten sind. Das heißt, es gibt so etwas wie Suchen, Entdecken, Kaufen und so weiter. Das ist glaube ich nicht so unüblich in dem Umfeld. Und in der Vertikale, in der ich unterwegs bin, passieren verschiedene Dinge, nachdem der Kunde eine Bestellung abgeschickt hat. Beispielsweise eine Risikoprüfung und eine Art von Betrugsprävention, die Logistikanbindung, Payment-Services. Das sind alles so Themen, die in der Vertikale angesiedelt sind und es sind halt relativ unterschiedliche Dinge, die alle ihre eigene Teildomäne haben. Deswegen haben wir uns innerhalb unserer Vertikale dafür entschieden, da auch eine Microservice-Architektur aufzusetzen.
Lucas Dohmen: Um das nochmal als Gesamtsystem zu verstehen: Es gibt diese großen Vertikalen, das sind Microservices in ganz großen Anführungsstrichen, weil sie nicht besonders “micro” sind, das sind wahrscheinlich schon komplexe Anwendungen. Und innerhalb dieser Systeme habt ihr dann auch wieder Microservices.
Torsten Mandry: Ja, zumindest innerhalb unserer Vertikale haben wir uns dafür entschieden. Und dort spielen auch die Erfahrungen, von denen ich berichten kann. Alles innerhalb dieser Vertikale, innerhalb von einem Team. Da hatten wir genau diese Idee, zu sagen: Wir haben jetzt hier diese unterschiedlichen Services, die haben wir alle einigermaßen gut und jeder für sich getestet und wir sind zuversichtlich, dass das funktioniert. Aber es bleibt immer noch ein bisschen Ungewissheit: Wie ist das, wenn die zusammen funktionieren müssen? Klappt das auch oder gibt es da irgendwelche Probleme?
Lucas Dohmen: Aber du sprichst jetzt von den Microservices innerhalb eurer Vertikale?
Torsten Mandry: Ja, genau. Die Idee, die wir dann hatten, waren genau solche End-to-End-Tests über solche Services hinweg. Also genau so etwas, wie du eben geschildert hast. In unserem Kontext würde das bedeuten, dass wir von der vorgelagerten Vertikale eine Bestellung bekommen, die der Kunde gerade platziert hat und ein erwartetes Ergebnis, nachdem die Bestellung dann einmal durch unsere Vertikale hindurch ist bzw. verarbeitet worden ist. Dass am anderen Ende dann eine Nachricht an die Logistik rausgeht, dass die die Artikel für die Bestellung schonmal reservieren können. Oder es kommt im entgegengesetzten Weg von der Logistik die Nachricht, dass sie gerade eine Lieferung rausgeschickt haben und dann müssen wir darauf reagieren und gegebenenfalls noch Zahlungen “capturen” und eine Rechnung erstellen und an den Kunden schicken. Das sind auch wieder Dinge, die man dann testen kann. Das war quasi unsere Idee, wir wollten das sehr grobgranular machen. Also keine Details testen, nicht welche Inhalte stehen beispielsweise in so einer Rechnung oder Email drin, sondern im Wesentlichen sehen: Ja, das ist eine Email rausgegangen und da steht die Bestellnummer drin und das reicht uns dann auf der Ebene schon. Größenordnungsmäßig hatten wir etwas zwischen 10 und 20 Tests in dieser Art, die wir ausführen wollten.
Lucas Dohmen: Also ein Interface-Test, der dann eure Webseite benutzt oder ist das eine API gewesen?
Torsten Mandry: Dadurch, dass wir in unserer Vertikalen nicht so wahnsinnig viel Customer-Facing-Interface haben, sind das vielfach auch API-Anbindungen gewesen. Die Bestellung, die wir von der vorgelagerten Vertikale bekommen, lesen wir beispielsweise aus einem Feed. Das heißt wir haben den Feed dieser Vertikale simuliert. Auf der anderen Seite nutzen wir einen Mailprovider, um eine Email an den Kunden zu verschicken. Das ist also auch wieder eine Schnittstelle, die wir ansteuern und wir haben einfach nur geguckt: Auf der anderen Seite von dieser Schnittstelle, kommt da das an, was wir erwarten? Das heißt wenig tatsächliche Oberfläche, sondern viele Schnittstellen. Aber das ist halt der Art und Weise unserer Vertikalen geschuldet. Wenn wir eine Vertikale wären, bei der wir viel Oberfläche haben, dann - ich habe ja gesagt, so wie der Anwender beziehungsweise der Endnutzer das System verwendet - dann wäre es halt die Oberfläche, bei der man sehen würde: Okay, da taucht jetzt etwas auf, das vorher nicht da war.
Lucas Dohmen: Etwas wie ein Entdecken-System oder so, das wäre dann sehr viel visueller und dann würde das auch mehr solche browserartigen Tests… Und wie seid ihr da jetzt herangegangen, um das Problem zu lösen? Ihr wolltet jetzt diesen gesamten Workflow durchtesten, wie habt ihr das gemacht?
Torsten Mandry: Wir haben es im ersten Ansatz tatsächlich so gemacht. Unser Ziel war es mögliche Probleme zu entdecken, bevor wir irgendetwas in Produktion deployen. Das heißt wir haben tatsächlich versucht diese End-to-End-Tests innerhalb unserer Build- und Deployment-Pipeline einzubauen, für jeden Service. Technisch haben wir das so gelöst, dass wir für diese End-to-End-Tests, die ja serviceübergreifend waren, ein separates Source-Code-Projekt aufgemacht haben, worin sie gepflegt worden sind. Dann haben wir innerhalb der Build-Pipeline jedes Services, am Ende, nachdem die ganzen lokalen Tests und alles durchgelaufen waren, den letzten Stand dieser End-to-End-Tests ausgecheckt. Dann haben wir eine entsprechende Testumgebung hochgefahren - das haben wir jetzt in dem Fall mit Docker Compose gemacht - und in dieser Testumgebung war dann der gerade in der Build-Umgebung gebaute Service drin, den wir hochgefahren haben. Und von allen anderen Services jeweils der Stand, der gerade in Produktion deployed ist, der jetzt gerade produktiv läuft. Und dann haben wir halt noch für irgendwelche Umsysteme, beispielsweise den Feed der vorangestellten Vertikale, die API des Mailproviders zum Beispiel, irgendwelche Mock-Systeme dahin gestellt. Damit wir Dinge simulieren konnten beziehungsweise hinterher gucken konnten, was da angekommen ist. Und wenn wir diese Tests innerhalb der Pipeline ausgeführt haben und da irgendetwas schiefgelaufen ist, also die Tests rot geworden sind, haben wir halt gesagt: Okay, dann brechen wir den Build ab und dann deployen wir nicht.
Lucas Dohmen: Wenn ich das jetzt so höre, dann klingt es für mich so, als hätte jeder Service seinen eigenen Docker-Container, auch in Produktion nachher und ihr habt die dann nur so zusammengestellt, wie man sie dann für den Test braucht quasi.
Torsten Mandry: Genau.
Lucas Dohmen: Und habt ihr dann auch in Produktion Docker Compose verwendet?
Torsten Mandry: Nein, das haben wir nicht gemacht. Das ist schon das erste, das nicht ganz so optimal war in dem Ansatz. In der Produktion haben wir tatsächlich Kubernetes-Cluster, wo wir hin deployen. Ich glaube wir haben uns, als wir die End-to-End-Tests aufgesetzt haben, ein bisschen davor gescheut, dort Kubernetes auch noch in die Build-Pipeline zu integrieren und haben gedacht, dass es mit Docker Compose alles viel einfacher und viel eleganter geht. Was aber im Endeffekt dazu geführt hat, dass man die End-to-End-Test-Umgebung in keinster Weise mit der tatsächlichen Produktionsumgebung vergleichen konnte. Es waren zwar immer noch dieselben Services, dieselben Docker-Images und -Container, die wir da haben laufen lassen, aber die Art und Weise, wie wir die da deployed haben, wie wir die hochgefahren haben, hat sich schon ziemlich unterschieden.
Lucas Dohmen: Ich könnte mir auch vorstellen, dass das auch leicht mal auseinanderlaufen kann, wenn vielleicht ein Docker-Container dazu kommt oder wegfällt. Dass man dann auch mal drauf achtet, dass die in beiden Umgebungen nachgezogen werden.
Torsten Mandry: Die Container, also die Services, die wir da deployed haben, die waren eigentlich relativ stabil, da hat sich nicht so wahnsinnig viel geändert. Und nachdem wir die Testumgebung einmal aufgesetzt hatten und die einmal vernünftig lief, war das eigentlich auch relativ stabil. Aber was halt definitiv ein Problem war, war, dass wir dieses ganze Deployment, dadurch dass das jetzt nicht mehr Kubernetes, sondern Docker Compose war, im Grunde genommen für Docker Compose nochmal nachgebaut haben. Wir haben halt ziemlich viele Sachen nochmal neu gemacht.
Lucas Dohmen: Du hast ja gesagt, der Feed und der Mail-Provider waren Mocks, aber die Datenbank war ja sicherlich eine echte Datenbank, es wurde ja bestimmt eine benutzt, oder?
Torsten Mandry: Wir haben glaube ich auch innerhalb von Docker Compose eine oder mehrere Datenbanken hochgefahren und benutzt.
Lucas Dohmen: Okay, das heißt auch, da musstet ihr dann das replizieren, was in der Produktion gemacht wurde. Wenn da also Postgres 11 war, musste man gucken, dass das auch Postgres 11 in Docker Compose war.
Torsten Mandry: Das machen wir nach wie vor sowieso auch für die End-to-End-Tests, so einen Service, dass da jeder seine eigene Datenbank hat. Wir haben da tatsächlich zwei verschiedene Datenbanksysteme, die wir nutzen. Der hat seine eigene Testumgebung, wo seine eigenen End-to-End-Tests ausgeführt werden. Das ist eigentlich kein so großes Problem. Das war noch das kleinste Problem, das wir hatten.
Lucas Dohmen: Und was waren die größeren Probleme?
Torsten Mandry: Größere Probleme waren beispielsweise die Services miteinander sprechen zu lassen. Und vor allen Dingen ein Problem, das zumindest ich am Anfang gar nicht auf dem Schirm hatte, ist, dass es alles andere als trivial ist, Docker Compose hochzufahren und dann wirklich darauf zu warten, dass alle Services auch ready sind. Die ersten Testläufe, die wir hatten, sind dunkelrot geworden, weil die Tests schon losgelaufen waren, während die Services gerade noch hochfuhren. Ich weiß nicht, wie das mittlerweile ist - das ist bei uns schon ein bisschen her, kann sein, dass es da in Docker Compose mittlerweile bessere Mechanismen gibt - aber damals gab es da wirklich gar nichts. Das heißt Docker Compose hat Bescheid gesagt: Okay, jetzt ist der Container da und das war dann das, worauf wir uns verlassen hatten. Was aber nicht heißt, dass der Service in dem Container auch schon da ist. Der braucht dann eventuell mal ein paar Sekunden länger, um hochzufahren. Sodass wir beispielsweise dafür wirklich von Hand irgendwelche Skripte mit eingebaut haben, in dieses End-To-End-Test-Setup, die dann wirklich wussten: Welche Services müssen jetzt auf welchen Ports überall da sein? Und die dann halt solange probiert haben, bis irgendwann alle da waren. Und wenn irgendeiner von den Services dann nicht hochkam, dann mussten die auch irgendwann in einen Timeout laufen. Das war beispielsweise eine Baustelle, an die ich mich erinnern kann, in die wir ein bisschen Zeit investiert haben, bis wir das irgendwie einigermaßen stabil laufen hatten.
Lucas Dohmen: Du hast es schon ein bisschen angedeutet: Wahrscheinlich haben die Testläufe dann auch sehr, sehr lange gedauert, oder?
Torsten Mandry: Die Testläufe selbst, also die Ausführung der Tests, nicht einmal so sehr. Vor allem auch, weil wir, wie gesagt, eine sehr überschaubare Anzahl von Tests hatten. Aber es hat ziemlich lange gedauert, bis die ganzen Systeme dann mal hochgefahren waren. Das heißt insgesamt waren das schon ein paar Minuten, die quasi innerhalb der Deployment-Pipeline unserer Services auf einmal dazu gekommen sind. Alleine dadurch, dass die Testumgebung hochgefahren wird.
Lucas Dohmen: Aber es klingt ein bisschen so wie: Nur weil der Build gerade rot ist, heißt das noch lange nicht, dass das Ding kaputt ist. Oder? Viele False Positives?
Torsten Mandry: Definitiv, ja. Eine Sache, die wir gelernt haben, ist: Wir haben da relativ viele, jedes für sich auch schon relativ komplexe Dinge miteinander integriert und das ist definitiv ein Punkt - wenn man das tut, dann gibt es auch ziemlich viel, das schieflaufen kann. Irgendein Service reagiert dann gerade nicht oder was auch immer. Das heißt, die Tests waren eigentlich von Anfang an spürbar instabil. Und in vielen Fällen hat es nicht daran gelegen, dass irgendwo in der Implementierung ein Bug war. In vielen Fällen hat es wirklich am Test-Setup gelegen. Dass beim Hochfahren irgendetwas schiefgegangen ist, dass ein Service nicht hochgekommen ist, dass sich ein anderer Service aus irgendeinem Grund schon wieder beendet hatte, bis der erste hochgekommen ist und so weiter. Das waren definitiv Probleme, die wir hatten.
Lucas Dohmen: Und wie seid ihr jetzt damit umgegangen? Was war euer Schluss daraus?
Torsten Mandry: Letztendlich haben wir nach einiger Zeit irgendwann gemerkt, dass in der Art, wie wir es aufgesetzt haben, wir keinen oder sogar einen gefühlt negativen Nutzen haben. Wir haben zwar viele rote Tests gehabt, aber wie gerade schon gesagt: In der Regel waren das wirklich False Positives, aus unterschiedlichsten Gründen. Teilweise auch aus Gründen, die wir dann selbst verursacht haben. Es war beispielsweise so, dass wir im Service bzw. in der API etwas angepasst haben, aber dann vergessen haben, die Tests auch mit anzupassen - die End-to-End-Tests - weil die halt in einem separaten Repository liegen und weil wir die nicht auf dem Schirm hatten, das ist öfters passiert. Teilweise hatten wir auch Race-Conditions, das heißt wenn zwei Services zufälligerweise parallel gerade geändert worden sind und man sogar dran gedacht hat in beiden Fällen auch die End-to-End-Tests entsprechend anzupassen, aber beide Services gerade parallel gebaut werden, das heißt beide sind noch nicht in Produktion, dann haben die End-to-End-Tests jeweils den alten Stand des jeweils anderen Services gegriffen und dann hat es auch wieder nicht geklappt.
Lucas Dohmen: Das heißt der Test konnte gar nicht richtig sein, weil er entweder zu alt war für einen der beiden oder zu neu war für den anderen der beiden?
Torsten Mandry: Er war eigentlich immer zu neu für einen der beiden, je nachdem in welcher Deployment-Pipeline er gerade gelaufen ist. Wir haben aber, soweit ich mich erinnern kann, kein einziges Mal wirklich durch diese Tests irgendein Problem gefunden, sodass wir gesagt hätten: Oh gut, dass wir das gefunden haben, bevor wir damit in Produktion waren. Es waren eigentlich immer nur False Positives, die wir gefunden haben. Und dafür haben wir uns halt einen relativ hohen Aufwand damit eingekauft, diese Tests initial überhaupt erst aufzusetzen, aber auch am Laufen zu halten und diese Probleme immer wieder zu beheben. Und wir hatten dann zusätzlich mit genau diesen False Positives und abbrechenden Builds zu tun, wo man das macht, was man eigentlich nie tun müssen sollte, nämlich: Oh, der Build ist rot, dann starte ich ihn einfach nochmal neu. Das haben wir in der Zeit relativ häufig gehabt. Zumindest deutlich häufiger, als man so etwas gerne haben möchte. Darauf haben wir dann insofern reagiert, als dass wir gesagt haben: Okay, wir nehmen diese Tests aus den Deployment-Pipelines, aus den Build-Pipelines unserer Services heraus, um genau diese Schmerzen nicht mehr zu haben. Wir haben aber trotzdem versucht, an diesen Tests festzuhalten. Ich glaube im Wesentlichen, weil wir eben soviel Arbeit hinein gesteckt haben und weil wir immer noch so ein bisschen das Gefühl hatten: Eigentlich wollen wir das schon ganz gerne wissen und wollen da schon ein bisschen Sicherheit haben, dass gerade nichts Schlimmes passiert. Was wir dann gemacht haben, ist, dass wir für diese Tests eine separate Pipeline aufgesetzt und einfach gesagt haben: Wir lassen diese Tests jetzt regelmäßig alle zehn oder fünfzehn Minuten laufen und nehmen dann halt den Stand, der in Produktion ist. Das heißt wir sind an der Stelle schon den Schritt gegangen in Kauf zu nehmen, dass wir vielleicht etwas in Produktion deployen, das da gar nicht hin gehört. Aber dann möchten wir es zumindest möglichst schnell wissen und innerhalb von ein paar Minuten darüber informiert werden. An den Tests selbst hat sich nicht so wahnsinnig viel geändert. Wir hatten nach wie vor die Probleme, dass die instabil waren, dass die regelmäßig mal fehlgeschlagen sind. Das, was sich geändert hat, ist: Dadurch, dass die Deployments jetzt nicht mehr blockiert waren, haben wir nicht mehr diese zwingende Notwendigkeit gehabt uns darum zu kümmern. In der Kombination mit “In der Regel sind das sowieso False Positives” hat das dann relativ schnell dazu geführt, dass die normalerweise rot waren und ab und zu, wenn mal jemand Zeit und Langeweile hatte, hat er sich mal angeguckt, warum die gerade rot waren und hat versucht das zu fixen. Dann waren sie wieder kurz grün und dann waren sie irgendwann wieder rot.
Lucas Dohmen: Muss ich mir das ein bisschen so vorstellen, dass ihr quasi alle fünf Minuten eine Email im Postfach hattet, in der stand: Der Build ist gerade rot, bitte reparieren?
Torsten Mandry: Bei uns war das keine Email, sondern eine Nachricht im RocketChat, den wir verwenden.
Lucas Dohmen: Also im Team-Channel?
Torsten Mandry: Im Team-Channel. Ja, so ungefähr. Gerade in einem Chat kann man eine solche Nachricht auch ganz gut ignorieren und irgendwann hat man sich das dann wie gesagt mal angeguckt. Aber im Grunde genommen war an der Stelle schon klar, dass wir jetzt gar keinen Nutzen mehr haben, weil, selbst wenn die Tests etwas finden würden, es überhaupt keiner merken würde. Was auch noch dazu beigetragen hat, war: Dadurch, dass die Tests jetzt immer ein bisschen zeitversetzt gelaufen sind, war auch keine direkte Verbindung mehr zu irgendeinem Commit gegeben. Wenn ich also eine Änderung gemacht habe und das deployed habe und zehn Minuten später sind die End-to-End-Tests rot geworden - ja, vielleicht habe ich dann die Befürchtung gehabt, dass ich das gewesen sein könnte, aber vielleicht haben ja auch noch drei andere Leute gleichzeitig etwas commitet und wer weiß, wer es gewesen ist. Das hat definitiv auch nochmal dazu beigetragen, dass in der ersten Zeit vielleicht so ein bisschen Panik geherrscht hat, immer wenn die rot geworden sind, aber nachdem man hinein geguckt und wieder gesehen hat “Ah, wieder False Positives”, hat irgendwann keiner mehr drauf geachtet.
Lucas Dohmen: Das heißt man war so abgestumpft, dass man es irgendwann gar nicht mehr mitbekommen hat?
Torsten Mandry: Ja. Dieses typische Broken-Window-Prinzip: Die sind so oft kaputt, da achte ich einfach nicht mehr drauf.
Lucas Dohmen: Und was habt ihr dann gemacht?
Torsten Mandry: Nachdem wir diese Erkenntnis hatten, haben wir sie wirklich abgeschaltet und gesagt: Okay, es bringt nichts mehr. Wir versuchen die auf Stand zu halten, wir stecken da nur Geld rein, aber haben nichts davon. Und dann haben wir sie wirklich abgeschaltet. Zumal wir nach wie vor festgestellt haben, dass wir nie echte Fehler gefunden haben, das heißt wir hatten auch über diese Erfahrung, dass die nie etwas gezeigt haben, in der Zeit, in der sie gelaufen sind - und es waren diverse Monate - ein bisschen Sicherheit gewonnen, dass wir mit unseren Service-spezifischen Tests anscheinend doch besser unterwegs sind, als wir am Anfang gedacht haben. Und wir haben auch danach nicht, zumindest nicht dass ich wüsste, nochmal Fehlerfälle gehabt, bei denen wir gesagt hätten: Wenn wir jetzt noch die End-to-End-Tests gehabt hätten, dann hätten wir das früher gemerkt. Aber das war definitiv ein Lernprozess, den wir durchlaufen haben, bis wir die Erkenntnis hatten.
Lucas Dohmen: Was schließt du denn jetzt daraus für dich? Ist das in diesem Projekt eine schlechte Idee gewesen, also wie ihr es umgesetzt habt oder ist es eher eine konzeptionelle Sache, wo du sagst: Eigentlich macht das so keinen Sinn.
Torsten Mandry: Ich würde sagen, in der Art und Weise, in der wir es versucht haben, war es definitiv eine schlechte Idee. Ein bisschen anders formuliert: In dem Moment, in dem ich mich für eine Microservice-Architektur entscheide und diese Unabhängigkeit und das unabhängige Deployen haben will, muss ich meiner Meinung nach akzeptieren, dass ich mir dadurch eben auch ein gewisses Risiko einfange, welches ich nur schlecht wieder abgefedert bekomme. Zumindest nicht, bevor etwas passiert ist. Ich muss damit leben, dass ich vielleicht tatsächlich eine Version eines Services X in Produktion deploye und erst dann feststelle, dass der Service Y ein Problem hat, weil er Service X verwendet und dieser sich jetzt anders verhält. Ich glaube, dass es Dinge gibt, die ich tun kann, wenn ich ein großes Risiko sehe, aber End-to-End-Tests sind glaube ich in diesem Kontext nicht das richtige Mittel, zumindest nicht End-to-End-Tests innerhalb der Build-Pipeline. Denn wenn ich sage, dass ich unabhängig deployen möchte, heißt das, dass ich die Services unabhängig in Produktion bringen möchte. Wenn die in Produktion sind, sind sie aber nicht mehr unabhängig. Da habe ich genau diese Integration und da muss ich die auch haben, denn da ist die Umgebung, wo sie alle zusammenspielen müssen. Innerhalb der Produktivumgebung, also ein bisschen so wie wir den zweiten Ansatz gefahren haben, wenn die Stände, die jetzt gerade alle produktiv sind - da können End-to-End-Tests in meinen Augen tatsächlich Sinn machen und auch ein Hilfsmittel sein.
Lucas Dohmen: Also in Produktion?
Torsten Mandry: In Produktion, zusammen mit einem Monitoring und einem Alerting, welches ich in einer Produktion sowieso haben möchte, denn es gibt ja auch durchaus Szenarien und Situationen, in denen auf einmal etwas schiefgeht, ohne dass ich etwas deployed habe, ohne dass ich als Entwickler einen Bug hineingebracht habe. Es kann sein, dass Netzwerkkomponenten ausfallen oder dass etwas anderes passiert und das will ich ja auch mitkriegen, das heißt, da möchte ich sowieso ein Auge drauf haben, wenn irgendwas passiert. Diese Monitoring-Dinge haben aber häufig die Abhängigkeit, dass sie nur dann merken können, dass irgendetwas nicht funktioniert, wenn das entsprechende Ding auch gerade benutzt wird. Und wenn ich jetzt beispielsweise ein System habe, in dem irgendwelche Teile drin sind, die vielleicht ein bisschen seltener benutzt werden, also nicht ein paarmal die Minute, sondern nur ein paarmal die Stunde, dann kann ich halt über End-to-End-Tests die Nutzung simulieren und kann dafür sorgen, dass wenn ich einen End-to-End-Test alle fünf Minuten laufen lasse, dass das Ding dann alle fünf Minuten benutzt wird. Und wenn da ein Fehler auftritt, dann ist nur mein End-to-End-Test ein bisschen enttäuscht, aber kein echter Benutzer. Trotzdem habe ich darüber aber mein Monitoring und mein Alerting angetriggert. Oder kann direkt aus dem End-to-End-Test ein Feedback geben: Achtung, hier funktioniert gerade irgendetwas nicht, hier komme ich gerade nicht von A nach B. Wie gesagt, das testet dann nicht nur Fehler, die durch ein Deployment oder durch eine Änderung reingebracht worden sind, sondern automatisch auch Fehler, die durch andere Ausfälle entstehen können.
Lucas Dohmen: Oder durch Konfigurationsfehler im Ops-Bereich oder so etwas.
Torsten Mandry: Ganz genau.
Lucas Dohmen: Und wenn man das in Produktion macht, wie geht man da mit Testdaten um? Je nachdem, was man für ein System braucht, könnte es ja auch sein, dass dadurch, wie man mit dem System interagiert, auch andere Benutzer betroffen werden als wenn man den Test immer wieder laufen lässt.
Torsten Mandry: Genau, da muss man dann ein bisschen kritischer drauf gucken. Das sind dann andere Probleme, um die ich mir dann Gedanken machen muss. Viele solche End-to-End-Tests kann ich vielleicht machen, ohne überhaupt Daten zu erzeugen, das hängt immer ein bisschen vom System ab, aber wenn ich beispielsweise im Online-Shop-System in der Suchen-und-Entdecken-Welt unterwegs bin, könnte ich mir vorstellen, dass ich da gerade im Produktivsystem, wo ich Daten habe, irgendwas suchen kann. Je nachdem, wonach ich da suche, bin ich auch sehr sicher, dass ich auf jeden Fall etwas finden werde. Vielleicht ändert sich das, was ich da finde, von Zeit zu Zeit, weil sich halt die Daten ein bisschen ändern, aber auch da wieder, im End-to-End-Test, will ich nicht so genau hingucken, was ich da genau finde und wie das aussieht, sondern ich will gucken: Finde ich etwas oder kriege ich einen Fehler? Und dafür reicht das. Und wenn ich tatsächlich Daten erzeugen muss, um irgendetwas zu testen, dann gibt es vielleicht auch wieder verschiedene Wege, wie ich das machen kann. Aus dem Kontext meines Projektes kann ich erzählen - es ist nichts, an dem ich aktiv beteiligt war - dass das, was da über alle Vertikalen hinweg läuft, auch End-to-End-Tests sind, die einen Happy-Path im Shop durchtesten. Das heißt alle paar Minuten einen Kunden über die Oberfläche durch die Suche und die Produktdetailseite hindurch leiten, durch den Warenkorb, durch den Checkout und wo ein Testbenutzer, ein automatisierter Benutzer, dann halt wirklich eine Bestellung platziert. Und da sind wir beispielsweise den Weg gegangen - oder die Kollegen, die das gemacht haben - dass diese Nutzer sich immer mit einer Email-Adresse aus einer bestimmten Domäne, mit einem bestimmten Suffix registrieren, sodass ich dadurch eine Möglichkeit habe, hinterher diese Bestellung wieder rauszufiltern. Ansonsten muss ich vielleicht gucken, dass ich andere Möglichkeiten finde, die Bestellung komplett wieder rauszulöschen aus dem Shop-System. Aber das kann unter Umständen einfacher sein, als mit dem Risiko zu leben, dass ich da vielleicht irgendwelche potentiellen Fehler in der Produktion habe, die ich eben nicht so ohne Weiteres finde oder die meine Kunden bzw. meine Endnutzer zuerst finden.
Lucas Dohmen: Wenn ich also keine End-to-End-Tests mache, bevor ich in Produktion gehe, wie kann ich denn das Risiko von Fehlern in der Produktion trotzdem noch reduzieren, damit ich mich sicherer fühle zu deployen?
Torsten Mandry: Im Grunde genommen ist auch das wieder analog zu der Art und Weise wie ich innerhalb eines Systems mit Komponenten und Komponententests umgehe. Da versuche ich halt auch, nicht nur aus Testgründen, auch aus anderen Gründen, eine hohe Kohäsion und lose Kopplung hinzukriegen. Das heißt die Dinge zusammenzupacken bzw. zusammenzuhalten, die auch wirklich zusammen gehören und eine Einheit bilden und die halt möglichst lose nur mit anderen Teilen zu verbinden. Und genau das kann ich auf der Ebene von Microservices auch versuchen, das heißt es geht ein bisschen darum, zu überlegen: Wie schneide ich meine Services? Sie insbesondere in einer Art und Weise zu schneiden, dass ich in einem Service möglichst viel von dem drin habe, was für einen bestimmten Use-Case, für ein Szenario, wie wir sie für die End-to-End-Tests formuliert hatten, wichtig ist. Sodass ich das innerhalb der End-to-End-Tests von diesem Service testen kann. Da ist beispielsweise Domain-driven-Design mit dem Bounded Context ein guter Ansatz, wo ich genau diese abgeschlossenen Einheiten von Teilen meiner Domäne und meiner Geschäftslogik habe. Ein anderer Ansatz, wenn ich eine UI mit inkludiert habe, wäre beispielsweise statt von einem Frontend-Monolithen und vielen Microservices dadrunter, die aber dann nur eine API anbieten, zu einem sogenannten Self-Contained-System zu kommen, das heißt zu einer Einheit, die wirklich die Domäne, die Geschäftslogik und auch die entsprechende UI beinhaltet. Sodass ich Use-Cases, die die UI und die Geschäftslogik inkludieren, da auch innerhalb eines Services testen kann. Je besser ich das schaffe, desto geringer wird die Notwendigkeit, dass ich die Interaktion, die Verbindung zwischen den Services noch testen muss. Ich kriege die natürlich nicht ganz weg, klar, ich muss irgendwie hinkriegen, die Services miteinander sprechen zu lassen, das muss immer noch funktionieren. Aber das Risiko wird aus meiner Sicht geringer und reduziert sich dann am Ende vielleicht nur noch darauf, ob ich die richtigen Parameter für die entsprechende Umgebung, wo das Ding gerade läuft, gesetzt habe. Und ob ich die notwendige API erfülle.
Lucas Dohmen: Das spricht alles etwas dafür, auch etwas größere Microservices zu haben, nicht diese 100 Zeilen Microservices, sondern schon etwas, das ein bisschen mehr kann.
Torsten Mandry: Das hängt von dem Anwendungsfall, von dem Use-Case ab, den der Service abdecken soll. Das kann aus meiner Sicht auch etwas Kleines, etwas sehr Überschaubares sein, da ist nicht so sehr die Größe wichtig, sondern wie sehr es zusammengehört und wie sehr es sich von anderen Bereichen abtrennen lässt. Wenn ich merke, dass ich einen relativ kleinen Service habe, der aber ein Kommunikations-Ping-Pong mit einem anderen Service spielt, wo aber wieder Requests und Responses über die Leitung gehen, ist das vielleicht ein Zeichen dafür, dass die Services an der Stelle noch nicht richtig geschnitten sind.
Lucas Dohmen: Wenn du das jetzt zusammenfassen würdest, was würdest du sagen hast du aus deiner Erfahrung gelernt?
Torsten Mandry: Die Erfahrung hat mich nicht davon abgebracht, End-to-End-Tests gut zu finden. Sie sind für mich nach wie vor ein wichtiges und hilfreiches Werkzeug, um festzustellen, dass Dinge insgesamt so funktionieren, wie sie sollen. Aber ich habe gelernt, dass sie über Servicegrenzen hinweg in der Regel keine gute Idee sind, also zumindest nicht innerhalb des Builds und des Deployments, sondern dass man da das Augenmerk eher auf das Produktivsystem legen muss und andere Mechanismen, andere Hilfsmittel finden sollte, um möglichst schnell Probleme zu erkennen - wenn denn welche in Produktion sind, unabhängig davon, wo die herkommen. Ob die aus Changes kommen oder ob die aufgrund von anderen Gegebenheiten entstehen.
Lucas Dohmen: Gut, dann danke ich dir für das Gespräch und den Hörerinnen und Hörern, bis zum nächsten Mal. Auf Wiedersehen!
Torsten Mandry: Tschüss!