Des tests bien écrits réduisent considérablement le risque de «casser» l'application lors de l'ajout d'une nouvelle fonctionnalité ou de la correction d'un bogue. Dans les systèmes complexes constitués de plusieurs composants interconnectés, le plus difficile est de tester leur terrain commun.
Dans cet article, je vais vous expliquer comment nous avons rencontré la difficulté d'écrire de bons tests lors du développement d'un composant sur Go et comment nous avons résolu ce problème en utilisant la bibliothèque RSpec dans Ruby on Rails.
Ajout de Go à la pile technologique du projet
L'un des projets développés par eTeam, où je travaille, peut être divisé en: panneau d'administration, compte d'utilisateur, générateur de rapports et traitement des demandes de divers services avec lesquels nous sommes intégrés.
La partie responsable du traitement des demandes est la plus importante, j'ai donc voulu la rendre aussi fiable et abordable que possible. Faisant partie d'une application monolithique, elle risquait de recevoir un bogue lors de la modification de sections de code sans rapport avec elle. Il y avait également un risque d'abandon du traitement lors du chargement d'autres composants d'application. Le nombre de travailleurs Ngnix par application est limité et, à mesure que la charge augmentait, par exemple, ouvrant de nombreuses pages lourdes dans le panneau d'administration, les travailleurs libres s'arrêtaient et le traitement des demandes ralentissait, voire tombait.
Ces risques, ainsi que la maturité de ce système (pendant des mois sans modifications) en ont fait un candidat idéal pour la séparation en un service distinct.
Il a été décidé d'écrire ce service séparé sur Go. Il devait partager l'accès à la base de données avec l'application Rails. La responsabilité des modifications possibles de la structure de la table incombait à Rails. En principe, un tel schéma avec une base de données commune fonctionne bien, alors qu'il n'y a que deux applications. Cela ressemblait à ceci:

