In Projekten, die mit der Entwicklung von Microservices-Architekturen zu tun haben, wird CI/CD von einer netten Option zu einer zwingenden Notwendigkeit. Automatisiertes Testen ist ein wesentlicher Bestandteil der kontinuierlichen Integration. Ein durchdachter Ansatz kann dem Team viele angenehme Abende mit Familie und Freunden schenken. Andernfalls riskiert das Projekt, niemals abgeschlossen zu werden.
Man kann den gesamten Code eines Microservices mit Unit-Tests und Mock-Objekten abdecken, aber das löst das Problem nur teilweise und hinterlässt viele Fragen und Herausforderungen, insbesondere bei der Testung der Datenverarbeitung. Besonders kritisch sind das Testen der Datenkonsistenz in relationalen Datenbanken, das Testen der Interaktion mit Cloud-Diensten und falsche Annahmen beim Schreiben von Mock-Objekten.
All dies und noch mehr wird durch das Testen des gesamten Microservices in einem Docker-Container gelöst. Ein unbestreitbarer Vorteil für die Validität der Tests ist, dass die gleichen Docker-Images, die in die Produktion gehen, auch getestet werden.
Die Automatisierung eines solchen Ansatzes bringt eine Reihe von Herausforderungen mit sich, deren Lösungen weiter unten beschrieben werden.
- Konflikte bei parallelen Aufgaben auf einem Docker-Host;
- Konflikte bei Datenbank-IDs während der Testiterationen;
- Warten auf die Verfügbarkeit von Mikrodiensten;
- Kombination und Ausgabe von Logs an externe Systeme;
- Testen von ausgehenden HTTP-Anfragen;
- Testen von Web-Sockets (unter Verwendung von SignalR);
- Testen der OAuth-Authentifizierung und -Autorisierung.
Dieser Artikel ist inspiriert von SECR 2019. Für diejenigen, die nicht gerne lesen, .

In diesem Artikel erkläre ich, wie man mit einem Skript einen zu testenden Dienst, eine Datenbank und Amazon AWS-Dienste in Docker startet, anschließend Tests mit Postman durchführt und nach Abschluss die erstellten Container stoppt und löscht. Die Tests werden bei jeder Codeänderung durchgeführt. Auf diese Weise stellen wir sicher, dass jede Version korrekt mit der Datenbank und den AWS-Diensten funktioniert.
Dasselbe Skript wird sowohl von den Entwicklern auf ihren Windows-Desktops als auch vom Gitlab CI-Server unter Linux ausgeführt.
Um die Implementierung neuer Tests zu rechtfertigen, darf diese keine zusätzlichen Werkzeuge auf dem Computer des Entwicklers oder auf dem Server erfordern, auf dem die Tests bei einem Commit ausgeführt werden. Docker löst dieses Problem.
Der Test muss aus folgenden Gründen auf einem lokalen Server laufen:
- Ein Netzwerk ist nie vollkommen zuverlässig. Von tausend Anfragen kann eine nicht durchkommen;
Ein automatischer Test würde in diesem Fall fehlschlagen, die Arbeit würde gestoppt und man müsste die Ursache in den Logs suchen; - Zu häufige Anfragen sind bei manchen Drittanbieterdiensten nicht erlaubt.
Zudem ist es unerwünscht, eine Testumgebung zu nutzen, weil:
- Nicht nur schlechter Code, der auf dieser läuft, kann die Testumgebung beschädigen, sondern auch Daten, die von gutem Code nicht verarbeitet werden können;
- So sehr wir uns auch bemühen, alle Änderungen, die durch den Test vorgenommen werden, während des Tests zurückzusetzen, kann etwas schiefgehen (ansonsten, warum gibt es Tests?).
Über das Projekt und den Prozess der Organisation
Unser Unternehmen hat eine Microservice-Webanwendung entwickelt, die in Docker in der Cloud von Amazon AWS läuft. Im Projekt wurden bereits Unit-Tests verwendet, jedoch traten häufig Fehler auf, die von den Unit-Tests nicht erkannt wurden. Es war notwendig, den gesamten Microservice zusammen mit der Datenbank und den Amazon-Diensten zu testen.
Im Projekt wird ein standardisierter Prozess für die kontinuierliche Integration angewendet, der das Testen von Mikrodiensten bei jedem Commit umfasst. Nach Zuweisung der Aufgabe nimmt der Entwickler Änderungen am Mikrodienst vor, testet diesen manuell und führt alle vorhandenen automatisierten Tests aus. Bei Bedarf passt der Entwickler die Tests an. Wenn keine Probleme festgestellt werden, erfolgt der Commit in den Branch dieser Aufgabe. Nach jedem Commit werden die Tests automatisch auf dem Server ausgeführt. Der Merge in den Haupt-Branch und der Start der automatisierten Tests dort erfolgen nach einer erfolgreichen Überprüfung. Wenn die Tests im Haupt-Branch bestanden werden, wird der Dienst automatisch in der Testumgebung auf dem Amazon Elastic Container Service (Staging) aktualisiert. Diese Staging-Umgebung wird von allen Entwicklern und Testern benötigt und sollte nicht beeinträchtigt werden. Die Tester überprüfen in dieser Umgebung den Fix oder das neue Feature durch manuelle Tests.
Architektur des Projekts

