Gonkey - Outil de test de microservices

Gonkey teste nos microservices à Lamoda, et nous pensions qu'il pourrait tester le vôtre, nous l'avons donc mis en open source . Si la fonctionnalité de vos services est principalement mise en œuvre via l'API et que JSON est utilisé pour l'échange de données, alors Gonkey vous convient presque certainement.


image


Ci-dessous, j'en parlerai plus en détail et montrerai avec des exemples spécifiques comment l'utiliser.


Comment Gonkey est né


Nous avons plus d'une centaine de microservices, dont chacun résout une tâche spécifique. Tous les services ont une API. Bien sûr, certains d'entre eux sont également une interface utilisateur, mais, néanmoins, leur rôle principal est d'être une source de données pour un site, des applications mobiles ou d'autres services internes, et donc de fournir une interface logicielle .


Lorsque nous avons réalisé qu'il y avait beaucoup de services, et qu'il y en aurait encore plus, nous avons développé un document interne décrivant l'approche standard de la conception d'API et pris Swagger comme outil de description (et même écrit des utilitaires pour générer du code basé sur la spécification de swagger). Si vous souhaitez en savoir plus à ce sujet, consultez la discussion d'Andrew avec Highload ++.


L'approche standard de la conception d'API a naturellement conduit à l'idée d'une approche standard des tests. Voici ce que je voulais réaliser:


  1. Testez les services via l'API , car presque toutes les fonctionnalités du service sont implémentées via celle-ci
  2. La possibilité d'automatiser le lancement de tests pour l'intégrer dans notre processus CI / CD, comme on dit, "run by button"
  3. L'écriture de tests doit être aliénable , c'est-à-dire que non seulement un programmeur peut écrire des tests, idéalement une personne qui n'est pas familière avec la programmation.

Gonkey est donc né.


Alors qu'est-ce que c'est?


Gonkey est une bibliothèque (pour les projets sur Golang) et un utilitaire de console (pour les projets dans toutes les langues et technologies), avec lesquels vous pouvez effectuer des tests fonctionnels et de régression des services en accédant à leur API selon un script prédéfini. Les scripts de test sont décrits dans les fichiers YAML.


Autrement dit, Gonkey peut:


  • bombardez votre service avec des requêtes HTTP et assurez-vous que ses réponses sont comme prévu. Il suppose que JSON est utilisé dans les demandes et les réponses, mais, très probablement, il fonctionnera sur des cas simples avec des réponses dans un format différent;
  • préparer la base de données pour le test en la remplissant avec les données des appareils (également spécifiées dans les fichiers YAML);
  • imiter les réponses des services externes à l'aide de simulateurs (cette fonctionnalité n'est disponible que si vous connectez Gonkey en tant que bibliothèque);
  • donner des résultats de test à la console ou générer un rapport Allure.

Dépôt de projets
Image Docker


Exemple de test de service avec Gonkey


Afin de ne pas vous encombrer de texte, je veux passer des mots aux actes et ici tester une API et dire et montrer comment les scripts de test sont écrits en cours de route.


Esquissons un petit service sur Go qui simulera le travail d'un feu de circulation. Il stocke la couleur du signal actuel: rouge, jaune ou vert. Vous pouvez obtenir la couleur de signal actuelle ou en définir une nouvelle via l'API.


//    const ( lightRed = "red" lightYellow = "yellow" lightGreen = "green" ) //      type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"` } //   var lights = trafficLights{ currentLight: lightRed, } func main() { //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { lights.mutex.RLock() defer lights.mutex.RUnlock() resp, err := json.Marshal(lights) if err != nil { log.Fatal(err) } w.Write(resp) }) //       http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { lights.mutex.Lock() defer lights.mutex.Unlock() request, err := ioutil.ReadAll(r.Body) if err != nil { log.Fatal(err) } var newTrafficLights trafficLights if err := json.Unmarshal(request, &newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := validateRequest(&newTrafficLights); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } lights = newTrafficLights }) //   () log.Fatal(http.ListenAndServe(":8080", nil)) } func validateRequest(lights *trafficLights) error { if lights.currentLight != lightRed && lights.currentLight != lightYellow && lights.currentLight != lightGreen { return fmt.Errorf("incorrect current light: %s", lights.currentLight) } return nil } 

