Comment tester une application Go avec RSpec dans une boîte noire

Les tests automatisés font actuellement fureur dans le développement Web et sont répandus dans l'ensemble du secteur. Un test bien rédigé réduit considérablement le risque de rupture accidentelle d'une application lorsque vous ajoutez de nouvelles fonctionnalités ou corrigez des bugs. Lorsque vous avez un système complexe construit à partir de plusieurs composants qui interagissent, il est extrêmement difficile de tester l’interaction de chaque composant avec d’autres.

Voyons comment écrire de bons tests automatiques pour développer des composants dans Go et comment le faire à l’aide de la bibliothèque RSpec dans Ruby on Rails.

Ajout de Go à la pile technologique de notre projet

L'un des projets sur lesquels je travaille dans ma société, eTeam, peut être divisé en un panneau d'administration, un tableau de bord utilisateur, un générateur de rapports et un processeur de requêtes qui traite les demandes de différents services intégrés à l'application.

La partie du projet qui traite les demandes est la plus importante. Nous devions donc maximiser sa fiabilité et sa disponibilité.

Dans le cadre d’une application monolithique, il existe un risque élevé de dysfonctionnement du processeur de requêtes, même en cas de modification du code dans des parties de l’application qui n’y sont pas liées. De même, le processeur de requêtes risque de tomber en panne lorsque les autres composants sont soumis à une charge importante. Le nombre de travailleurs Ngnix pour l'application est limité, ce qui peut poser des problèmes lorsque la charge augmente. Par exemple, lorsqu'un certain nombre de pages gourmandes en ressources sont ouvertes simultanément dans le panneau d'administration, le processeur ralentit voire bloque l'application entière.

Ces risques, ainsi que la maturité du système en question - nous n’avons pas eu à apporter de modifications majeures pendant des mois - ont fait de cette application un candidat idéal pour la création d’un service séparé permettant de gérer le traitement des demandes.

Nous avons décidé d'écrire le service séparé dans Go, qui partageait l'accès à la base de données avec l'application Rails qui restait responsable des modifications apportées à la structure de la table. Avec seulement deux applications, un tel schéma avec une base de données partagée fonctionne bien. Voici à quoi cela ressemblait:

Nous avons écrit et déployé le service dans une instance distincte de Rails. De cette façon, il était inutile de craindre que le processeur de requêtes ne soit affecté chaque fois que l'application Rails était déployée. Le service accepte directement les requêtes HTTP sans Ngnix et n’utilise pas beaucoup de mémoire. Vous pourriez l'appeler une application minimaliste!

Le problème avec les tests unitaires dans Go

Nous avons créé des tests unitaires pour l'application Go, où toutes les demandes de base de données ont été simulées. Outre les autres arguments de cette solution, l’application principale Rails était responsable de la structure de la base de données. L’application Go ne disposait donc pas des informations nécessaires à la création d’une base de données de test. La moitié du traitement était de la logique métier, l’autre moitié était constituée de requêtes sur la base de données, qui étaient toutes simulées.

Les objets moqués sont beaucoup moins lisibles en Go qu'en Ruby. Chaque fois que de nouvelles fonctions étaient ajoutées pour lire les données de la base de données, nous devions ajouter des objets fictifs au cours de nombreux tests infructueux qui avaient fonctionné auparavant. En fin de compte, ces tests unitaires ne se sont pas révélés très efficaces et extrêmement fragiles.

Notre solution

Afin de pallier ces inconvénients, nous avons décidé de couvrir le service avec des tests fonctionnels dans l'application Rails et de tester le service dans Go comme une boîte noire. Les tests en boîte blanche ne fonctionneraient en aucun cas, car il était impossible d’utiliser Ruby pour accéder au service et voir si une méthode était appelée.

Cela signifie également que les demandes envoyées via le service de test étaient également impossibles à simuler. Nous avions donc besoin d'une autre application pour gérer et écrire ces tests. Quelque chose comme RequestBin fonctionnerait, mais il devait fonctionner localement. Nous avions déjà écrit un utilitaire qui ferait l'affaire, alors nous avons décidé d'essayer de l'utiliser.