Le service a été écrit et déployé sur des instances distinctes de Rails. Désormais, lorsque vous déployez des applications Rails, vous n'avez pas à vous soucier que cela affecterait le traitement des requêtes. Le service a accepté directement les requêtes HTTP, sans Ngnix, utilisé un peu de mémoire, était en quelque sorte minimaliste.
Le problème avec nos tests unitaires dans Go
Des tests unitaires ont été implémentés dans l'application Go et toutes les requêtes de base de données y ont été verrouillées. Parmi les autres arguments en faveur d'une telle solution, on peut citer les suivants: l'application Rails principale est responsable de la structure de la base de données, de sorte que l'application go ne «possède» pas les informations pour créer une base de données de test. Le traitement des demandes pour la moitié consistait en une logique métier et la moitié de l'utilisation de la base de données, et cette moitié était complètement bloquée. Moki dans Go semble moins «lisible» que dans Ruby. Lors de l'ajout d'une nouvelle fonction pour lire les données de la base de données, il était nécessaire d'ajouter moki pour cela dans l'ensemble des tests tombés qui fonctionnaient auparavant. En conséquence, ces tests unitaires étaient inefficaces et extrêmement fragiles.
Méthode de solution
Pour éliminer ces lacunes, il a été décidé de couvrir le service avec des tests fonctionnels situés dans l'application Rails et de tester le service sur Go dans une boîte noire. En tant que boîte blanche, cela ne fonctionnerait toujours pas, car à partir de rubis, même avec tout le désir, il serait impossible d'intervenir dans le service, par exemple, mouiller une méthode pour vérifier si elle est appelée. Cela signifiait également que les demandes envoyées par le service testé étaient également impossibles à verrouiller, par conséquent, une autre application était nécessaire pour les capturer et les enregistrer. Quelque chose comme RequestBin, mais local. Nous avons déjà écrit un utilitaire similaire, nous l'avons donc utilisé.
Le schéma suivant s'est avéré:
- rspec compile et démarre le service en cours de route, en lui passant une configuration, qui contient l'accès à la base de test et un certain port pour recevoir les requêtes HTTP, par exemple 8082
- un utilitaire est également lancé pour enregistrer les requêtes HTTP reçues dessus, sur le port 8083
- nous écrivons des tests ordinaires sur RSpec, c'est-à-dire créer les données nécessaires dans la base de données et envoyer une demande à localhost: 8082, comme à un service externe, par exemple en utilisant HTTParty
- réponse parsim; vérifier les changements dans la base de données; nous obtenons la liste des demandes enregistrées du «RequestBin» et les vérifions.
Détails d'implémentation:
Maintenant, comment il a été mis en œuvre. À des fins de démonstration, nommons le service testé: «TheService» et créons un wrapper pour celui-ci:
Au cas où, je ferai une réservation pour que dans Rspec il soit configuré pour charger automatiquement les fichiers à partir du dossier "support":
Dir[Rails.root.join('spec/support/**/*.rb')].each {|f| require f}
La méthode de démarrage:
- lit dans une configuration distincte le chemin d'accès aux sources de TheService et les informations nécessaires à l'exécution. Parce que ces informations peuvent différer de différents développeurs, cette configuration est exclue de Git. La même configuration contient les paramètres nécessaires au lancement du programme. Ces configurations hétérogènes sont situées au même endroit afin de ne pas produire de fichiers supplémentaires.
- compile et exécute le programme via "go run {path to main.go} {path to config}"
- interrogation chaque seconde, il attend que le programme en cours d'exécution soit prêt à accepter les demandes
- se souvient de l'identifiant du processus afin de ne pas redémarrer et de pouvoir l'arrêter.
se configurer:
#/spec/support/the_service_config.yml server: addr: 127.0.0.1:8082 db: dsn: dbname=project_test sslmode=disable user=postgres password=secret redis: url: redis://127.0.0.1:6379/1 rails: main_go: /home/me/go/src/github.com/company/theservice/main.go recorder_addr: 127.0.0.1:8083 env: PATH: '/home/me/.gvm/gos/go1.10.3/bin' GOROOT: '/home/me/.gvm/gos/go1.10.3' GOPATH: '/home/me/go'
La méthode stop arrête simplement le processus. La nouveauté est que ruby exécute la commande «go run» qui exécute le binaire compilé dans un processus enfant dont l'ID est inconnu. Si vous arrêtez simplement le processus démarré à partir de ruby, le processus enfant ne s'arrête pas automatiquement et le port reste occupé. Par conséquent, l'arrêt se produit par ID de groupe de processus:
Nous allons maintenant préparer un shared_context où nous définirons les variables par défaut, démarrer TheService s'il n'a pas été démarré et désactiver temporairement le magnétoscope (de son point de vue, nous parlons à un service externe, mais pour nous maintenant ce n'est pas le cas):
et maintenant vous pouvez commencer à écrire les spécifications elles-mêmes:
TheService peut effectuer ses requêtes HTTP vers des services externes. En utilisant la configuration, nous redirigeons vers un utilitaire local qui les écrit. Il existe également un wrapper pour démarrer et arrêter, il est similaire à la classe «TheServiceControl», sauf que l'utilitaire peut simplement être démarré sans compilation.
Petits pains supplémentaires
L'application Go a été écrite pour que tous les journaux et les informations de débogage soient affichés dans STDOUT. Lorsqu'elle est lancée en production, cette sortie est envoyée dans un fichier. Et lorsqu'il est lancé à partir de Rspec, il s'affiche dans la console, ce qui aide beaucoup lors du débogage.
Si les spécifications sont exécutées de manière sélective, pour lesquelles TheService n'est pas nécessaire, il ne démarre pas.
Afin d'éviter de perdre du temps à développer le service chaque fois que vous redémarrez la spécification lors du développement, vous pouvez démarrer le service manuellement dans le terminal et ne pas l'éteindre. Si nécessaire, vous pouvez même l'exécuter dans l'EDI en mode débogage, puis la spécification préparera tout ce dont vous avez besoin, lancera une demande de service, elle s'arrêtera et vous pourrez l'avilir sans problème. Cela rend l'approche TDD très pratique.
Conclusions
Un tel programme fonctionne depuis environ un an et n'a jamais échoué. Les spécifications sont beaucoup plus lisibles que les tests unitaires sur Go, et 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'aurons pas à modifier les spécifications, à l'exception de l'encapsuleur, qui a juste besoin de démarrer le service de test avec une autre commande.