Die Anwendung besteht aus mehr als zehn Diensten. Einige von ihnen sind in .NET Core, andere in NodeJs geschrieben. Jeder Dienst läuft in einem Docker-Container im Amazon Elastic Container Service. Jeder hat seine eigene Postgres-Datenbank, und einige haben zusätzlich Redis. Es gibt keine gemeinsamen Datenbanken. Wenn mehrere Dienste dieselben Daten benötigen, werden diese Daten bei ihrer Änderung über den SNS (Simple Notification Service) und SQS (Amazon Simple Queue Service) an jeden dieser Dienste übermittelt, und die Dienste speichern sie in ihren eigenen isolierten Datenbanken.
SQS und SNS
SQS ermöglicht es, Nachrichten über das HTTPS-Protokoll in eine Warteschlange zu legen und Nachrichten aus der Warteschlange zu lesen.
Wenn mehrere Dienste eine Warteschlange lesen, erhält jede Nachricht nur einen dieser Dienste. Dies ist nützlich, wenn mehrere Exemplare eines Dienstes gestartet werden, um die Last zwischen ihnen zu verteilen.
Wenn jede Nachricht an mehrere Dienste zugestellt werden muss, benötigt jeder Empfänger seine eigene Warteschlange, und um Nachrichten in mehrere Warteschlangen zu duplizieren, ist ein SNS erforderlich.
In SNS erstellen Sie ein Topic und abonnieren beispielsweise eine SQS-Warteschlange. Nachrichten können an dieses Topic gesendet werden. Dabei wird jede Nachricht an jede Warteschlange geschickt, die auf dieses Topic abonniert ist. In SNS gibt es keine Methode zum Lesen von Nachrichten. Wenn während der Fehlersuche oder des Testens erforderlich ist, zu erfahren, was an SNS gesendet wird, können Sie eine SQS-Warteschlange erstellen, sie an das gewünschte Topic abonnieren und die Warteschlange lesen.

