Méthodes de test de `test::nginx` : Configuration, envoi de requêtes et gestion des réponses

API7.ai

November 18, 2022

OpenResty (NGINX + Lua)

Dans l'article précédent, nous avons déjà eu un premier aperçu de test::nginx et exécuté l'exemple le plus simple. Cependant, dans un projet open source réel, les cas de test écrits en test::nginx sont beaucoup plus complexes et difficiles à maîtriser que le code d'exemple. Sinon, cela ne s'appellerait pas un obstacle.

Dans cet article, je vais vous guider à travers les commandes et méthodes de test fréquemment utilisées dans test::nginx, afin que vous puissiez comprendre la plupart des ensembles de cas de test dans le projet OpenResty et avoir la capacité d'écrire des cas de test plus réalistes. Même si vous n'avez pas encore contribué au code d'OpenResty, vous familiariser avec le framework de test d'OpenResty sera une grande source d'inspiration pour concevoir et écrire des cas de test dans votre travail.

Le test test::nginx génère essentiellement un nginx.conf et démarre un processus NGINX basé sur la configuration de chaque cas de test. Ensuite, il simule une requête client avec le corps et les en-têtes spécifiés. Ensuite, le code Lua dans le cas de test traite la requête et fait une réponse. À ce moment-là, test::nginx analyse les informations critiques comme le corps de la réponse, les en-têtes de la réponse et les journaux d'erreur, et les compare à la configuration du test. S'il existe une divergence, le test échoue avec une erreur ; sinon, il réussit.

test::nginx fournit beaucoup de primitives DSL (langage spécifique à un domaine). J'ai fait une classification simple selon la configuration de NGINX, l'envoi de requêtes, le traitement des réponses et la vérification des journaux. Ces 20% de fonctionnalités peuvent couvrir 80% des scénarios d'application, donc nous devons les maîtriser fermement. Quant aux autres primitives et utilisations plus avancées, nous les introduirons dans le prochain article.

Configuration de NGINX

Commençons par regarder la configuration de NGINX. La primitive de test::nginx avec le mot-clé "config" est liée à la configuration de NGINX, comme config, stream_config, http_config, etc.

Leurs fonctions sont les mêmes : insérer la configuration NGINX spécifiée dans différents contextes NGINX. Ces configurations peuvent être soit des commandes NGINX, soit du code Lua encapsulé dans content_by_lua_block.

Lorsque nous faisons des tests unitaires, config est la primitive la plus couramment utilisée, dans laquelle nous chargeons des bibliothèques Lua et appelons des fonctions pour des tests en boîte blanche. Voici un extrait de code de test, qui ne peut pas être exécuté entièrement. Il provient d'un projet open source réel, donc si vous êtes intéressé, vous pouvez cliquer sur le lien pour voir le test complet, ou vous pouvez essayer de l'exécuter localement.

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            local plugin = require("apisix.plugins.key-auth")
            local ok, err = plugin.check_schema({key = 'test-key'})
            if not ok then
                ngx.say(err)
            end
            ngx.say("done")
        }
    }

Le but de ce cas de test est de vérifier si la fonction check_schema dans le fichier de code plugins.key-auth fonctionne correctement. Il utilise la commande NGINX content_by_lua_block dans location /t pour exiger le module à tester et pour appeler directement la fonction qui doit être vérifiée.

C'est un moyen courant de test en boîte blanche dans test::nginx. Cependant, cette configuration seule ne suffit pas pour compléter le test, donc continuons et voyons comment envoyer une requête client.

Envoi de Requêtes

Simuler un client envoyant une requête implique pas mal de détails, donc commençons par le plus simple - envoyer une seule requête.

request

Continuons avec le cas de test ci-dessus, si nous voulons que le code de test unitaire soit exécuté, alors nous devons initier une requête HTTP à l'adresse /t spécifiée dans la config, comme montré dans le code de test suivant :

--- request
GET /t

Ce code envoie une requête GET à /t dans la primitive de requête. Ici, nous ne spécifions pas l'adresse IP, le nom de domaine ou le port d'accès, ni si c'est HTTP 1.0 ou HTTP 1.1. Tous ces détails sont masqués par test::nginx, donc nous n'avons pas à nous en soucier. C'est l'un des avantages du DSL - nous avons seulement besoin de nous concentrer sur la logique métier sans être distraits par tous les détails.