Le code source complet de main.go est ici .


Exécutez le programme:


 go run . 

Esquissé très rapidement en 15 minutes! Il s'est sûrement trompé quelque part, alors nous allons écrire un test et vérifier.


Téléchargez et exécutez Gonkey:


 mkdir -p tests/cases docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Cette commande démarre l'image avec gonkey via le docker, monte le répertoire tests / cases à l'intérieur du conteneur et démarre gonkey avec les paramètres -tests tests / cases / -host.


Si vous n'aimez pas l'approche docker, alors une alternative à une telle commande serait d'écrire:


 go get github.com/lamoda/gonkey go run github.com/lamoda/gonkey -tests tests/cases -host localhost:8080 

Lancé et obtenu le résultat:


 Failed tests: 0/0 

Aucun test - rien à vérifier. Écrivons le premier test. Créez un fichier tests / cases / light_get.yaml avec le contenu minimum:


 - name: WHEN currentLight is requested MUST return red method: GET path: /light/get response: 200: > { "currentLight": "red" } 

Au premier niveau se trouve une liste. Cela signifie que nous avons décrit un cas de test, mais il peut y en avoir plusieurs dans le fichier. Ensemble, ils constituent le scénario de test. Ainsi, un fichier - un script. Vous pouvez créer n'importe quel nombre de fichiers avec des scripts de test, si vous le souhaitez, les organiser en sous-répertoires - gonkey lit tous les fichiers yaml et yml du répertoire transféré et est plus récursif.


Le fichier ci-dessous décrit les détails de la requête qui sera envoyée au serveur: méthode, chemin. Le code de réponse (200) et le corps de réponse que nous attendons du serveur sont encore plus bas.


Le format de fichier complet est décrit dans le fichier README .


Exécutez à nouveau:


 docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Résultat:


  Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body: <no body> Response: Status: 200 OK Body: {} Result: ERRORS! Errors: 1) at path $ values do not match: expected: { "currentLight": "red" } actual: {} Failed tests: 1/1 

Erreur! Une structure avec le champ currentLight était attendue et une structure vide est retournée. C'est mauvais. Le premier problème est que le résultat a été interprété comme une chaîne, cela est indiqué par le fait que, en tant que lieu de problème, gonkey a mis en évidence la réponse entière sans aucun détail:


  expected: { "currentLight": "red" } 

La raison est simple: j'ai oublié d'écrire pour que le service dans la réponse indique le type de contenu application / json. Nous réparons:


 //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { lights.mutex.RLock() defer lights.mutex.RUnlock() resp, err := json.Marshal(lights) if err != nil { log.Fatal(err) } w.Header().Add("Content-Type", "application/json") // <--  w.Write(resp) }) 

Nous redémarrons le service et exécutons à nouveau les tests:


  Name: WHEN currentlight is requested MUST return red Request: Method: GET Path: /light/get Query: Body: <no body> Response: Status: 200 OK Body: {} Result: ERRORS! Errors: 1) at path $ key is missing: expected: currentLight actual: <missing> 

Génial, il y a du progrès. Gonkey reconnaît maintenant la structure, mais elle est toujours incorrecte: la réponse est vide. La raison en est que j'ai utilisé un champ currentLight non exportable dans la définition de type:


 //      type trafficLights struct { currentLight string `json:"currentLight"` mutex sync.RWMutex `json:"-"` } 