API Gateway
Die meisten Dienste sind nicht direkt aus dem Internet erreichbar. Der Zugriff erfolgt über das API Gateway, das die Zugriffsrechte überprüft. Dies ist auch unser Dienst, und dafür gibt es ebenfalls Tests.
Echtzeit-Benachrichtigungen
Die Anwendung verwendet , um dem Benutzer Echtzeit-Benachrichtigungen anzuzeigen. Dies wird im Benachrichtigungsdienst realisiert. Er ist direkt aus dem Internet zugänglich und arbeitet selbst mit OAuth, da es sich als unpraktisch erwies, die Unterstützung für Websockets in das Gateway zu integrieren, im Vergleich zur Integration von OAuth und dem Benachrichtigungsdienst.
Ein bekannter Ansatz für Tests
Unit-Tests ersetzen Dinge wie Datenbanken durch Mock-Objekte. Wenn ein Mikrodienst beispielsweise versucht, einen Datensatz in einer Tabelle mit einem Fremdschlüssel zu erstellen, der auf einen nicht vorhandenen Datensatz verweist, kann die Anfrage nicht ausgeführt werden. Unit-Tests können das nicht erkennen.
In es wird empfohlen, eine In-Memory-Datenbank zu verwenden und Mock-Objekte einzuführen.
Eine In-Memory-Datenbank ist eine der von Entity Framework unterstützten Datenbanken. Sie wurde speziell für Tests entwickelt. Die Daten in einer solchen Datenbank werden nur bis zum Ende des Prozesses gespeichert, der sie verwendet. Es müssen keine Tabellen erstellt werden, und die Integrität der Daten wird nicht überprüft.
Mock-Objekte modellieren eine ersetzbare Klasse nur in dem Maße, wie der Entwickler des Tests deren Funktionsweise versteht.
Wie man die automatische Ausführung von Postgres und die Durchführung von Migrationen beim Starten des Tests erreicht, wird im Artikel von Microsoft nicht angegeben. Meine Lösung macht dies und fügt dem Mikrodienst darüber hinaus keinen speziellen Code für Tests hinzu.
Kommen wir zur Lösung
Im Verlauf der Entwicklung wurde klar, dass Unit-Tests nicht ausreichten, um alle Probleme rechtzeitig zu identifizieren. Daher entschlossen wir uns, das Thema aus einer anderen Perspektive anzugehen.
Einrichten der Testumgebung
Die erste Aufgabe besteht darin, die Testumgebung bereitzustellen. Die Schritte, die erforderlich sind, um den Mikrodienst zu starten, sind:
- Den zu testenden Dienst in der lokalen Umgebung einrichten, in den Umgebungsvariablen die Zugangsdaten für die Verbindung zur Datenbank und zu AWS angeben;
- Postgres starten und die Migration durch Ausführung von Liquibase durchführen.
In relationalen Datenbanksystemen muss vor dem Schreiben von Daten in die Datenbank zunächst ein Datenschema erstellt werden, einfach gesagt: Tabellen. Bei der Aktualisierung der Anwendung müssen die Tabellen an das Format der neuen Version angepasst werden, und dies idealerweise ohne Datenverlust. Dies wird als Migration bezeichnet. Das Erstellen von Tabellen in einer ursprünglich leeren Datenbank ist ein Spezialfall der Migration. Die Migration kann in die Anwendung selbst integriert werden. Sowohl in .NET als auch in NodeJS gibt es Frameworks für Migrationen. In unserem Fall sind die Mikrodienste aus Sicherheitsgründen ohne das Recht, das Datenschema zu ändern, und die Migration erfolgt mithilfe von Liquibase. - Starten Sie Amazon LocalStack. Dies ist eine Implementierung von AWS-Services, die lokal betrieben werden kann. Für LocalStack gibt es ein vorgefertigtes Image im Docker Hub.
- Führen Sie das Skript aus, um die erforderlichen Entitäten in LocalStack zu erstellen. Shell-Skripte verwenden das AWS CLI.
Für das Projekt wird getestet . Es gab ihn bereits, aber früher wurde er manuell gestartet und die Anwendung, die bereits bereitgestellt war, getestet. Dieses Tool ermöglicht es, beliebige HTTP(S)-Anfragen zu erstellen und zu überprüfen, ob die Antworten den Erwartungen entsprechen. Die Anfragen werden in einer Sammlung zusammengefasst, und die gesamte Sammlung kann auf einmal ausgeführt werden.