C'était la configuration résultante:

  1. RSpec compile et exécute le binaire Go avec la configuration dans laquelle l'accès à la base de données de test est spécifié, ainsi qu'un port particulier pour la réception de requêtes HTTP, à savoir 8082.
  2. Il exécute également l'utilitaire, qui enregistre les demandes HTTP arrivant sur le port 8083.
  3. Nous écrivons des tests réguliers dans RSpec. Cela crée les données nécessaires dans la base de données et envoie une demande à localhost: 8082 comme s'il s'agissait d'un service externe tel que HTTParty.
  4. Nous analysons la réponse, vérifions les modifications dans la base de données, recevons une liste des demandes enregistrées par le substitut de RequestBin et les vérifions.

Détails de la mise en œuvre

Voici comment nous avons implémenté cela. En guise de démonstration, appelons le service de test TheService et créons un wrapper:

Il est à noter que le chargement automatique de fichiers doit être configuré dans le dossier de support lorsque vous utilisez RSpec:

Dir [Rails.root.join ('spec / support / ** / * .rb')]. Each {| f | besoin de f}

La méthode de départ:

  • Lit les informations de configuration nécessaires pour démarrer TheService. Ces informations peuvent différer selon les développeurs et sont donc exclues de Git. La configuration contient les paramètres nécessaires au démarrage du programme. Toutes ces différentes configurations sont regroupées au même endroit, vous n’avez donc pas à créer de fichiers inutiles.
  • Compile et exécute go run
  • Sondages toutes les secondes et attend que TheService soit prêt à accepter les demandes.
  • Enregistre l'identifiant de chaque processus afin de ne rien répéter et de pouvoir arrêter un processus.

La configuration elle-même:

La méthode «stop» arrête simplement le processus. Il y a un truc à faire! Ruby exécute une commande «go run», qui compile TheService et lance un fichier binaire dans un processus enfant avec un ID inconnu. Si nous arrêtons simplement le processus en cours dans Ruby, le processus enfant ne s’arrête pas automatiquement et le port restera utilisé. Ainsi, l'arrêt de TheService doit passer par l'ID de groupe de processus:

Ensuite, nous préparons le «contexte_partagé» où nous définissons les variables par défaut, démarrons TheService s'il n'a pas encore été lancé et désactivons temporairement le magnétoscope, car celui-ci verrait ce que nous faisons en tant que demande de service externe, mais nous ne voulons pas. Le magnétoscope peut simuler des demandes à ce stade:

Et maintenant, nous pouvons regarder écrire les spécifications elles-mêmes:

TheService peut envoyer des requêtes HTTP à des services externes. Nous pouvons le configurer pour rediriger les demandes vers l'utilitaire local qui les enregistre. Pour cet utilitaire, il existe également un wrapper similaire à ‘TheServiceControl’ pour son démarrage et son arrêt, à la différence près que cet utilitaire peut simplement être démarré sous forme binaire sans compilation.

Faits saillants supplémentaires

L'application Go a été écrite pour que tous les journaux et les informations de débogage soient envoyés à STDOUT. En production, cette sortie est envoyée dans un fichier. Lors du lancement à partir de RSpec, le journal est affiché dans la console, ce qui facilite grandement le débogage.

Si vous exécutez spécifiquement les spécifications qui n’ont pas besoin de TheService, cela ne démarrera pas.

Afin de ne pas perdre de temps à lancer TheService à chaque fois qu'une spécification change, vous pouvez lancer TheService manuellement dans le terminal pendant le processus de développement, sans simplement la désactiver. Chaque fois que cela est nécessaire, vous pouvez même le lancer en mode de débogage IDE. Ensuite, les spécifications préparent tout, envoient la demande au service, il s’arrête et vous pouvez facilement le déboguer. Cela rend l'approche TDD très pratique.

Conclusion

Nous utilisons cette configuration depuis environ un an et n’avons rencontré aucun problème. Les spécifications sont bien plus lisibles que les tests unitaires dans Go, et ils ne reposent pas sur la connaissance de la structure interne du service. Si, pour une raison quelconque, nous devons réécrire le service dans une autre langue, nous n’avons pas besoin de modifier les spécifications. Seuls les wrappers utilisés pour lancer le service de test avec une commande différente doivent être réécrits.