Tests automatisés de microservices dans Docker pour une intégration continue

Dans les projets liés au développement d'architectures de microservices, le CI/CD passe de la catégorie d'opportunité agréable à celle de nécessité urgente. Les tests automatisés font partie intégrante de l'intégration continue, dont une approche compétente peut offrir à l'équipe de nombreuses soirées agréables en famille et entre amis. Sinon, le projet risque de ne jamais être achevé.

Il est possible de couvrir l'intégralité du code du microservice avec des tests unitaires avec des objets fictifs, mais cela ne résout que partiellement le problème et laisse de nombreuses questions et difficultés, en particulier lors des tests de travail avec des données. Comme toujours, les plus urgents consistent à tester la cohérence des données dans une base de données relationnelle, à tester le travail avec les services cloud et à formuler des hypothèses incorrectes lors de l'écriture d'objets fictifs.

Tout cela et bien plus encore peut être résolu en testant l’intégralité du microservice dans un conteneur Docker. Un avantage incontestable pour garantir la validité des tests est que les mêmes images Docker qui entrent en production sont testées.

L'automatisation de cette approche présente un certain nombre de problèmes, dont la solution sera décrite ci-dessous :

  • conflits de tâches parallèles dans le même hôte Docker ;
  • conflits d'identifiants dans la base de données lors des itérations de test ;
  • attendre que les microservices soient prêts ;
  • fusion et sortie des journaux vers des systèmes externes ;
  • tester les requêtes HTTP sortantes ;
  • test de socket Web (à l'aide de SignalR) ;
  • tester l'authentification et l'autorisation OAuth.

Cet article est basé sur mon discours au SECR 2019. Alors pour ceux qui ont la flemme de lire, voici un enregistrement du discours.

Tests automatisés de microservices dans Docker pour une intégration continue

Dans cet article, je vais vous expliquer comment utiliser un script pour exécuter le service testé, une base de données et les services Amazon AWS dans Docker, puis des tests sur Postman et, une fois terminés, arrêter et supprimer les conteneurs créés. Les tests sont exécutés à chaque fois que le code change. De cette façon, nous nous assurons que chaque version fonctionne correctement avec la base de données et les services AWS.

Le même script est exécuté à la fois par les développeurs eux-mêmes sur leurs bureaux Windows et par le serveur Gitlab CI sous Linux.

Pour être justifié, l'introduction de nouveaux tests ne devrait nécessiter l'installation d'outils supplémentaires ni sur l'ordinateur du développeur, ni sur le serveur sur lequel les tests sont exécutés sur un commit : Docker résout ce problème.

Le test doit s'exécuter sur un serveur local pour les raisons suivantes :

  • Le réseau n'est jamais complètement fiable. Sur mille demandes, une peut échouer ;
    Dans ce cas, le test automatique ne fonctionnera pas, le travail s'arrêtera et vous devrez rechercher la raison dans les journaux ;
  • Les demandes trop fréquentes ne sont pas autorisées par certains services tiers.

De plus, il n'est pas souhaitable d'utiliser le support car :

  • Un stand peut être brisé non seulement par un mauvais code exécuté dessus, mais également par des données que le code correct ne peut pas traiter ;
  • Peu importe les efforts que nous déployons pour annuler toutes les modifications apportées par le test pendant le test lui-même, quelque chose peut mal se passer (sinon, pourquoi tester ?).

À propos de l'organisation du projet et du processus

Notre société a développé une application Web de microservice exécutée dans Docker dans le cloud Amazon AWS. Les tests unitaires étaient déjà utilisés sur le projet, mais des erreurs se produisaient souvent que les tests unitaires ne détectaient pas. Il était nécessaire de tester un microservice complet ainsi que la base de données et les services Amazon.

Le projet utilise un processus d'intégration continue standard, qui comprend le test du microservice à chaque validation. Après avoir attribué une tâche, le développeur apporte des modifications au microservice, le teste manuellement et exécute tous les tests automatisés disponibles. Si nécessaire, le développeur modifie les tests. Si aucun problème n’est détecté, une validation est effectuée dans la branche de ce problème. Après chaque commit, des tests sont automatiquement exécutés sur le serveur. La fusion dans une branche commune et le lancement de tests automatiques sur celle-ci se produisent après un examen réussi. Si les tests sur la branche partagée réussissent, le service est automatiquement mis à jour dans l'environnement de test sur Amazon Elastic Container Service (bench). Le support est nécessaire à tous les développeurs et testeurs, et il est déconseillé de le casser. Les testeurs de cet environnement vérifient un correctif ou une nouvelle fonctionnalité en effectuant des tests manuels.

Architecture du projet

Tests automatisés de microservices dans Docker pour une intégration continue

L'application comprend plus de dix services. Certains d'entre eux sont écrits en .NET Core et d'autres en NodeJs. Chaque service s'exécute dans un conteneur Docker dans Amazon Elastic Container Service. Chacun possède sa propre base de données Postgres, et certains disposent également de Redis. Il n'existe pas de bases de données communes. Si plusieurs services ont besoin des mêmes données, alors ces données, lorsqu'elles changent, sont transmises à chacun de ces services via SNS (Simple Notification Service) et SQS (Amazon Simple Queue Service), et les services les enregistrent dans leurs propres bases de données distinctes.

SQS et SNS

SQS vous permet de mettre des messages dans une file d'attente et de lire les messages de la file d'attente à l'aide du protocole HTTPS.

Si plusieurs services lisent une file d'attente, chaque message n'arrive qu'à l'un d'entre eux. Ceci est utile lors de l'exécution de plusieurs instances du même service pour répartir la charge entre elles.

Si vous souhaitez que chaque message soit remis à plusieurs services, chaque destinataire doit avoir sa propre file d'attente et SNS est nécessaire pour dupliquer les messages dans plusieurs files d'attente.

Dans SNS, vous créez un sujet et vous y abonnez, par exemple une file d'attente SQS. Vous pouvez envoyer des messages au sujet. Dans ce cas, le message est envoyé à chaque file d'attente abonnée à cette rubrique. SNS n'a pas de méthode pour lire les messages. Si lors du débogage ou des tests, vous avez besoin de savoir ce qui est envoyé à SNS, vous pouvez créer une file d'attente SQS, l'abonner au sujet souhaité et lire la file d'attente.

Tests automatisés de microservices dans Docker pour une intégration continue

Passerelle API

La plupart des services ne sont pas directement accessibles depuis Internet. L'accès se fait via API Gateway, qui vérifie les droits d'accès. C'est aussi notre service, et il existe également des tests pour cela.

Notifications en temps réel

L'application utilise Signal Rpour afficher des notifications en temps réel à l'utilisateur. Ceci est implémenté dans le service de notification. Il est accessible directement depuis Internet et fonctionne lui-même avec OAuth, car il s'est avéré peu pratique de prendre en charge les sockets Web dans Gateway, par rapport à l'intégration d'OAuth et du service de notification.

Approche de test bien connue

Les tests unitaires remplacent des éléments comme la base de données par des objets fictifs. Si un microservice, par exemple, tente de créer un enregistrement dans une table avec une clé étrangère et que l'enregistrement référencé par cette clé n'existe pas, la requête ne peut pas être exécutée. Les tests unitaires ne peuvent pas détecter cela.

В article de Microsoft Il est proposé d'utiliser une base de données en mémoire et d'implémenter des objets fictifs.

La base de données en mémoire est l'un des SGBD pris en charge par Entity Framework. Il a été créé spécifiquement pour les tests. Les données d'une telle base de données ne sont stockées que jusqu'à la fin du processus qui l'utilise. Il ne nécessite pas de création de tables et ne vérifie pas l'intégrité des données.

Les objets fictifs modélisent la classe qu'ils remplacent uniquement dans la mesure où le développeur du test comprend son fonctionnement.

Comment faire en sorte que Postgres démarre et effectue automatiquement des migrations lorsque vous exécutez un test n'est pas spécifié dans l'article de Microsoft. Ma solution fait cela et, en outre, n'ajoute aucun code spécifiquement pour les tests au microservice lui-même.

Passons à la solution

Au cours du processus de développement, il est devenu évident que les tests unitaires n'étaient pas suffisants pour détecter tous les problèmes en temps opportun. Il a donc été décidé d'aborder cette question sous un angle différent.

Mise en place d'un environnement de test

La première tâche consiste à déployer un environnement de test. Étapes requises pour exécuter un microservice :

  • Configurez le service testé pour l'environnement local, spécifiez les détails de connexion à la base de données et à AWS dans les variables d'environnement ;
  • Démarrez Postgres et effectuez la migration en exécutant Liquibase.
    Dans les SGBD relationnels, avant d'écrire des données dans la base de données, vous devez créer un schéma de données, c'est-à-dire des tables. Lors de la mise à jour d'une application, les tableaux doivent être amenés sous la forme utilisée par la nouvelle version, et, de préférence, sans perte de données. C’est ce qu’on appelle la migration. Créer des tables dans une base de données initialement vide est un cas particulier de migration. La migration peut être intégrée à l'application elle-même. .NET et NodeJS disposent tous deux de frameworks de migration. Dans notre cas, pour des raisons de sécurité, les microservices sont privés du droit de modifier le schéma des données, et la migration s'effectue à l'aide de Liquibase.
  • Lancez Amazon LocalStack. Il s'agit d'une implémentation des services AWS à exécuter à la maison. Il existe une image prête à l'emploi pour LocalStack sur Docker Hub.
  • Exécutez le script pour créer les entités nécessaires dans LocalStack. Les scripts Shell utilisent l'AWS CLI.

Utilisé pour les tests sur le projet Facteur. Il existait auparavant, mais il était lancé manuellement et testait une application déjà déployée sur le stand. Cet outil vous permet de faire des requêtes HTTP(S) arbitraires et de vérifier si les réponses correspondent aux attentes. Les requêtes sont combinées dans une collection et la collection entière peut être exécutée.

Tests automatisés de microservices dans Docker pour une intégration continue

Comment fonctionne le test automatique ?

Lors du test, tout fonctionne dans Docker : le service testé, Postgres, l'outil de migration, et Postman, ou plutôt sa version console - Newman.

Docker résout un certain nombre de problèmes :

  • Indépendance de la configuration de l'hôte ;
  • Installation des dépendances : Docker télécharge les images depuis Docker Hub ;
  • Remettre le système dans son état d'origine : simplement retirer les conteneurs.

Docker-composer rassemble les conteneurs dans un réseau virtuel, isolé d'Internet, dans lequel les conteneurs se retrouvent par noms de domaine.

Le test est contrôlé par un script shell. Pour exécuter le test sous Windows, nous utilisons git-bash. Ainsi, un seul script suffit pour Windows et Linux. Git et Docker sont installés par tous les développeurs du projet. Lors de l'installation de Git sur Windows, git-bash est installé, donc tout le monde l'a aussi.

Le script effectue les étapes suivantes :

  • Création d'images Docker
    docker-compose build
  • Lancement de la base de données et de LocalStack
    docker-compose up -d <контейнер>
  • Migration de base de données et préparation de LocalStack
    docker-compose run <контейнер>
  • Lancement du service en test
    docker-compose up -d <сервис>
  • Exécution du test (Newman)
  • Arrêter tous les conteneurs
    docker-compose down
  • Publication des résultats dans Slack
    Nous avons un chat où vont les messages avec une coche verte ou une croix rouge et un lien vers le journal.

Les images Docker suivantes sont impliquées dans ces étapes :

  • Le service testé est la même image que pour la production. La configuration du test se fait via des variables d'environnement.
  • Pour Postgres, Redis et LocalStack, des images prêtes à l'emploi de Docker Hub sont utilisées. Il existe également des images prêtes à l'emploi pour Liquibase et Newman. Nous construisons le nôtre sur leur squelette, en y ajoutant nos fichiers.
  • Pour préparer LocalStack, vous utilisez une image AWS CLI prête à l'emploi et créez une image contenant un script basé sur celle-ci.

Utilisation volumes, vous n'avez pas besoin de créer une image Docker uniquement pour ajouter des fichiers au conteneur. Cependant, les volumes ne sont pas adaptés à notre environnement car les tâches Gitlab CI elles-mêmes s'exécutent dans des conteneurs. Vous pouvez contrôler Docker à partir d'un tel conteneur, mais les volumes montent uniquement les dossiers à partir du système hôte, et non à partir d'un autre conteneur.

Problèmes que vous pouvez rencontrer

En attente de préparation

Lorsqu'un conteneur avec un service est en cours d'exécution, cela ne signifie pas qu'il est prêt à accepter les connexions. Vous devez attendre que la connexion continue.

Ce problème est parfois résolu à l'aide d'un script attends-le.sh, qui attend une opportunité d'établir une connexion TCP. Cependant, LocalStack peut générer une erreur 502 Bad Gateway. De plus, il se compose de nombreux services, et si l'un d'eux est prêt, cela ne dit rien des autres.

décision: scripts de provisionnement LocalStack qui attendent une réponse 200 de SQS et SNS.

Conflits de tâches parallèles

Plusieurs tests peuvent s'exécuter simultanément sur le même hôte Docker, les noms de conteneur et de réseau doivent donc être uniques. De plus, les tests de différentes branches d'un même service peuvent également s'exécuter simultanément, il ne suffit donc pas d'écrire leurs noms dans chaque fichier de composition.

décision: Le script définit la variable COMPOSE_PROJECT_NAME sur une valeur unique.

Fonctionnalités Windows

Il y a un certain nombre de choses que je souhaite souligner lors de l'utilisation de Docker sous Windows, car ces expériences sont importantes pour comprendre pourquoi des erreurs se produisent.

  1. Les scripts Shell dans un conteneur doivent avoir des fins de ligne Linux.
    Le symbole Shell CR est une erreur de syntaxe. Il est difficile de dire à partir du message d'erreur que c'est le cas. Lors de la modification de tels scripts sous Windows, vous avez besoin d'un éditeur de texte approprié. De plus, le système de contrôle de version doit être configuré correctement.

Voici comment git est configuré :

git config core.autocrlf input

  1. Git-bash émule les dossiers Linux standard et, lors de l'appel d'un fichier exe (y compris docker.exe), remplace les chemins Linux absolus par les chemins Windows. Cependant, cela n'a pas de sens pour les chemins qui ne se trouvent pas sur la machine locale (ou pour les chemins dans un conteneur). Ce comportement ne peut pas être désactivé.

décision: ajoutez une barre oblique supplémentaire au début du chemin : //bin au lieu de /bin. Linux comprend de tels chemins ; pour lui, plusieurs barres obliques équivalent à une seule. Mais git-bash ne reconnaît pas ces chemins et n'essaie pas de les convertir.

Sortie du journal

Lors de l'exécution de tests, j'aimerais voir les journaux de Newman et du service testé. Étant donné que les événements de ces journaux sont interconnectés, leur combinaison dans une seule console est bien plus pratique que deux fichiers distincts. Newman se lance via exécution de docker-compose, et donc sa sortie se retrouve dans la console. Il ne reste plus qu'à s'assurer que la sortie du service y passe également.

La solution originale était de faire docker-compose jusqu'à pas de drapeau -d, mais en utilisant les capacités du shell, envoyez ce processus en arrière-plan :

docker-compose up <service> &

Cela a fonctionné jusqu'à ce qu'il soit nécessaire d'envoyer les journaux de Docker vers un service tiers. docker-compose jusqu'à arrêté de générer des journaux sur la console. Cependant, l'équipe a travaillé attacher docker.

décision:

docker attach --no-stdin ${COMPOSE_PROJECT_NAME}_<сервис>_1 &

Conflit d'identifiant lors des itérations de test

Les tests sont exécutés en plusieurs itérations. La base de données n'est pas effacée. Les enregistrements de la base de données ont des identifiants uniques. Si nous notons des identifiants spécifiques dans les requêtes, nous obtiendrons un conflit à la deuxième itération.

Pour éviter cela, soit les identifiants doivent être uniques, soit tous les objets créés par le test doivent être supprimés. Certains objets ne peuvent pas être supprimés en raison d'exigences.

décision: générer des GUID à l'aide de scripts Postman.

var uuid = require('uuid');
var myid = uuid.v4();
pm.environment.set('myUUID', myid);

Utilisez ensuite le symbole dans la requête {{myUUID}}, qui sera remplacé par la valeur de la variable.

Collaboration via LocalStack

Si le service testé lit ou écrit dans une file d'attente SQS, alors pour le vérifier, le test lui-même doit également fonctionner avec cette file d'attente.

décision: requêtes de Postman à LocalStack.

L'API des services AWS est documentée, permettant d'effectuer des requêtes sans SDK.

Si un service écrit dans une file d'attente, nous le lisons et vérifions le contenu du message.

Si le service envoie des messages à SNS, au stade de la préparation, LocalStack crée également une file d'attente et s'abonne à ce sujet SNS. Ensuite, tout se résume à ce qui a été décrit ci-dessus.

Si le service a besoin de lire un message dans la file d'attente, lors de l'étape de test précédente, nous écrivons ce message dans la file d'attente.

Tester les requêtes HTTP provenant du microservice testé

Certains services fonctionnent via HTTP avec autre chose qu'AWS, et certaines fonctionnalités AWS ne sont pas implémentées dans LocalStack.

décision: dans ces cas-là, cela peut aider Serveur fictif, qui a une image prête à l'emploi dans Docker Hub. Les requêtes attendues et leurs réponses sont configurées par une requête HTTP. L'API est documentée, nous faisons donc des demandes auprès de Postman.

Test de l'authentification et de l'autorisation OAuth

Nous utilisons OAuth et Jetons Web JSON (JWT). Le test nécessite un fournisseur OAuth que nous pouvons exécuter localement.

Toute interaction entre le service et le fournisseur OAuth se résume à deux requêtes : premièrement, la configuration est demandée /.well-known/openid-configuration, puis la clé publique (JWKS) est demandée à l'adresse de la configuration. Tout cela est du contenu statique.

décision: Notre fournisseur OAuth de test est un serveur de contenu statique et deux fichiers dessus. Le jeton est généré une fois et validé dans Git.

Caractéristiques des tests SignalR

Postman ne fonctionne pas avec les websockets. Un outil spécial a été créé pour tester SignalR.

Un client SignalR peut être plus qu'un simple navigateur. Il existe une bibliothèque cliente sous .NET Core. Le client, écrit en .NET Core, établit une connexion, est authentifié et attend une séquence spécifique de messages. Si un message inattendu est reçu ou si la connexion est perdue, le client quitte avec un code de 1. Si le dernier message attendu est reçu, le client quitte avec un code de 0.

Newman travaille simultanément avec le client. Plusieurs clients sont lancés pour vérifier que les messages sont délivrés à tous ceux qui en ont besoin.

Tests automatisés de microservices dans Docker pour une intégration continue

Pour exécuter plusieurs clients, utilisez l'option --échelle sur la ligne de commande docker-compose.

Avant de s'exécuter, le script Postman attend que tous les clients établissent des connexions.
Nous avons déjà rencontré le problème de l'attente d'une connexion. Mais il y avait des serveurs, et voici le client. Une approche différente est nécessaire.

décision: le client dans le conteneur utilise le mécanisme Bilan de santépour informer le script sur l'hôte de son statut. Le client crée un fichier sur un chemin spécifique, par exemple /healthcheck, dès que la connexion est établie. Le script HealthCheck dans le fichier Docker ressemble à ceci :

HEALTHCHECK --interval=3s CMD if [ ! -e /healthcheck ]; then false; fi

Équipe docker inspecter Affiche l'état normal, l'état de santé et le code de sortie du conteneur.

Une fois Newman terminé, le script vérifie que tous les conteneurs avec le client sont terminés, avec le code 0.

Le bonheur existe

Après avoir surmonté les difficultés décrites ci-dessus, nous avons effectué une série de tests de fonctionnement stables. Lors des tests, chaque service fonctionne comme une unité unique, interagissant avec la base de données et Amazon LocalStack.

Ces tests protègent une équipe de plus de 30 développeurs contre les erreurs dans une application avec une interaction complexe de plus de 10 microservices avec des déploiements fréquents.

Source: habr.com

Ajouter un commentaire