Wie der automatische Test funktioniert
Während des Tests läuft alles in Docker: der zu testende Dienst, Postgres, das Migrationswerkzeug und Postman, genauer gesagt, die Konsolenversion – Newman.
Docker löst eine Reihe von Problemen:
- Unabhängigkeit von der Host-Konfiguration;
- Abhängigkeiten installieren: Docker zieht Images aus dem Docker Hub;
- Systemzustand zurücksetzen: Container einfach löschen.
Docker-compose verbindet die Container in ein virtuelles Netzwerk, das vom Internet isoliert ist, in dem die Container sich über Domänennamen finden.
Der Test wird durch ein Shell-Skript gesteuert. Um den Test unter Windows zu starten, verwenden wir git-bash. Somit benötigt man lediglich ein Skript sowohl für Windows als auch für Linux. Git und Docker sind bei allen Entwicklern im Projekt installiert. Bei der Installation von Git unter Windows wird git-bash installiert, daher haben es auch alle.
Das Skript führt folgende Schritte aus:
- Erstellung der Docker-Images
docker-compose build - Start der Datenbank und LocalStack
docker-compose up -d - Migration der Datenbank und Vorbereitung von LocalStack
docker-compose run - Start des zu testenden Dienstes
docker-compose up -d - Start des Tests (Newman)
- Stoppen aller Container
docker-compose down - Posten der Ergebnisse in Slack
Wir haben einen Chat, in den Nachrichten mit grünem Häkchen oder rotem Kreuz und einem Link zum Log gelangen.
In diesen Schritten sind folgende Docker-Images beteiligt:
- Der zu testende Dienst verwendet dasselbe Image wie in der Produktion. Die Konfiguration für den Test erfolgt über Umgebungsvariablen.
- Für Postgres, Redis und LocalStack werden fertige Images aus Docker Hub verwendet. Auch für Liquibase und Newman gibt es fertige Images. Wir bauen unsere eigenen darauf auf, indem wir unsere Dateien hinzufügen.
- Zur Vorbereitung von LocalStack wird ein fertiges AWS CLI-Image verwendet, und darauf basierend wird ein Image erstellt, das das Skript enthält.
Durch die Verwendung von , es ist nicht nötig, ein Docker-Image nur zum Hinzufügen von Dateien zum Container zu erstellen. Allerdings sind Volumes für unser Umfeld nicht geeignet, da die Gitlab CI-Jobs selbst in Containern laufen. Aus einem solchen Container kann Docker verwaltet werden, aber Volumes binden nur Ordner vom Host-System und nicht aus einem anderen Container.
Mögliche Probleme
Warten auf Bereitstellung
Wenn der Container mit dem Dienst läuft, bedeutet das noch nicht, dass er bereit ist, Verbindungen anzunehmen. Man muss auf die Verbindung warten, um fortzufahren.
Diese Aufgabe wird manchmal mit einem Skript gelöst , das darauf wartet, dass eine TCP-Verbindung hergestellt werden kann. Allerdings kann LocalStack einen 502 Bad Gateway-Fehler ausgeben. Außerdem besteht es aus mehreren Diensten, und wenn einer bereit ist, sagt das nichts über die anderen aus.
Lösung: Vorbereitende Skripte für LocalStack, die auf eine 200-Antwort sowohl von SQS als auch von SNS warten.
Konflikte zwischen parallelen Aufgaben
Mehrere Tests können gleichzeitig auf einem Docker-Host ausgeführt werden, daher müssen die Namen der Container und Netzwerke eindeutig sein. Darüber hinaus können Tests aus verschiedenen Branches desselben Dienstes ebenfalls gleichzeitig laufen, sodass es nicht ausreicht, in jeder Compose-Datei eigene Namen zu definieren.
Lösung: Das Skript setzt einen einzigartigen Wert für die Variable COMPOSE_PROJECT_NAME.
Besonderheiten von Windows
Bei der Verwendung von Docker unter Windows gibt es einige Dinge, die ich Ihrer Aufmerksamkeit empfehlen möchte, da dieses Wissen für das Verständnis der Fehlerursachen wichtig ist.
- Shell-Skripte im Container müssen Unix-Zeilenenden haben.
Ein CR-Zeichen im Shell-Skript ist ein Syntaxfehler. Aus der Fehlermeldung geht oft nicht hervor, dass dies der Grund ist. Beim Bearbeiten solcher Skripte unter Windows ist ein geeigneter Texteditor erforderlich. Zudem muss das Versionskontrollsystem korrekt konfiguriert sein.
So konfigurieren Sie Git:
git config core.autocrlf input- Git Bash emuliert die Standard-Linux-Ordner und ersetzt beim Aufruf einer exe-Datei (einschließlich docker.exe) absolute Linux-Pfade durch Windows-Pfade. Dieses Verhalten kann jedoch nicht für Pfade, die nicht auf der lokalen Maschine liegen (oder Pfade im Container), deaktiviert werden.
Lösung: Einen zusätzlichen Slash am Anfang des Pfades hinzufügen: //bin anstelle von /bin. Linux erkennt solche Pfade, für ihn sind mehrere Slashes das gleiche wie einer. Aber git-bash erkennt solche Pfade nicht und versucht nicht, sie zu konvertieren.
Log-Ausgabe
Bei der Durchführung von Tests möchte ich sowohl die Logs von Newman als auch vom getesteten Dienst sehen. Da die Ereignisse dieser Logs miteinander verbunden sind, ist es viel praktischer, sie in einer Konsole zu kombinieren, als in zwei separaten Dateien. Newman wird überdocker-compose run
, und deshalb wird seine Ausgabe in die Konsole geleitet. Es bleibt zu gewährleisten, dass auch die Ausgabe des Dienstes dort erscheint. docker-compose up Die ursprüngliche Lösung bestand darin, dies -dohne Flag
, aber unter Nutzung der Shell-Funktionen diesen Prozess in den Hintergrund zu schicken:docker-compose up <service> & docker-compose up Das funktionierte, bis es erforderlich wurde, die Logs aus Docker an einen externen Dienst zu senden. Es hörte auf, Logs in die Konsole auszugeben. Die Kommandozeile.
Lösung:
docker attachdocker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<dienst>_1 &
Widerspruch von Identifikatoren bei Iterationen des Tests
Um dies zu vermeiden, müssen entweder die IDs einzigartig sein oder alle durch den Test erstellten Objekte müssen gelöscht werden. Einige Objekte dürfen nicht gelöscht werden, gemäß den Anforderungen.
Lösung: GUIDs müssen mit Skripten in Postman generiert werden.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);Dann in der Anfrage das Zeichen verwenden {{myUUID}}, das durch den Wert der Variablen ersetzt wird.
Interaktion über LocalStack
Wenn der zu testende Service eine SQS-Warteschlange liest oder in sie schreibt, muss auch der Test selbst mit dieser Warteschlange arbeiten, um dies zu überprüfen.
Lösung: Anfragen von Postman an LocalStack.
Die API-Dienste von AWS sind dokumentiert, was Anfragen ohne SDK ermöglicht.
Wenn der Dienst Nachrichten in die Warteschlange schreibt, lesen wir diese und überprüfen den Inhalt der Nachricht.
Wenn der Dienst Nachrichten an SNS sendet, wird in der Vorbereitungsphase von LocalStack zusätzlich eine Warteschlange eingerichtet und auf dieses SNS-Thema abonniert. Alles Weitere entspricht dem oben Beschriebenen.
Wenn der Dienst eine Nachricht aus der Warteschlange lesen soll, schreiben wir diese Nachricht im vorherigen Schritt des Tests in die Warteschlange.
Testen von HTTP-Anfragen, die von dem zu testenden Mikrodienst ausgehen.
Einige Dienste arbeiten über HTTP mit anderen Services als AWS, und einige AWS-Funktionen sind in LocalStack nicht umgesetzt.
Lösung: in solchen Fällen kann helfen , der ein vorgefertigtes Image in hat. Erwartete Anfragen und Antworten darauf werden mit einer HTTP-Anfrage konfiguriert. Die API ist dokumentiert, daher führen wir Anfragen über Postman aus.
Testen der OAuth-Authentifizierung und -Autorisierung
Wir verwenden OAuth und . Für den Test benötigen wir einen OAuth-Anbieter, den wir lokal starten können.
Die gesamte Interaktion des Dienstes mit dem OAuth-Anbieter besteht aus zwei Anfragen: Zuerst wird die Konfiguration angefragt /.well-known/openid-configuration, danach wird der öffentliche Schlüssel (JWKS) von der in der Konfiguration angegebenen Adresse angefragt. All dies ist statischer Inhalt.
Lösung: unser Test-OAuth-Anbieter ist ein Server für statische Inhalte und zwei Dateien darauf. Der Token wurde einmal generiert und in Git eingecheckt.
Besonderheiten beim Testen von SignalR
Postman funktioniert nicht mit Websockets. Für das Testen von SignalR wurde ein spezielles Tool erstellt.
Der SignalR-Client kann nicht nur ein Browser sein. Es gibt eine Client-Bibliothek für .NET Core. Der in .NET Core geschriebene Client stellt eine Verbindung her, authentifiziert sich und wartet auf eine bestimmte Nachrichtenfolge. Wenn eine unerwartete Nachricht empfangen wird oder die Verbindung abbricht, wird der Client mit dem Code 1 beendet. Bei Empfang der letzten erwarteten Nachricht endet er mit dem Code 0.
Gleichzeitig läuft Newman mit dem Client. Es werden mehrere Clients gestartet, um sicherzustellen, dass die Nachrichten an alle relevanten Empfänger zugestellt werden.