De plus, cela offre une certaine flexibilité. Par exemple, le protocole par défaut est HTTP 1.1, ou si nous voulons tester HTTP 1.0, nous pouvons spécifier séparément :

--- request
GET /t  HTTP/1.0

En plus de la méthode GET, la méthode POST doit également être supportée. Dans l'exemple suivant, nous pouvons POST la chaîne hello world à l'adresse spécifiée.

--- request
POST /t  
hello world

Encore une fois, test::nginx calcule la longueur du corps de la requête pour vous ici, et ajoute les en-têtes de requête host et connection pour s'assurer que c'est une requête normale automatiquement.

Bien sûr, nous pouvons ajouter des commentaires pour le rendre plus lisible. Ceux qui commencent par # seront reconnus comme des commentaires de code.

--- request
# post request
POST /t  
hello world

La requête supporte également un mode plus complexe et flexible, qui utilise eval comme filtre pour intégrer directement du code Perl, puisque test::nginx est écrit en Perl. Si le langage DSL actuel ne répond pas à vos besoins, eval est l'"arme ultime" pour exécuter directement du code Perl.

Pour l'utilisation de eval, regardons quelques exemples simples ici, et nous continuerons avec d'autres plus complexes dans le prochain article.

--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"

Dans le premier exemple, nous utilisons eval pour spécifier des caractères non imprimables, ce qui est l'une de ses utilisations. Le contenu entre les guillemets doubles sera traité comme une chaîne Perl puis passé à la request comme argument.

Voici un exemple plus intéressant :

--- request eval
"POST /t\n" . "a" x 1024

Cependant, pour comprendre cet exemple, nous devons savoir quelque chose sur les chaînes en Perl, donc je dois brièvement mentionner deux points ici.

  • En Perl, nous utilisons un point pour représenter la concaténation de chaînes. Cela ne ressemble-t-il pas un peu aux deux points de Lua ?
  • Un x minuscule indique le nombre de fois qu'un caractère est répété. Par exemple, le "a" x 1024 ci-dessus signifie que le caractère "a" est répété 1024 fois.

Donc, le deuxième exemple signifie que la méthode POST envoie une requête contenant 1024 caractères a à l'adresse /t.

pipelined_requests

Après avoir compris comment envoyer une seule requête, regardons comment envoyer plusieurs requêtes. Dans test::nginx, nous pouvons utiliser la primitive pipelined_requests pour envoyer plusieurs requêtes en séquence dans la même connexion keep-alive :

--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]

Par exemple, cet exemple accédera à ces quatre API en séquence dans la même connexion. Il y a deux avantages à cela :

  • Le premier est qu'une grande quantité de code de test répétitif peut être éliminée, et les quatre cas de test peuvent être compressés en un.
  • La deuxième et la plus importante raison est que nous pouvons utiliser les requêtes pipelinées pour détecter si la logique du code aura des exceptions dans le cas de multiples accès.

Vous pourriez vous demander, si j'écris plusieurs cas de test en séquence, alors le code sera également exécuté plusieurs fois dans la phase d'exécution. Cela ne couvre-t-il pas également le deuxième problème ci-dessus ?

Cela revient au mode d'exécution de test::nginx, qui fonctionne différemment de ce que vous pourriez penser. Après chaque cas de test, test::nginx arrête le processus NGINX actuel, et toutes les données en mémoire disparaissent. Lors de l'exécution du cas de test suivant, nginx.conf est régénéré, et un nouveau Worker NGINX est démarré. Ce mécanisme garantit que les cas de test ne s'influencent pas mutuellement.

Donc, lorsque nous voulons tester plusieurs requêtes, nous devons utiliser la primitive pipelined_requests. Sur cette base, nous pouvons simuler des scénarios de limitation de débit, de limitation de concurrence, et bien d'autres pour tester si votre système fonctionne correctement avec des scénarios plus réalistes et complexes. Nous laisserons cela pour le prochain article également, car cela impliquera plusieurs commandes et primitives.

repeat_each

Nous venons de mentionner le cas de test de plusieurs requêtes, alors comment devrions-nous exécuter le même test plusieurs fois ?

Pour ce problème, test::nginx fournit un paramètre global : repeat_each, qui est une fonction Perl qui par défaut est repeat_each(1), indiquant que le cas de test ne sera exécuté qu'une seule fois. Donc dans les cas de test précédents, nous ne nous embêtons pas à le définir séparément.

