In Projekten rund um die Entwicklung von Microservice-Architekturen bewegt sich CI/CD von der Kategorie einer angenehmen Gelegenheit in die Kategorie einer dringenden Notwendigkeit. Automatisiertes Testen ist ein integraler Bestandteil der kontinuierlichen Integration, ein kompetenter Ansatz, der dem Team viele angenehme Abende mit Familie und Freunden bescheren kann. Andernfalls besteht die Gefahr, dass das Projekt nie abgeschlossen wird.
Es ist möglich, den gesamten Microservice-Code durch Unit-Tests mit Scheinobjekten abzudecken, dies löst das Problem jedoch nur teilweise und hinterlässt viele Fragen und Schwierigkeiten, insbesondere bei der Testarbeit mit Daten. Zu den dringendsten gehören wie immer das Testen der Datenkonsistenz in einer relationalen Datenbank, das Testen der Arbeit mit Cloud-Diensten und das Treffen falscher Annahmen beim Schreiben von Scheinobjekten.
All dies und noch etwas mehr lässt sich lösen, indem man den gesamten Microservice in einem Docker-Container testet. Ein zweifelloser Vorteil für die Sicherstellung der Validität von Tests besteht darin, dass dieselben Docker-Images getestet werden, die in die Produktion gehen.
Die Automatisierung dieses Ansatzes bringt eine Reihe von Problemen mit sich, deren Lösung im Folgenden beschrieben wird:
- Konflikte paralleler Aufgaben im selben Docker-Host;
- Bezeichnerkonflikte in der Datenbank während Testiterationen;
- Warten darauf, dass Microservices bereit sind;
- Protokolle zusammenführen und an externe Systeme ausgeben;
- Testen ausgehender HTTP-Anfragen;
- Web-Socket-Tests (mit SignalR);
- Testen der OAuth-Authentifizierung und -Autorisierung.
Dieser Artikel basiert auf
In diesem Artikel erkläre ich Ihnen, wie Sie mit einem Skript den zu testenden Dienst, eine Datenbank und Amazon AWS-Dienste in Docker ausführen, dann Tests auf Postman durchführen und nach Abschluss die erstellten Container stoppen und löschen. Tests werden jedes Mal ausgeführt, wenn sich der Code ändert. Auf diese Weise stellen wir sicher, dass jede Version korrekt mit der AWS-Datenbank und den AWS-Diensten funktioniert.
Das gleiche Skript wird sowohl von den Entwicklern selbst auf ihren Windows-Desktops als auch vom Gitlab CI-Server unter Linux ausgeführt.
Um gerechtfertigt zu sein, sollte die Einführung neuer Tests nicht die Installation zusätzlicher Tools erfordern, weder auf dem Computer des Entwicklers noch auf dem Server, auf dem die Tests auf einem Commit ausgeführt werden. Docker löst dieses Problem.
Der Test muss aus folgenden Gründen auf einem lokalen Server ausgeführt werden:
- Das Netzwerk ist nie absolut zuverlässig. Von tausend Anfragen kann eine scheitern;
In diesem Fall funktioniert der automatische Test nicht, die Arbeit wird unterbrochen und Sie müssen in den Protokollen nach der Ursache suchen; - Zu häufige Anfragen werden von einigen Drittanbieterdiensten nicht zugelassen.
Darüber hinaus ist die Verwendung des Ständers aus folgenden Gründen unerwünscht:
- Ein Stand kann nicht nur dadurch beschädigt werden, dass darauf fehlerhafter Code läuft, sondern auch durch Daten, die der richtige Code nicht verarbeiten kann;
- Ganz gleich, wie sehr wir uns auch bemühen, alle vom Test während des Tests selbst vorgenommenen Änderungen rückgängig zu machen, es kann etwas schief gehen (warum sonst testen?).
Über die Projekt- und Ablauforganisation
Unser Unternehmen hat eine Microservice-Webanwendung entwickelt, die in Docker in der Amazon AWS-Cloud läuft. Im Projekt kamen bereits Unit-Tests zum Einsatz, allerdings traten häufig Fehler auf, die durch die Unit-Tests nicht erkannt wurden. Es war notwendig, einen gesamten Microservice zusammen mit der Datenbank und den Amazon-Diensten zu testen.
Das Projekt verwendet einen standardmäßigen kontinuierlichen Integrationsprozess, der das Testen des Microservices bei jedem Commit umfasst. Nach der Zuweisung einer Aufgabe nimmt der Entwickler Änderungen am Microservice vor, testet ihn manuell und führt alle verfügbaren automatisierten Tests durch. Bei Bedarf ändert der Entwickler die Tests. Wenn keine Probleme gefunden werden, erfolgt ein Commit für den Zweig dieses Problems. Nach jedem Commit werden automatisch Tests auf dem Server ausgeführt. Nach einer erfolgreichen Überprüfung erfolgt die Zusammenführung in einen gemeinsamen Zweig und das Starten automatischer Tests darauf. Wenn die Tests im gemeinsam genutzten Zweig erfolgreich sind, wird der Dienst automatisch in der Testumgebung auf Amazon Elastic Container Service (Bench) aktualisiert. Der Ständer ist für alle Entwickler und Tester notwendig und es ist nicht ratsam, ihn zu zerbrechen. Tester in dieser Umgebung prüfen einen Fix oder eine neue Funktion, indem sie manuelle Tests durchführen.
Projektarchitektur
Die Anwendung besteht aus mehr als zehn Diensten. Einige davon sind in .NET Core geschrieben, andere in NodeJs. Jeder Dienst wird in einem Docker-Container im Amazon Elastic Container Service ausgeführt. Jeder verfügt über eine eigene Postgres-Datenbank und einige verfügen auch über Redis. Es gibt keine gemeinsamen Datenbanken. Benötigen mehrere Dienste die gleichen Daten, so werden diese Daten, wenn sie sich ändern, über SNS (Simple Notification Service) und SQS (Amazon Simple Queue Service) an jeden dieser Dienste übermittelt und von den Diensten in jeweils eigenen Datenbanken gespeichert.
SQS und SNS
Mit SQS können Sie mithilfe des HTTPS-Protokolls Nachrichten in eine Warteschlange stellen und Nachrichten aus der Warteschlange lesen.
Wenn mehrere Dienste eine Warteschlange lesen, kommt jede Nachricht nur bei einem von ihnen an. Dies ist nützlich, wenn mehrere Instanzen desselben Dienstes ausgeführt werden, um die Last zwischen ihnen zu verteilen.
Wenn jede Nachricht an mehrere Dienste übermittelt werden soll, muss jeder Empfänger über eine eigene Warteschlange verfügen und SNS ist erforderlich, um Nachrichten in mehrere Warteschlangen zu duplizieren.
In SNS erstellen Sie ein Thema und abonnieren es, beispielsweise eine SQS-Warteschlange. Sie können Nachrichten an das Thema senden. In diesem Fall wird die Nachricht an jede Warteschlange gesendet, die dieses Thema abonniert hat. SNS verfügt nicht über eine Methode zum Lesen von Nachrichten. Wenn Sie beim Debuggen oder Testen herausfinden müssen, was an SNS gesendet wird, können Sie eine SQS-Warteschlange erstellen, diese beim gewünschten Thema abonnieren und die Warteschlange lesen.
API-Gateway
Die meisten Dienste sind nicht direkt über das Internet zugänglich. Der Zugriff erfolgt über API Gateway, welches die Zugriffsrechte prüft. Das ist auch unser Service und es gibt auch Tests dazu.
Echtzeitbenachrichtigungen
Die Anwendung verwendet
Bekannter Testansatz
Unit-Tests ersetzen Dinge wie die Datenbank durch Scheinobjekte. Wenn ein Microservice beispielsweise versucht, einen Datensatz in einer Tabelle mit einem Fremdschlüssel zu erstellen, und der Datensatz, auf den dieser Schlüssel verweist, nicht existiert, kann die Anfrage nicht abgeschlossen werden. Unit-Tests können dies nicht erkennen.
В
Die In-Memory-Datenbank ist eines der vom Entity Framework unterstützten DBMS. Es wurde speziell zum Testen erstellt. Daten in einer solchen Datenbank werden nur so lange gespeichert, bis der Prozess, der sie verwendet, beendet wird. Das Erstellen von Tabellen ist nicht erforderlich und die Datenintegrität wird nicht überprüft.
Scheinobjekte modellieren die Klasse, die sie ersetzen, nur in dem Maße, in dem der Testentwickler versteht, wie sie funktioniert.
Wie Sie Postgres dazu bringen, automatisch zu starten und Migrationen durchzuführen, wenn Sie einen Test ausführen, wird im Microsoft-Artikel nicht beschrieben. Meine Lösung erledigt dies und fügt dem Microservice selbst darüber hinaus keinen Code speziell für Tests hinzu.
Kommen wir zur Lösung
Während des Entwicklungsprozesses wurde klar, dass Unit-Tests nicht ausreichten, um alle Probleme rechtzeitig zu finden. Daher wurde beschlossen, dieses Problem aus einem anderen Blickwinkel anzugehen.
Einrichten einer Testumgebung
Die erste Aufgabe besteht darin, eine Testumgebung bereitzustellen. Erforderliche Schritte zum Ausführen eines Microservices:
- Konfigurieren Sie den zu testenden Dienst für die lokale Umgebung, geben Sie die Details für die Verbindung zur Datenbank und AWS in den Umgebungsvariablen an;
- Starten Sie Postgres und führen Sie die Migration durch, indem Sie Liquibase ausführen.
In relationalen DBMS müssen Sie vor dem Schreiben von Daten in die Datenbank ein Datenschema, also Tabellen, erstellen. Bei der Aktualisierung einer Anwendung müssen die Tabellen in die Form gebracht werden, die von der neuen Version verwendet wird, und zwar möglichst ohne Datenverlust. Dies nennt man Migration. Das Erstellen von Tabellen in einer zunächst leeren Datenbank ist ein Sonderfall der Migration. Die Migration kann in die Anwendung selbst integriert werden. Sowohl .NET als auch NodeJS verfügen über Migrationsframeworks. In unserem Fall wird Microservices aus Sicherheitsgründen das Recht entzogen, das Datenschema zu ändern, und die Migration wird mithilfe von Liquibase durchgeführt. - Starten Sie Amazon LocalStack. Dies ist eine Implementierung von AWS-Diensten zur Ausführung zu Hause. Auf Docker Hub gibt es ein fertiges Image für LocalStack.
- Führen Sie das Skript aus, um die erforderlichen Entitäten in LocalStack zu erstellen. Shell-Skripte verwenden die AWS CLI.
Wird zum Testen des Projekts verwendet
Wie funktioniert der automatische Test?
Während des Tests funktioniert alles in Docker: der zu testende Dienst, Postgres, das Migrationstool und Postman bzw. dessen Konsolenversion – Newman.
Docker löst eine Reihe von Problemen:
- Unabhängigkeit von der Hostkonfiguration;
- Abhängigkeiten installieren: Docker lädt Bilder vom Docker Hub herunter;
- Das System in seinen ursprünglichen Zustand zurückversetzen: Einfaches Entfernen der Behälter.
Docker-Compose vereint Container zu einem vom Internet isolierten virtuellen Netzwerk, in dem sich Container über Domänennamen finden.
Der Test wird durch ein Shell-Skript gesteuert. Um den Test unter Windows auszuführen, verwenden wir git-bash. Somit reicht ein Skript sowohl für Windows als auch für Linux. Git und Docker werden von allen Entwicklern im Projekt installiert. Bei der Installation von Git unter Windows wird git-bash installiert, sodass das auch jeder hat.
Das Skript führt die folgenden Schritte aus:
- Docker-Images erstellen
docker-compose build
- Starten der Datenbank und LocalStack
docker-compose up -d <контейнер>
- Datenbankmigration und Vorbereitung von LocalStack
docker-compose run <контейнер>
- Starten des zu testenden Dienstes
docker-compose up -d <сервис>
- Den Test durchführen (Newman)
- Stoppen aller Container
docker-compose down
- Ergebnisse in Slack veröffentlichen
Wir haben einen Chat, in den Nachrichten mit einem grünen Häkchen oder einem roten Kreuz und einem Link zum Protokoll gehen.
An diesen Schritten sind die folgenden Docker-Images beteiligt:
- Der getestete Dienst ist das gleiche Image wie für die Produktion. Die Konfiguration für den Test erfolgt über Umgebungsvariablen.
- Für Postgres, Redis und LocalStack werden vorgefertigte Images von Docker Hub verwendet. Es gibt auch vorgefertigte Bilder für Liquibase und Newman. Wir bauen unsere auf ihrem Grundgerüst auf und fügen dort unsere Dateien hinzu.
- Um LocalStack vorzubereiten, verwenden Sie ein vorgefertigtes AWS CLI-Image und erstellen ein Image, das ein darauf basierendes Skript enthält.
Mit
Mögliche Probleme
Warten auf Bereitschaft
Wenn ein Container mit einem Dienst ausgeführt wird, bedeutet dies nicht, dass er bereit ist, Verbindungen anzunehmen. Sie müssen warten, bis die Verbindung hergestellt wird.
Dieses Problem wird manchmal mithilfe eines Skripts gelöst
Lösung: LocalStack-Bereitstellungsskripts, die auf eine 200-Antwort von SQS und SNS warten.
Parallele Aufgabenkonflikte
Mehrere Tests können gleichzeitig auf demselben Docker-Host ausgeführt werden, daher müssen Container- und Netzwerknamen eindeutig sein. Darüber hinaus können auch Tests aus verschiedenen Zweigen desselben Dienstes gleichzeitig ausgeführt werden, sodass es nicht ausreicht, ihre Namen in jede Compose-Datei zu schreiben.
Lösung: Das Skript setzt die Variable COMPOSE_PROJECT_NAME auf einen eindeutigen Wert.
Windows-Funktionen
Bei der Verwendung von Docker unter Windows möchte ich auf einige Dinge hinweisen, da diese Erfahrungen wichtig sind, um zu verstehen, warum Fehler auftreten.
- Shell-Skripte in einem Container müssen Linux-Zeilenenden haben.
Das Shell-CR-Symbol ist ein Syntaxfehler. Anhand der Fehlermeldung lässt sich nur schwer erkennen, dass dies der Fall ist. Wenn Sie solche Skripte unter Windows bearbeiten, benötigen Sie einen geeigneten Texteditor. Darüber hinaus muss das Versionskontrollsystem ordnungsgemäß konfiguriert sein.
So ist Git konfiguriert:
git config core.autocrlf input
- Git-bash emuliert Standard-Linux-Ordner und ersetzt beim Aufruf einer exe-Datei (einschließlich docker.exe) absolute Linux-Pfade durch Windows-Pfade. Dies ist jedoch für Pfade, die sich nicht auf dem lokalen Computer befinden (oder Pfade in einem Container), nicht sinnvoll. Dieses Verhalten kann nicht deaktiviert werden.
Lösung: Fügen Sie am Anfang des Pfads einen zusätzlichen Schrägstrich hinzu: //bin anstelle von /bin. Linux versteht solche Pfade; bei ihm sind mehrere Schrägstriche gleich einem. Aber git-bash erkennt solche Pfade nicht und versucht nicht, sie zu konvertieren.
Protokollausgabe
Beim Ausführen von Tests möchte ich Protokolle sowohl von Newman als auch vom getesteten Dienst sehen. Da die Ereignisse dieser Protokolle miteinander verbunden sind, ist es viel praktischer, sie in einer Konsole zusammenzufassen als zwei separate Dateien. Newman startet über Docker-Compose-Ausführung, und so landet seine Ausgabe in der Konsole. Es bleibt nur noch sicherzustellen, dass der Output des Dienstes auch dort ankommt.
Die ursprüngliche Lösung bestand darin, dies zu tun Docker-komponieren keine Flagge -d, aber senden Sie diesen Prozess mithilfe der Shell-Funktionen in den Hintergrund:
docker-compose up <service> &
Dies funktionierte, bis es notwendig wurde, Protokolle von Docker an einen Drittanbieterdienst zu senden. Docker-komponieren Die Ausgabe von Protokollen an die Konsole wurde gestoppt. Das Team hat jedoch funktioniert Docker anbringen.
Lösung:
docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<сервис>_1 &
Bezeichnerkonflikt während Testiterationen
Tests werden in mehreren Iterationen durchgeführt. Die Datenbank wird nicht gelöscht. Datensätze in der Datenbank haben eindeutige IDs. Wenn wir bestimmte IDs in Anfragen aufschreiben, kommt es bei der zweiten Iteration zu einem Konflikt.
Um dies zu vermeiden, müssen entweder die IDs eindeutig sein oder alle durch den Test erstellten Objekte müssen gelöscht werden. Einige Objekte können aufgrund von Anforderungen nicht gelöscht werden.
Lösung: GUIDs mithilfe von Postman-Skripten generieren.
var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);
Verwenden Sie dann das Symbol in der Abfrage {{myUUID}}, der durch den Wert der Variablen ersetzt wird.
Zusammenarbeit über LocalStack
Wenn der zu testende Dienst in eine SQS-Warteschlange liest oder schreibt, muss der Test selbst zur Überprüfung auch mit dieser Warteschlange arbeiten.
Lösung: Anfragen von Postman an LocalStack.
Die AWS-Services-API ist dokumentiert, sodass Abfragen ohne SDK durchgeführt werden können.
Wenn ein Dienst in eine Warteschlange schreibt, lesen wir diese und prüfen den Inhalt der Nachricht.
Wenn der Dienst Nachrichten an SNS sendet, erstellt LocalStack in der Vorbereitungsphase auch eine Warteschlange und abonniert dieses SNS-Thema. Dann kommt es darauf an, was oben beschrieben wurde.
Wenn der Dienst eine Nachricht aus der Warteschlange lesen muss, schreiben wir diese Nachricht im vorherigen Testschritt in die Warteschlange.
Testen von HTTP-Anfragen, die vom zu testenden Microservice stammen
Einige Dienste funktionieren über HTTP mit etwas anderem als AWS und einige AWS-Funktionen sind in LocalStack nicht implementiert.
Lösung: In diesen Fällen kann es helfen
Testen der OAuth-Authentifizierung und -Autorisierung
Wir verwenden OAuth und
Die gesamte Interaktion zwischen dem Dienst und dem OAuth-Anbieter läuft auf zwei Anfragen hinaus: Zunächst wird die Konfiguration angefordert /.well-known/openid-configuration, und dann wird der öffentliche Schlüssel (JWKS) an der Adresse aus der Konfiguration angefordert. Das alles sind statische Inhalte.
Lösung: Unser Test-OAuth-Anbieter ist ein statischer Inhaltsserver und zwei Dateien darauf. Das Token wird einmal generiert und an Git übergeben.
Merkmale des SignalR-Tests
Postman funktioniert nicht mit Websockets. Zum Testen von SignalR wurde ein spezielles Tool erstellt.
Ein SignalR-Client kann mehr als nur ein Browser sein. Unter .NET Core gibt es dafür eine Client-Bibliothek. Der in .NET Core geschriebene Client stellt eine Verbindung her, wird authentifiziert und wartet auf eine bestimmte Nachrichtenfolge. Wenn eine unerwartete Nachricht empfangen wird oder die Verbindung unterbrochen wird, beendet sich der Client mit dem Code 1. Wenn die letzte erwartete Nachricht empfangen wird, beendet sich der Client mit dem Code 0.
Newman arbeitet gleichzeitig mit dem Kunden. Es werden mehrere Clients gestartet, um zu überprüfen, ob die Nachrichten an jeden zugestellt werden, der sie benötigt.
Um mehrere Clients auszuführen, verwenden Sie die Option --Skala in der Docker-Compose-Befehlszeile.
Vor der Ausführung wartet das Postman-Skript darauf, dass alle Clients Verbindungen herstellen.
Wir sind bereits auf das Problem gestoßen, auf eine Verbindung zu warten. Aber es gab Server, und hier ist der Client. Ein anderer Ansatz ist erforderlich.
Lösung: Der Client im Container verwendet den Mechanismus
HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fi
Team Docker inspizieren Zeigt den Normalstatus, den Gesundheitsstatus und den Exit-Code für den Container an.
Nachdem Newman fertig ist, prüft das Skript, ob alle Container mit dem Client beendet wurden, mit Code 0.
Glück gibt es
Nachdem wir die oben beschriebenen Schwierigkeiten überwunden hatten, führten wir eine Reihe stabiler Lauftests durch. In Tests arbeitet jeder Dienst als einzelne Einheit und interagiert mit der Datenbank und Amazon LocalStack.
Diese Tests schützen ein Team von mehr als 30 Entwicklern vor Fehlern in einer Anwendung mit komplexer Interaktion von mehr als 10 Microservices mit häufigen Bereitstellungen.
Source: habr.com