Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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 meine Rede beim SECR 2019. Also für diejenigen, die zu faul zum Lesen sind: Hier ist eine Aufzeichnung der Rede.

Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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

Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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.

Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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 SignalRum dem Benutzer Echtzeitbenachrichtigungen anzuzeigen. Dies ist im Benachrichtigungsdienst implementiert. Es ist direkt aus dem Internet zugänglich und funktioniert selbst mit OAuth, da es sich im Vergleich zur Integration von OAuth und dem Benachrichtigungsdienst als unpraktisch erwies, Unterstützung für Web-Sockets in Gateway zu integrieren.

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.

В Artikel von Microsoft Es wird vorgeschlagen, eine In-Memory-Datenbank zu verwenden und Scheinobjekte zu implementieren.

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 Postman. Es existierte bereits zuvor, wurde jedoch manuell gestartet und testete eine bereits am Stand bereitgestellte Anwendung. Mit diesem Tool können Sie beliebige HTTP(S)-Anfragen stellen und prüfen, ob die Antworten den Erwartungen entsprechen. Abfragen werden zu einer Sammlung zusammengefasst und die gesamte Sammlung kann ausgeführt werden.

Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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 Volumen, müssen Sie kein Docker-Image erstellen, nur um Dateien zum Container hinzuzufügen. Allerdings sind Volumes für unsere Umgebung nicht geeignet, da Gitlab CI-Aufgaben selbst in Containern laufen. Sie können Docker von einem solchen Container aus steuern, Volumes mounten jedoch nur Ordner vom Hostsystem und nicht von einem anderen Container.

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 warte-darauf.sh, der auf eine Gelegenheit zum Aufbau einer TCP-Verbindung wartet. Allerdings kann LocalStack den Fehler 502 Bad Gateway auslösen. Darüber hinaus besteht es aus vielen Diensten, und wenn einer davon bereit ist, sagt das nichts über die anderen aus.

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.

  1. 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

  1. 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 MockServer, das ein fertiges Bild enthält Docker-Hub. Erwartete Anfragen und Antworten darauf werden durch eine HTTP-Anfrage konfiguriert. Die API ist dokumentiert, daher stellen wir Anfragen an Postman.

Testen der OAuth-Authentifizierung und -Autorisierung

Wir verwenden OAuth und JSON-Web-Token (JWT). Für den Test ist ein OAuth-Anbieter erforderlich, den wir lokal ausführen können.

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.

Automatisiertes Testen von Microservices in Docker für kontinuierliche Integration

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 Gesundheitskontrolleum das Skript auf dem Host über seinen Status zu informieren. Sobald die Verbindung hergestellt ist, erstellt der Client eine Datei unter einem bestimmten Pfad, beispielsweise /healthcheck. Das HealthCheck-Skript in der Docker-Datei sieht folgendermaßen aus:

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

Kommentar hinzufügen