Um mehrere Clients zu starten, wird die Option —scale in der Docker-Compose-Befehlszeile verwendet.
Vor dem Start des Postman-Skripts wartet es darauf, dass alle Clients eine Verbindung herstellen.
Das Problem mit dem Warten auf die Verbindung haben wir bereits erlebt. Dort waren es Server, hier ist es der Client. Ein anderer Ansatz ist erforderlich.
Lösung: Der Client im Container verwendet den Mechanismus , um dem Skript auf dem Host seinen Status zu melden. Der Client erstellt eine Datei an einem bestimmten Pfad, beispielsweise /healthcheck, sobald die Verbindung hergestellt ist. Das HealthCheck-Skript in der Docker-Datei sieht folgendermaßen aus:
HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fiDer Befehl docker inspect zeigt den regulären Status, den Gesundheitsstatus und den Exit-Code des Containers an.
Nach dem Abschluss von Newman überprüft das Skript, dass alle Container mit dem Client beendet wurden, und zwar mit dem Code 0.
Glück ist da
Nachdem wir die oben beschriebenen Herausforderungen überwunden haben, verfügen wir nun über einen Satz stabil funktionierender Tests. In den Tests funktioniert jeder Dienst als Einheit, interagiert mit der Datenbank und mit Amazon LocalStack.
Diese Tests schützen das Team von über 30 Entwicklern vor Fehlern in der Anwendung, die komplexe Interaktionen von mehr als 10 Mikrodiensten bei häufigen Deployments betreffen.
Quelle: habr.com