Dans Go, un champ de structure nommé avec une lettre minuscule est considéré comme non exportable, c'est-à-dire inaccessible à partir d'autres packages. Le sérialiseur JSON ne le voit pas et ne peut pas l'inclure dans la réponse. Nous corrigeons: nous faisons le champ avec une majuscule, ce qui signifie qu'il est exporté:


 //      type trafficLights struct { urrentLight string `json:"currentLight"` // <--   mutex sync.RWMutex `json:"-"` } 

Redémarrez le service. Exécutez à nouveau les tests.


 Failed tests: 0/1 

Les tests ont réussi!


Nous allons écrire un autre script qui testera la méthode set. Remplissez le fichier tests / cases / light_set.yaml avec le contenu suivant:


 - name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN get is requested MUST return green method: GET path: /light/get response: 200: > { "currentLight": "green" } 

Le premier test définit une nouvelle valeur pour le feu de circulation, et le second vérifie l'état pour s'assurer qu'il a changé.


Exécutez les tests avec la même commande:


 docker run -it -v $(pwd)/tests:/tests lamoda/gonkey -tests tests/cases -host host.docker.internal:8080 

Résultat:


 Failed tests: 0/3 

Un résultat réussi, mais nous avons eu la chance que les scripts aient été exécutés dans l'ordre dont nous avions besoin: d'abord light_get, puis light_set. Que se passerait-il s'ils faisaient le contraire? Renommons:


 mv tests/cases/light_set.yaml tests/cases/_light_set.yaml 

Et exécutez à nouveau:


 Errors: 1) at path $.currentLight values do not match: expected: red actual: green Failed tests: 1/3 

Tout d'abord, le jeu a été exécuté et le feu de signalisation a été laissé dans l'état vert, donc le test de démarrage a ensuite trouvé une erreur - il attendait le rouge.


Une façon de se débarrasser du fait que le test dépend du contexte est d'initialiser le service au début du script (c'est-à-dire au début du fichier), ce que nous faisons généralement dans l'ensemble de test - nous définissons d'abord une valeur connue qui devrait produire un effet connu, puis vérifiez que l'effet a eu un effet.


Une autre façon de préparer le contexte d'exécution si le service utilise la base de données consiste à utiliser des appareils avec des données qui sont chargées dans la base de données au début du script, formant ainsi un état prévisible du service qui peut être vérifié. La description et les exemples de travail avec des appareils dans gonkey que je veux mettre dans un article séparé.


En attendant, je propose la solution suivante. Étant donné que dans le script set, nous testons à la fois la méthode light / set et la méthode light / get, nous n'avons tout simplement pas besoin du script light_get, qui est sensible au contexte. Je le supprime et renomme le script restant afin que le nom reflète l'essence.


 rm tests/cases/light_get.yaml mv tests/cases/_light_set.yaml tests/cases/light_set_get.yaml 

À l'étape suivante, je voudrais vérifier certains scénarios négatifs de collaboration avec notre service, par exemple, cela fonctionnera-t-il correctement si j'envoie une couleur de signal incorrecte? Ou ne pas envoyer de couleur du tout?


Créez un nouveau script tests / cases / light_set_get_negative.yaml:


 - name: WHEN set is requested MUST return no response method: POST path: /light/set request: > { "currentLight": "green" } response: 200: '' - name: WHEN incorrect color is passed MUST return error method: POST path: /light/set request: > { "currentLight": "blue" } response: 400: > incorrect current light: blue - name: WHEN color is missing MUST return error method: POST path: /light/set request: > {} response: 400: > incorrect current light: - name: WHEN get is requested MUST have color untouched method: GET path: /light/get response: 200: > { "currentLight": "green" } 

Il vérifie que:


  • lorsque la mauvaise couleur est transmise, une erreur se produit;
  • lorsque la couleur n'est pas transmise, une erreur se produit;
  • une transmission incorrecte des couleurs ne modifie pas l'état interne du feu de circulation.

Exécuter:


 Failed tests: 0/6 

Tout va bien :)