Naturellement, nous pouvons le définir avant la fonction run_test(), par exemple, en changeant l'argument à 2.

repeat_each(2);
run_tests();

Ensuite, chaque cas de test est exécuté deux fois, et ainsi de suite.

more_headers

Après avoir parlé du corps de la requête, regardons les en-têtes de la requête. Comme nous l'avons mentionné ci-dessus, test::nginx envoie la requête avec les en-têtes host et connection par défaut. Qu'en est-il des autres en-têtes de requête ?

more_headers est spécifiquement conçu pour cela.

--- more_headers
X-Foo: blah

Nous pouvons l'utiliser pour définir divers en-têtes personnalisés. Si nous voulons définir plus d'un en-tête, alors définissons plus d'une ligne :

--- more_headers
X-Foo: 3
User-Agent: openresty

Traitement des Réponses

Après avoir envoyé la requête, la partie la plus importante de test::nginx est le traitement de la réponse, où nous déterminerons si la réponse répond aux attentes. Ici, nous la divisons en quatre parties et les présentons : le corps de la réponse, l'en-tête de la réponse, le code d'état de la réponse et le journal.

response_body

Le pendant de la primitive de requête est response_body, et voici un exemple de leurs deux configurations en utilisation :

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            ngx.say("hello")
        }
    }
--- request
GET /t
--- response_body
hello

Ce cas de test passera si le corps de la réponse est hello, et signalera une erreur dans les autres cas. Mais comment testons-nous un corps de retour long ? Ne vous inquiétez pas, test::nginx s'en est déjà occupé pour vous. Il supporte la détection du corps de la réponse avec une expression régulière, comme suit :

--- response_body_like
^he\w+$

Cela vous permet d'être très flexible avec le corps de la réponse. De plus, test::nginx supporte également les opérations unlike :

--- response_body_unlike
^he\w+$

À ce stade, si le corps de la réponse est hello, le test ne passera pas.

Dans la même veine, après avoir compris la détection d'une seule requête, regardons la détection de plusieurs requêtes. Voici un exemple de son utilisation avec pipelined_requests :

--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]

Bien sûr, la chose importante à noter ici est que pour autant de requêtes que vous envoyez, vous devez avoir autant de réponses pour correspondre.

response_headers

Ensuite, parlons de l'en-tête de la réponse. L'en-tête de la réponse est similaire à l'en-tête de la requête en ce que chaque ligne correspond à la clé et à la valeur d'un en-tête.

--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1

Comme la détection du corps de la réponse, les en-têtes de réponse supportent également les expressions régulières et les opérations unlike, telles que response_headers_like, raw_response_headers_like, et raw_response_headers_unlike.

error_code

Le troisième est le code de réponse. La détection du code de réponse supporte la comparaison directe et supporte également les opérations like, comme les deux exemples suivants :

--- error_code: 302
--- error_code_like: ^(?:500)?$

Dans le cas de plusieurs requêtes, le error_code doit être vérifié plusieurs fois :

--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]

error_log

Le dernier élément de test est le journal d'erreur. Dans la plupart des cas de test, aucun journal d'erreur n'est généré. Nous pouvons utiliser no_error_log pour détecter :

--- no_error_log
[error]

Dans l'exemple ci-dessus, si la chaîne [error] apparaît dans le error.log de NGINX, le test échouera. C'est une fonctionnalité très courante, et il est recommandé d'ajouter la détection du journal d'erreur à tous vos tests normaux.

--- error_log
hello world

La configuration ci-dessus détecte la présence de hello world dans error.log. Bien sûr, vous pouvez utiliser eval intégré dans le code Perl pour implémenter la détection par expression régulière, comme suit :

--- error_log eval
qr/\[notice\] .*?  \d+ hello world/

Résumé

Aujourd'hui, nous apprenons comment envoyer des requêtes et tester des réponses dans test::nginx, contenant le corps de la requête, l'en-tête, le code d'état de la réponse et le journal d'erreur. Nous pouvons implémenter un ensemble complet de cas de test avec la combinaison de ces primitives.

Enfin, voici une question de réflexion : Quels sont les avantages et les inconvénients de test::nginx, un DSL abstrait ? N'hésitez pas à laisser des commentaires et à discuter avec moi, et vous êtes également invités à partager cet article pour communiquer et réfléchir ensemble.