Connecter Gonkey en tant que bibliothèque


Comme vous l'avez remarqué, nous testons l'API de service, en faisant complètement abstraction du langage et des technologies dans lesquels elle est écrite. De la même manière, nous pourrions tester n'importe quelle API publique pour laquelle nous n'avons pas accès aux codes sources - il suffit d'envoyer des demandes et de recevoir des réponses.


Mais pour nos propres applications écrites en go, il existe un moyen plus pratique d'exécuter gonkey - pour le connecter au projet en tant que bibliothèque. Cela permettra, sans rien compiler à l'avance - ni gonkey, ni le projet lui-même - d'exécuter le test en exécutant simplement go test .


Avec cette approche, nous semblons commencer à écrire un test unitaire, et dans le corps du test, nous faisons ce qui suit:


  • initialiser le serveur Web de la même manière qu'au démarrage du service;
  • exécutez le serveur d'applications de test sur l'hôte local et le port aléatoire;
  • nous appelons la fonction à partir de la bibliothèque gonkey, en lui passant l'adresse du serveur de test et d'autres paramètres. Ci-dessous, je vais illustrer cela.

Pour ce faire, notre application aura besoin d'un petit refactoring. Son point clé est de faire de la création du serveur une fonction distincte, car nous avons maintenant besoin de cette fonction à deux endroits: lorsque le service démarre et même lorsque les tests gonkey sont exécutés.


J'ai mis le code suivant dans une fonction distincte:


 func initServer() { //       http.HandleFunc("/light/get", func(w http.ResponseWriter, r *http.Request) { //   }) //       http.HandleFunc("/light/set", func(w http.ResponseWriter, r *http.Request) { //   }) } 

La fonction principale sera alors la suivante:


 func main() { initServer() //   () log.Fatal(http.ListenAndServe(":8080", nil)) } 

Le fichier go principal modifié complètement .


Cela a libéré nos mains, alors commençons à écrire un test. Je crée un fichier func_test.go:


 func Test_API(t *testing.T) { initServer() srv := httptest.NewServer(nil) runner.RunWithTesting(t, &runner.RunWithTestingParams{ Server: srv, TestsDir: "tests/cases", }) } 

Voici le fichier func_test.go complet .


C'est tout! Nous vérifions:


 go test ./... 

Résultat:


 ok github.com/lamoda/gonkey/examples/traffic-lights-demo 0.018s 

Les tests ont réussi. Si j'ai à la fois des tests unitaires et des tests de gonkey, ils fonctionneront tous ensemble - très facilement.


Générer un rapport Allure


Allure est un format de rapport de test pour afficher les résultats d'une manière claire et belle. Gonkey peut enregistrer les résultats des tests dans ce format. L'activation d'Allure est très simple:


 docker run -it -v $(pwd)/tests:/tests -w /tests lamoda/gonkey -tests cases/ -host host.docker.internal:8080 -allure 

Le rapport sera placé dans le sous-répertoire allure-results du répertoire de travail actuel (c'est pourquoi j'ai spécifié -w / tests).


Lors de la connexion de gonkey en tant que bibliothèque, le rapport Allure est activé en définissant une variable d'environnement supplémentaire GONKEY_ALLURE_DIR:


 GONKEY_ALLURE_DIR="tests/allure-results" go test ./… 

Les résultats des tests enregistrés dans des fichiers sont convertis en un rapport interactif par les commandes:


 allure generate allure serve 

À quoi ressemble le rapport:
image


Conclusion


Dans les articles suivants, je m'attarderai sur l'utilisation de fixtures chez gonkey et sur l'imitation des réponses d'autres services utilisant des simulacres.


Je vous invite à essayer gonkey dans vos projets, à participer à son développement (les demandes de pool sont les bienvenues!) Ou à le marquer d'un astérisque sur le github si ce projet peut vous être utile dans le futur.

Source: https://habr.com/ru/post/fr463301/


All Articles