Comment développer et tester des API avec mon «vélo» PieceofScript

PieceofScript est un langage simple pour écrire des scripts pour le test automatique de l'API HTTP JSON.

PieceofScript vous permet de:

  • décrire les méthodes API au format YAML, avec le nom de la méthode dans un langage presque naturel, ce qui est pratique pour lire les tests
  • suffisamment flexible pour décrire les modèles au format YAML et générer des données aléatoires à partir d'eux
  • écrire des scripts d'appel d'API complexes dans un langage facile à lire avec une syntaxe simple
  • obtenir les résultats des tests aux formats JUnit et HTML

J'ai écrit ce «vélo» parce que l'interface SoapUI m'a conduit vers le bas. Je voulais décrire simplement et clairement les tests dans un éditeur de texte sans interface graphique spéciale. De plus, git ne digère pas l'énorme fichier xml que SoapUI émet, il est donc difficile de placer des tests pour une tâche spécifique dans la même branche où la tâche elle-même a été effectuée. L'interface Postman est beaucoup plus agréable, mais lors du développement, il faut beaucoup de temps pour y composer / modifier des requêtes et les répéter dans le bon ordre. Je voulais l'automatiser. J'ai également étudié d'autres outils de test, chacun avait un " défaut fatal ", alors dans le cas du syndrome NIH, j'ai ouvert un IDE.

Voici ce qui en est ressorti.



L'interpréteur est écrit en PHP et est une archive phar; il nécessite la version PHP 7.2, bien qu'il puisse également fonctionner sur 7.1. Code source et documentation https://github.com/maximw/PieceofScript . Documentation en développement. Il s'est avéré que c'est la partie la plus difficile et la plus fastidieuse.

Projet de test, sa structure et lancement
Script de test
Méthodes de test API
Appel de méthode API
Génération de modèles et de données de test
Fonctions intégrées
Cas de test
Variables et portées
Types et opérations
Enregistrement de données entre les exécutions
Sortie vers stdout et rapports
Exemples - assez de mots, montrez le code!
Commentaires et plans pour l'avenir, le cas échéant

Projet de test, sa structure et lancement


Le projet est un répertoire avec un ensemble de fichiers de script, des fichiers de description de méthode API et des générateurs de données de test.

Dans la version minimale, le projet ressemble à ceci:

./tests endpoints.yaml -  API generators.yaml -  start.pos -    

Le fichier de démarrage est le script à partir duquel le processus de test commence. Il est défini au démarrage:

 pos.phar run ./start.pos --junit=junit_report.xml -vvv --config=config.yaml 

Tous les chemins relatifs sont lus à partir du répertoire de travail contenant le fichier de démarrage.
Le fichier de configuration peut être spécifié sur la ligne de commande avec l'option --config ou mettez config.yaml dans le répertoire de travail. La configuration est facultative, vous devez y grimper au besoin. En savoir plus sur la config .

Script de test


Pour ma part, j'ai décidé d'écrire des scripts dans des fichiers avec l'extension .pos, afin que vous puissiez effectuer des réglages de mise en évidence du code dans l'EDI avec une liaison par extension. Mais l'interprète est complètement indifférent à l'extension.

Voici un exemple de script simple pour un forum imaginaire où le test de création et de lecture d'un article par différents utilisateurs est effectué.

 require "./globals.pos" //    ,  $domain include "./globals_local.pos" //     ,    include "./user/*.pos" //       ./user  ./post include "./post/*.pos" //       var $author = User() var $reader = User() var $banned = User() var $post = Post() Register $author //   API,     must $response.code == 201 //     Register $reader must $response.code == 201 Register $banned must $response.code == 201 Add $banned to blacklist of $author //$banned      $author Create post $post by $author //   API   must $response.code == 201 //    API   201 // ...,   ,       assert $response.body.post.content == $post.content var $postId = $response.body.post.id // Id     Read post $postId by $author //     must $response.code == 200 assert $response.body.post.content == $post.content Read post $postId by $reader //      must $response.code == 200 assert $response.body.post.content == $post.content Read post $postId by $banned //        assert $response.code == 404 

Oui, ça n'a pas l'air très bien sans rétro-éclairage.

Chaque ligne du script commence par un opérateur ou est un appel à une méthode API. Si soudainement le nom de la méthode API commence par un mot qui correspond à l'un des opérateurs, vous pouvez utiliser le symbole " > ":

 >Include $user to group $userGroup 

Les opérateurs ne sont pas sensibles à la casse. assert, ASSERT ou aSsErT (mais pourquoi écrire comme ça?) fonctionnera.
Chaque instruction ou appel de méthode API doit être sur une ligne distincte. Mais le retour à la ligne est également possible si le dernier caractère de la chaîne est \ (bonjour, Python).

Détails inintéressants sur les sauts de ligne et les retraits
Si l'habillage de ligne est utilisé dans un commentaire, la ligne suivante sera également considérée comme faisant partie du commentaire. Lorsque vous encapsulez des lignes à l'intérieur de blocs ( testcase , if , while , foreach ), il est important de mettre en retrait de sorte que la ligne suivante tombe dans le même bloc.

 var $n = 20 var $i = 2 var $fib1 = 1; \ $fib2 = 1 while $i <= $n var $fib_sum = \ $fib2 + $fib1 print toString($i) + "  :" + \ toString($fib_sum) var $fib1 = $fib2 var $fib2 = $fib_sum var $i = $i + 1 

Lors de l'exécution d'instructions de bloc ( testcase , if , while , foreach ), un bloc est déterminé par l'indentation de ses lignes. L'indentation est comptée comme le nombre d'espaces au début d'une ligne. L'espace et les tabulations comptent pour un seul caractère, mais les tabulations sont généralement affichées dans les éditeurs comme plusieurs espaces. Par conséquent, pour éviter toute confusion, il est préférable d'utiliser des tabulations ou des espaces, mais pas tous ensemble.

Liste complète des opérateurs


require fileName - attachez le fichier à l'endroit où l'opérateur est appelé. Le fichier joint démarrera immédiatement à partir de la première ligne. Une fois terminé, l'interpréteur revient à la ligne suivante du fichier source. Si le fichier demandé n'est pas lisible, une erreur sera générée. Le chemin relatif est calculé à partir du répertoire de travail.

include fileMask - similaire à require, mais si le fichier demandé n'est pas lisible, il n'y aura pas d'erreur. C'est pratique, par exemple, pour créer des paramètres pour différents environnements de test. De plus, include peut connecter tous les fichiers par masque à la fois. Ainsi, par exemple, vous pouvez télécharger des répertoires entiers de fichiers contenant des cas de test. Mais en même temps, tout ordre de téléchargement de fichiers n'est pas garanti.

var $ variable1 = expression1 ; $ variable2 = expression2 ; ...; $ variableN = expressionN - attribue des valeurs aux variables. Si la variable n'existe pas déjà, elle sera créée dans le contexte courant.

let $ variable1 = expression1 ; $ variable2 = expression2 ; ...; $ variableN = expressionN - attribue des valeurs aux variables. Si la variable n'est pas dans le contexte actuel, il y aura une tentative de créer ou de modifier la variable dans le contexte global.

const $ const1 = expression1 ; $ const2 = expression2 ; ...; $ constN = expressionN - définit la valeur des constantes dans le contexte actuel. La différence entre les constantes et les variables est seulement qu'elles ne peuvent pas être modifiées; lorsque vous essayez d'affecter une valeur à une constante, un avertissement sera émis après la déclaration. S'il existe déjà une variable du même nom, une erreur sera générée lors de la tentative de déclaration de constante. Sinon, tout ce qui est vrai pour les variables l'est aussi pour les constantes.

import $ variable1 ; $ variable2 ; ...; $ variableN - copie les variables du contexte global dans le contexte actuel. Cela peut être utile si vous devez opérer sur la valeur d'une variable globale, mais pas la modifier.

testcase testCaseName - annonce un cas de test, qui peut ensuite être appelé en tant qu'unité avec l'instruction run . En savoir plus sur les cas de test plus loin dans l'article .

assert expression - vérifiez que l' expression est vraie , sinon imprimez un rapport sur l'échec du test.

expression doit être identique à assert , mais si le test échoue, le scénario de test en cours sera arrêté. Et en dehors du contexte du scénario de test, le script se terminera complètement. Il peut être utilisé si une erreur est détectée avec laquelle des vérifications supplémentaires n'ont pas de sens.

run testCaseName - exécutez le scénario de test spécifié pour l'exécution. exécuter sans spécifier le nom du scénario de test démarrera tous les scénarios de test déclarés qui ne nécessitent pas d'arguments dans l'ordre de leur déclaration.

while expression - une boucle, tandis que expression est vraie, exécute les instructions avec des lignes en retrait plus que while .

foreach $ array ; $ element - boucle à travers le tableau, le corps de la boucle est exécuté pour chaque élément suivant du tableau. Il est également possible d'obtenir la clé pour chaque tableau $ ; $ key ; $ element . Les variables $ key et $ element sont créées / écrasées dans le contexte actuel.

if expression - si expression est vraie, exécute les instructions avec des lignes en retrait plus que si

print expression1 ; expression2 ; ... expressionN - affiche la valeur de expressionM sur stdout. Il peut être utilisé pour le débogage, il ne fonctionne qu'avec le niveau de «bavard» --verbosité = 1 ou -v et plus.

expression de sommeil - pause pour un nombre donné, éventuellement un entier, secondes. Parfois, vous devez donner une pause à l'API testée.

pause expression - pas en mode interactif (option de ligne de commande -n ) est similaire à sleep . L'expression est facultative, dans ce cas il n'y aura pas de pause. Et en mode interactif, faites une pause avant d'appuyer sur Entrée.

annuler - terminer les tests. L'interprète termine le travail, crée des rapports.

Méthodes de test API


C'est en fait ce que vous devez tester - appelez avec certains paramètres et vérifiez si la réponse répond aux attentes.

Les méthodes API sont décrites au format YAML. Par défaut, les descriptions doivent se trouver dans le fichier endpoints.yaml du répertoire actuel et / ou dans les fichiers * .yaml de son sous-répertoire ./endpoints . Avant le test, l'interpréteur essaiera de lire tous ces fichiers à la fois.

Exemple de structure endpoints.yaml :

 Auth $user: method: "POST" url: $domain + "/login" headers: Content-Type: "application/json" format: "json" data: login: $user.login password: $user.password after: - assert $response.code == 200 - let $user.auth_token = $response.body.auth_token Create post $post by $user: method: "POST" url: $domain + "/posts" format: "json" data: $post headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 201 Read post $postId by $user: method: "GET" url: $domain + "/posts/" + $postId headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert ($response.code == 200) || ($response.code == 404) Create comment $comment on $post by $user: method: "POST" url: $domain + "/comments/create/" + $post.id format: "json" data: $comment headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 201 

Le nom de la méthode API (le niveau supérieur de la structure YAML) par laquelle elle peut être appelée est une chaîne dans un format presque arbitraire.

Les arguments peuvent être spécifiés n'importe où dans le nom. Ils doivent être séparés par des espaces du reste des mots. Par exemple, $ comment , $ post et $ user dans la dernière méthode.

De plus, n'importe où dans le nom, vous pouvez spécifier des valeurs de méthode facultatives entre crochets doubles.

 Get comments of $post {{$page=1; $perPage=$defaultGlobalPageSize}}: method: "GET" url: $domain + "/comments/" + $post.id query: page: $page per_page: $perPage headers: auth: "Bearer " + $user.auth_token content-type: "application/json" after: - assert $response.code == 200 

Dans les expressions qui spécifient des valeurs facultatives, des variables de contexte globales sont disponibles.
Les valeurs facultatives peuvent être utiles afin de ne pas les spécifier chaque fois que vous appelez la méthode API. Si la taille de la page doit être modifiée à un seul endroit, pourquoi l'indiquer à tous les autres endroits? Exemples d'appels à cette méthode:

 Get comments of $newPost //     $page  $perPage Get comments of $newPost {{$page=$currentPage+1}} Get comments of {$newPost} {{$perPage=10;$page=100}} 

Les autres variables utilisées ( $ domain dans l'exemple ci-dessus) seront prises dans le contexte global. Je vous en dirai plus sur les contextes plus tard.

Il me semble commode de donner aux méthodes API des noms lisibles par l'homme dans un langage naturel, alors le script de test est plus facile à lire. Les noms ne respectent pas la casse, c'est-à-dire que la méthode Auth $ User peut être appelée en tant qu'auth $ User et en tant que AUTH $ User . Cependant, les noms de variables sont sensibles à la casse, plus d'informations sur les variables ci-dessous.

Remarque importante. Le format YAML vous permet de ne pas mettre de chaînes entre guillemets. Mais pour l'interpréteur, une chaîne sans guillemets est une expression qui doit être évaluée. Par exemple, déclarer un champ url: http://example.com/login entraînera une erreur de syntaxe lors de l'exécution. Par conséquent, il sera correct: url: "http://example.com/login" ou url: "http://"+$domain+"/login"

Champs de description de la méthode API


méthode - méthode HTTP requise

url - l'URL réelle, obligatoire

headers - liste des en-têtes HTTP, facultatif

cookies - liste de cookies facultative

auth - données pour l'authentification HTTP, facultatif

 auth: login: $login password: $password type: "basic" //  "digest"  "ntlm", - "basic" 

requête - une liste de paramètres d'URL, facultatif

format - l'une des valeurs:

  • aucun - la demande n'a pas de corps
  • json - envoyer à JSON
  • raw - envoyer la chaîne "telle quelle"
  • formulaire - au format application / x-www-form-urlencoded
  • multipart - au format multipart / form-data

Facultatif, aucun par défaut

données - le corps de la demande, sera envoyé dans le format spécifié dans le format , facultatif

  • Pour aucun - le format de données peut être absent, s'il est présent, sera ignoré
  • Pour le format json , n'importe quelle valeur
  • Pour le format brut , toute valeur scalaire
  • Pour le format de formulaire , un tableau dont les clés sont des noms de champs:

     data: login: "Bob" password: $password remember_me: 1 
  • Pour le format en plusieurs parties , un tableau de la structure suivante:

     data: user_id: value: 42 headers: X-Baz: "bar" avatar: file: "/path/to/file" photo: file: "http://url.to/file" filename: "custom_filename.jpg" 

Les fichiers spécifiés dans les champs de fichiers doivent être lisibles. Si une URL est spécifiée, allow_url_fopen doit être activé dans php.ini

before - instructions qui seront exécutées avant la requête HTTP, facultatif

after - instructions qui seront exécutées après la requête HTTP, facultatif

L'idée des blocs avant et après dans l'exécution des vérifications ou le traitement des données nécessaires à chaque fois avant ou après l'exécution de la demande HTTP est dictée non pas tant par les besoins de test que par la logique métier. Par exemple, copier le jeton d'autorisation émis dans le champ de la structure $ user pour appeler toutes les méthodes API suivantes au nom de cet utilisateur. Ou pour vérifier l'état HTTP de la réponse, afin de ne pas le vérifier à chaque fois après un appel dans le script.

Appel de méthode API


Pour appeler la méthode API dans le script, vous devez spécifier son nom et ses paramètres, si nécessaire. Voici un exemple d'appel de la dernière méthode API à partir de la description ci-dessus:

 Create comment $comments.1 on {$newPost} by {$postAuthor} 

Si le paramètre est placé entre accolades, il sera transmis par valeur - de cette façon, vous pouvez passer n'importe quelle expression. Si vous spécifiez un paramètre sans accolades, il sera transmis par référence - il ne peut s'agir que de variables et d'accès statiques aux éléments du tableau (via un point, mais pas entre crochets []).

 Create comment {$comments[$i]} on $posts.0 by $users.1 Read post {123} by $user Get comments of $users.1.id {{$page = 2}} 

Chaque fois que la méthode API est appelée dans le contexte de l'appel lui-même (dans les listes d'instructions avant et après ) et dans le contexte où elle a été appelée, les variables $ request et $ response sont créées . Ce sont des noms réservés, je ne recommande pas de les utiliser à d'autres fins. $ request est disponible dans les blocs avant et après , et $ response est uniquement dans after , in avant que sa valeur ne devienne Null . Dans le contexte d'appel, ces variables sont disponibles jusqu'au prochain appel de méthode API, où elles seront réinitialisées.

$ Structure de demande


$ request.method - String - méthode HTTP
$ request.url - String - l'URL demandée
$ request.query - Array - une liste de paramètres GET
$ request.headers - Array - liste des en-têtes de requête
$ request.cookies - Array - liste des cookies
$ reuqest.auth - Array ou Null - données pour l'authentification HTTP
$ request.format - String - demande le format des données
$ request.data - tapez any - ce qui a été calculé dans le champ de données

$ Structure de réponse


$ response.network - Boolean - false si l'erreur était au niveau du réseau sous HTTP
$ response.code - Number ou Null - code de réponse, par exemple 200 ou 404
$ response.status - String ou Null - état de réponse, par exemple, "204 Aucun contenu" ou "401 non autorisé"
$ response.headers - Array - liste des en-têtes de réponse, les noms d'en-tête sont en minuscules
$ response.cookies - Array - liste des cookies
$ response.body - tout type - corps de réponse traité comme JSON, s'il y a eu une erreur lors de l'analyse, alors l'élément body n'existera pas du tout: @response.body == nullpropos de la vérification de l'existence de variables )
$ response.raw - String ou Null - le corps de réponse brut
$ response.duration - type Number - durée de la requête en secondes

Génération de modèles et de données de test


Les générateurs sont utilisés pour décrire les modèles et générer des données de test à partir d'eux. Les descriptions au format YAML doivent se trouver dans le fichier generators.yaml du répertoire de travail et / ou les fichiers * .yaml du sous-répertoire ./generators .

 User: body: login: Faker\login() name: Faker\name() email: Faker\email() password: Faker\text(16) child: Child() birthday: dateFormat(Faker\datetime(), "U") settings: notifications_enabled: Faker\boolean() Child: body: name: Faker\name() gender: Faker\integer(1, 2) age: Faker\integer(0, 18) Comment($user): body: content: "Hi! I'm " + $user.name tags: - "tag1" - "tag2" 

Dans l'exemple ci-dessus, les trois générateurs User () , Child () et Comment () sont déclarés. Dans ce cas, ce dernier a l'argument $ user et peut utiliser ces données lors de la génération. Les arguments des générateurs sont toujours passés par valeur. De plus, l'exemple utilise plusieurs autres fonctions intégrées: Faker \ name () , Faker \ email () , dateFormat () , etc. Section sur les fonctions intégrées .

Lors de l'appel du générateur User () à partir de l'exemple ci-dessus, une structure sera générée qui ressemble à ceci en JSON:

 { "login": "fgadrkq", "name": "Lucy Cechtelar", "email": "tkshlerin@collins.com", "password": "gbnaueyaaf", "child": { "name": "Adaline Reichel", "gender": 2, "age": 12 }, "birthday": 318038400, "settings": { "notifications_enabled": true } } 

La valeur du champ enfant est le résultat du générateur Child () .

Comme dans la description des méthodes API, toutes les chaînes qui ne sont pas placées entre guillemets sont traitées comme des expressions à évaluer. Cela peut être non seulement un appel à un autre générateur, mais une expression arbitraire, par exemple, dans le générateur Commentaire ($ utilisateur) , le champ de contenu représente la concaténation de la chaîne Hi! Je suis et le nom est passé à $ user

Les noms des générateurs ne respectent pas la casse et doivent commencer par une lettre latine; ils peuvent contenir des lettres latines, des chiffres, des traits de soulignement et des barres obliques inverses.

Étant donné que la syntaxe pour appeler les générateurs et les fonctions intégrées est la même, ils partagent un espace de noms commun. Par convention, je suggère d'utiliser une barre oblique inverse comme séparateur pour spécifier un "fournisseur" ou une bibliothèque de fonctions intégrées, telles que les fonctions Faker \ something (), basées sur la bibliothèque github.com/fzaninotto/Faker .

Les nuances de l'utilisation des générateurs, vous ne pouvez pas lire
À l'aide de générateurs, vous pouvez composer des structures de données:

 # Userredentials     $user Userredentials($user): body: login: $user.email password: $user.password #    .     ,    GlobalSearchResult($posts, $comments, $users): body: posts: title: " " list: $posts comments: title: " " list: $comments users: title: " " list: $users 

GlobalSearchResult n'est pas des données de test envoyées dans la demande à la méthode API, mais un modèle de réponse qui peut être vérifié avec ce que l'API enverra, par exemple, en utilisant les fonctions similaires () ou identiques () .

Le générateur peut modifier la structure obtenue dans le corps en utilisant des structures calculées dans les champs remplacer et supprimer . Je vais vous montrer un exemple.

Supposons que vous disposez déjà d'un générateur User () qui crée la structure de données appropriée pour l'utilisateur. Vous devez maintenant vérifier la réponse de l'API si vous fournissez des données incorrectes. Vous pouvez procéder de deux manières:

  • Créez un "mauvais" générateur d'utilisateurs à partir de zéro. Mais nous obtiendrons ensuite la duplication de code, et plus tard, par exemple, lorsque vous ajouterez un nouveau champ à l'utilisateur en fonction des besoins de la logique métier, vous devrez apporter des modifications à deux endroits. SEC!
  • Vous pouvez «hériter» de la structure User () existante en la définissant dans le corps . Et dans remplacer et supprimer, définissez les champs qui seront ajoutés / modifiés et supprimés.

 #      ,    , #    InvalidUser($user): body: $user replace: email: Faker\String(6, 15) #   password: Faker\String(1, 5) #    new_field: "      ,  " remove: name: size($user.name) < 10 #  ,    10  #      , #        InvalidNewUser: body: User() replace: login: "!@#$%^&*" #   remove: about: true settings: notifications: 100500 #       , #      true 

Lorsque le générateur fonctionne, la structure de données dans le corps est d' abord calculée, puis elle est remplacée et complétée par des éléments de replace, puis les champs spécifiés dans remove sont supprimés si leur valeur est équivalente à true . Si le résultat du calcul du corps , remplacer ou supprimer n'est pas un tableau, il n'y aura pas d'erreur, mais cela ne sert à rien non plus, car il n'y aura pas de champs qui pourraient être remplacés et supprimés.

Fonctions intégrées


Liste complète des fonctions intégrées . Par exemple, je n'en donnerai que quelques-uns.
Après le nom de la fonction et la liste d'arguments, le type de la valeur renvoyée est indiqué, s'il est défini.

Opérations avec variables:


similaire ($ var, $ sample, $ checkTypes) Boolean - renvoie true si les arguments sont du même type, si $ var est un tableau, alors toutes les clés de chaîne qui sont dans $ sample doivent être dans $ var , si $ checkTypes est vrai, alors les types des éléments correspondants doivent correspondre. En d'autres termes, les éléments du tableau $ var sont un sous-ensemble des éléments de $ sample .
identique ($ var, $ sample, $ checkTypes) Le booléen est un analogue de similar () , avec l'inverse supplémentaire, dans le cas des tableaux, toutes les clés de chaîne dans $ var devraient aussi être dans $ sample . En d'autres termes, les éléments du tableau $ var sont égaux aux éléments du tableau $ sample jusqu'au type d'élément.
max ($ var1, $ var2, ... $ varN) - le maximum des valeurs transmises (si elles peuvent être comparées).
min ($ var1, $ var2, ... $ varN) - le minimum des valeurs transmises.
if ($ condition, $ var1, $ var2) - Si $ condition == true, alors il retournera $ var1, sinon $ var2. Remplacement de l'opérateur de coaching (bonjour MySQL).
choix ($ condition1, $ var1, $ condition2, $ var2, ..., $ conditionN, $ varN) - Renvoie le premier $ varK rencontré si $ conditionK == true.

Travailler avec des chaînes:


size ($ string) Number - la longueur de la chaîne dans le codage UTF-8.
regex ($ string, $ regex) Boolean - vérifie la chaîne pour l'expression régulière .
regexMatch ($ string, $ regex) Array - retournera un tableau de chaînes - correspond aux groupes réguliers $ regex .

Traitement de la baie:


array ($ var1, $ var2, ... $ varN) Array - crée un tableau à partir des éléments passés.
size ($ array) Number - le nombre d'éléments dans le tableau.
keys ($ array) Array - une liste de clés dans le tableau.
slice ($ array, $ offset, $ length) Array - partie du tableau à partir de $ offset de longueur $ length, ( plus ).
append ($ array, $ value) Array - ajoute un élément à la fin du tableau.
prepend ($ array, $ value) Array - ajoute un élément au début du tableau.

Traitement de la date:


dateFormat ($ date, $ format) String - formatage de la date, (en savoir plus sur les formats ).
dateModify ($ date, $ format) Date - modifiez la date, pratique à utiliser avec les formats relatifs .

Génération aléatoire de données de test:


Faker \ integer ($ min, $ max) Number - entier aléatoire de $ min à $ max inclus
Faker \ ipv4 () String - aléatoire IPv4
Faker \ arrayElement ($ array) String - élément aléatoire du tableau
Faker \ name () String - random name
Faker \ email () String - e-mail aléatoire

Maintenant, il n'y a pas beaucoup de fonctions intégrées. Je n'ai ajouté que ce qui me semble nécessaire lors des tests. Vous pouvez ajouter de nouvelles fonctionnalités selon les besoins dans les nouvelles versions. Et à l'avenir, s'il est demandé, j'ajouterai la possibilité de créer des fonctions connectées dynamiquement implémentées en tant que classes spéciales en PHP.

Cas de test


Un scénario de test est une séquence d'instructions qui peut être appelée comme une unité. Un analogue de la procédure dans les langages de programmation.

Un scénario de test est créé par l' instruction testcase , suivi du nom du scénario de test, avec une syntaxe similaire aux noms de méthode API . Les cas de test imbriqués sont interdits.

 testcase Registration $device //     var $user = User() Register $user on $device assert $response.code == 201 //        var $user = User() //       InvalidUser() var $user.email = "some_bad_email" Register $user on $device assert $response.code == 400 

L'instruction run peut appeler un scénario de test distinct ou tous les scénarios de test qui ne nécessitent pas d'arguments.

 run Get all users //   -,    run Get user $user_id //   -   run //   -,     

L'idée d'un tel lancement est que les cas de test peuvent être utilisés comme des tests indépendants séparés d'une partie de la logique métier et comme des procédures pour éviter la duplication de code dans des scénarios de test complexes.

Les arguments sont passés au cas de test par référence ou par valeur dans l'analogie complète du passage d'arguments aux méthodes API.

Variables et portées


Les noms de variables sont sensibles à la casse et commencent par un signe $ (oui, oui, je suis pshpshnik).

Si le type de variable de la matrice , l'accès à des champs individuels ou des éléments de valeur est produite par le point: $users.12.password. Entre les points, seuls les chiffres ou les lettres latines, les traits de soulignement et les chiffres avec la première lettre latine sont autorisés. Les noms de champ sont également sensibles à la casse.

Un accès dynamique à un élément du tableau est possible:$post.comments[$i + 1].content

Il existe quatre types de contextes - la portée des variables.

Contexte global - créé au début, contient toutes les variables déclarées lors de l'exécution d'instructions en dehors des cas de test et en dehors des appels de méthode API.

Contexte du scénario de test - un nouveau est créé chaque fois que le scénario de test est exécuté avec l'instruction run .

Contexte de la méthode API - est créé lorsque la méthode API est appelée, lorsque les opérateurs spécifiés dans les sections avant et après sont exécutés .

Contexte du générateur- il n'y a aucune possibilité dans les générateurs de créer de nouvelles variables ou de modifier des variables existantes, donc les variables de contexte global et les arguments sont en lecture seule. Les variables sont toujours transmises par valeur au contexte du générateur.

Remarque importante. Dans tous les contextes, les variables de contexte global sont disponibles si leurs homonymes ne sont pas créés dans le contexte actuel.

Exemples d'opérateurs pour travailler avec des variables:

 const $a = 1 let $a = 2 //    var $b = 1 const $b = 3 //    const $c = 3; $c = 4 //   , $c      

 let $a = 1; $a = $a + 1; $a = $a + 2 print $a // 4 

 Testcase Context example changes $argument1, $argument2 and $argument3 var $a = "changed" let $b = "changed" const $c = "changed" import $i let $i = "changed" var $argument1 = "changed" var $argument2 = "changed" var $argument3 = "changed" // ,     var $a = "original" var $b = "original" var $i = "original" const $c = "original"; var $paramByRef = "original" var $paramByVal = "original" const $paramConst = "original" run Context example changes $paramByRef, {$paramByVal} and $paramConst //  $a     - print $a // "original" // $b     -,  let    $b print $b // "changed" // $i      print $i // "original" //      ,  var   print $c // "original" //     print $paramByRef // "changed" //     print $paramByVal // "original" //     ,     print $paramConst // "original" 

Type Système et opérations


Le typage dynamique laxiste est utilisé.

Les valeurs sont stockées dans des wrappers sur les types PHP correspondants. Pour une meilleure compréhension, voir le système de type PHP. J'ai fait un peu moins de liberté dans la conversion de type dynamique. Par exemple, lors de l'ajout d'une chaîne et d'un nombre "2" + 2, une erreur sera générée et PHP effectuera discrètement l'ajout. Peut-être aurai-je besoin de réviser les règles de la frappe dynamique à l'avenir, mais jusqu'à présent, j'ai essayé de trouver un équilibre entre la commodité et la rigueur requises pour des tests fiables.

Types de données disponibles dans PieceofScript:

NumberEst un nombre. Pour des raisons de simplicité, je n'ai pas fait de types séparés pour Integer et Float. La seule différence significative entre les nombres entiers et réels dans PieceofScript est l'utilisation d'un tableau comme clés: les nombres réels seront arrondis en entiers.
7 -42 3.14159

String - les chaînes entre guillemets, éventuellement échappées par une barre oblique
"I say \"Hello world!\""

Null - définies par une constante
null

booléenne insensible à la casse - sont le résultat d'opérations booléennes et d'opérations de comparaison, définies par des constantes insensibles à la casse
true false

Date - date et heure. «Sous le capot» est un DateTime . Les constantes sont spécifiées entre guillemets simples, dans l'un des formats .
'now', '2008-08-07 18:11:31', 'last day of next month'

Array est un tableau, le seul type non scalaire. EnvelopperLe tableau . Il n'y a pas de littéraux de ce type, mais les tableaux peuvent être le résultat du travail de générateurs, de fonctions intégrées (par exemple, array () - bonjour PHP 5.3 et inférieur), ou vous pouvez simplement accéder aux clés de variable qui seront créées dynamiquement lors de l'affectation.

 let $a.1 = 100 let $i = 1 let $a[$i + 1] = 200 let $a.sum = $a.1 + $a.2 print " "; $a.sum //  300 var $b = array(true, 3, $a, "Hi") // [true, 3, {1: 100, 2: 200, "sum":300}, "Hi"] 

Lors de l'accès à une variable ou à un élément de tableau inexistant, le script de test sera arrêté et une erreur sera générée. Mais lors de l'exécution des instructions assert ou must , si une variable inexistante est accédée, il n'y aura pas d'erreur, mais la vérification sera considérée comme ayant échoué.

Vérification de l'existence et du type d'une variable


Il faut mentionner séparément la construction de la vérification de l'existence et du type de la variable @ .

Si @ est spécifié dans le nom de la variable au lieu de $ , le résultat de cette construction sera l'un de:

  • une chaîne avec le nom du type de la variable ou le type de l'élément du tableau, si des clés ont été utilisées;
  • null si la variable n'est pas trouvée dans des contextes accessibles ou si l'élément avec la clé spécifiée dans le tableau n'existe pas.

Cette conception peut être utile lors de la vérification de la structure des réponses HTTP.

 var $a.string_field = "Hello World" var $a.number_field = 3.14 var $a.bool_field = true var $a.date_field = '+1 day' var $a.null_field = null var $a.array_field = array(1, "2") assert @a.string_field == "String" assert @a.number_field == "Number" assert @a.bool_field == "Boolean" assert @a.date_field == "Date" assert @a.null_field == "Null" assert @a.array_field == "Array" assert @a.array_field.0 == "Number" assert @a.array_field.1 == "String" assert @a.array_field.2 == null assert @notExistedVar == null 

Ou dans des constructions comme:

 assert @comment.optional_field && $comment.optional_field > 20 

Les opérations booléennes sont optimisées sur le premier opérande. Si le premier opérande est faux , l'opération && n'essaiera même pas de calculer le second opérande. De même avec || .

Enregistrement de données entre les exécutions


J'ai utilisé des scripts séparés non seulement pour les tests après la fin de la tâche, mais aussi pendant le développement. J'ai complété et modifié le script lors de l'implémentation de la fonctionnalité. Plus tard, ce script est devenu la base pour écrire un scénario de test, mais pendant le développement, il fallait répéter les mêmes appels d'API encore et encore. Dans le même temps, à chaque fois dans le script, il a fallu beaucoup de temps pour créer de nouvelles entités à partir de zéro (par exemple, l'enregistrement des utilisateurs), créé des ordures dans la base de données et perturbé de toutes les manières le développement. Par conséquent, j'ai décidé d'ajouter la possibilité d'enregistrer et de restaurer les valeurs des variables entre les démarrages dans le stockage de valeurs-clés.

L'enregistrement est activé par l' option de ligne de commande --storage , qui définit le nom du fichier de stockage:

 pos.phar run ./start.pos --storage=storage.yaml 

Les données sont enregistrées au format YAML, ce qui facilite leur lecture et leur modification.

storage \ get (string $ key, $ defaultValue, boolean $ saveValue = true) - si la clé $ key n'existe pas ou si le fichier de stockage n'est pas spécifié, retourne $ defaultValue. Sinon, il renvoie la valeur stockée. Si l'argument $ saveValue est vrai et que la clé $ key n'a pas été trouvée, $ defaultValue y sera écrit.

storage \ set (string $ key, $ value) - enregistre $ value avec la clé $ key et renvoie $ value. Si le fichier de stockage n'a pas été défini, il renvoie simplement $ value.

stockage \ clé (chaîne $ regexp = null) Tableau- renvoie un tableau de toutes les clés disponibles. Si l'argument $ regexp n'est pas nul, alors les clés correspondant à cette expression régulière seront retournées. Si le fichier de stockage n'a pas été défini, il renvoie un tableau vide.

Sortie vers sortie standard


PieceofScript peut générer des rapports au format JUnit et HTML. Le premier est nécessaire pour l'intégration avec les systèmes CI / CD, par exemple Jenkins. La seconde consiste à visualiser facilement les résultats du test vous-même, par exemple, lors d'un test local. Les fichiers de rapport peuvent être définis au démarrage:
 pos.phar run ./start.pos --junit=junit_report.xml --html=report.html 
Un exemple de rapport HTML

Diverses informations sur le travail de l'interprète sont affichées dans stdout. Il existe 5 niveaux standard de sortie d'informations. Tout ce qui est affiché au même niveau est également affiché sur les autres plus «bavards».

Silencieux - le niveau le plus «silencieux» est défini par l'option de ligne de commande -q .
À ce niveau, rien n'est sorti sur stdout, même les erreurs d'interprétation critiques. Mais par le code retour non nul, vous pouvez comprendre que quelque chose s'est mal passé.

Normal est le niveau par défaut, sans spécifier d'options.
À ce niveau, des erreurs sont générées dans l'interpréteur. Demandes erronées de méthodes API et échec de l' assertion et des vérifications obligatoires .

Verbose - défini par option-v .
À ce niveau, les résultats de l'instruction d' impression sont affichés .

Très verbeux - défini par l'option -vv .
À ce niveau, des avertissements d'interprète sont affichés.

Débogage - défini par l'option -vvv .
À ce niveau, toutes les lignes de script exécutées sont affichées. Toutes les demandes et réponses des méthodes API, les résultats de tous les assert et doivent vérifier .

Des exemples


Le proverbe «Il vaut mieux voir une fois qu'entendre cent fois» est vrai et dans l'interprétation «il vaut mieux voir le code une fois que de lire sa description cent fois». J'ai préparé et mis les exemples dans le référentiel https://github.com/maximw/PosExamples .

Virustotal


Virustotal.com - un service de vérification des fichiers et liens malveillants. Documentation API . Des tests sont effectués pour la partie publique de l'API, à l'exception des méthodes de commentaire, car Je ne veux pas jeter dans une véritable API de «combat» avec des données de test.

Pour accéder à l'API, vous devez vous inscrire , obtenir la clé et l'ajouter au fichier Virustotal / globals.pos .

Exécution de tests:

 pos.phar run ./Virustotal/start.pos 

Che là-bas pour un exe-shnik réside dans un référentiel?
Pour les tests, j'ai copié hiddeninput.exe à partir du composant Console du référentiel Symfony. Ce fichier peut être supprimé et pour les tests, utilisez n'importe quelle autre taille jusqu'à 32 Mo.

Le monastère


Analogue Pasebin. Documentation API .
Pour accéder à l'API, vous devez vous enregistrer , obtenir la clé et l'ajouter au fichier Pastery / globals.pos .

Exécution de tests:

 pos.phar run ./Pastery/start.pos 

Il est à noter qu'avec ces tests, un bug a été trouvé dans la limite du nombre de vues. Il a déjà été corrigé par les développeurs de Pastery.

Le rick and morty


Je pense que cette série animée est connue de beaucoup et appréciée de beaucoup. Documentation API . L'API se compose de trois sections presque identiques: caractère, emplacement et épisode. Par conséquent, les scénarios sont presque les mêmes, et il suffit de regarder les cas de test n'est qu'une des sections.

Exécution de tests:

 pos.phar run ./RickAndMorty/20MinutesTest.pos 

Si vous connaissez une API publique qui serait intéressante à tester de cette manière, veuillez écrire dans un e-mail personnel.

Commentaires et plans pour l'avenir, le cas échéant


0) J'ai une liste de petites et grandes améliorations dont je n'ai pas encore besoin, mais qui peuvent être utiles.

Afficher la liste
  • Json
  • body replace remove
  • , YAML
  • HTTP- ,
  • , run . .
  • HTML- stdout, -vvv
  • https
  • application/x-www-form-urlencoded CURLFile . Guzzle 6,
  • « »,
  • API,
  • HTML-, bootstrap- « », .

1) Pour la validation des modèles dans les réponses API à l'aide de générateurs, il n'y a jusqu'à présent que deux fonctions - similaire () et identique () . La validation avec eux est trop "maladroite". Bien sûr, il est déjà possible de valider les réponses «à la main», et dans certains cas, ce n'est pas possible autrement, mais je veux le rendre plus pratique et, si possible, éviter de vérifier manuellement la réponse. Il existe quelques idées sur la façon de permettre à la fois de générer et de valider des modèles en utilisant la même description de modèle, en évitant les doublons. Mais jusqu'à présent, ces idées n'ont pas été suffisamment formées pour que vous puissiez les implémenter dans le code.

2) Je pense que l'échafaudage des méthodes API basé sur les descriptions dans OpenAPI ( Swagger ), RAML , les collections sera très utileFacteur . Mais cela demande beaucoup de travail si PieceofScript en vaut la peine.

3) Ce serait bien de faire des plugins pour certains IDE, avec mise en évidence du code et auto-complétion. La saisie automatique des noms de cas de test, des méthodes API, des opérateurs et des variables serait tout simplement pratique. Mais il n'a pas encore "creusé" dans cette direction. Comprendre la création de surbrillance pour Sublime Text and Language Server Protocol . Je serais heureux s'il y avait déjà des gens partageant les mêmes idées qui connaissaient de telles choses.

4) Je ne sais pas quelle priorité mettre la possibilité de créer des fonctions connectées dynamiquementimplémenté en PHP. D'une part, tout y est simple, il suffit de s'occuper du chargement automatique et de faire une spécification des classes et des espaces de noms utilisés. En revanche, les fonctions complexes avec leurs dépendances provoqueront inévitablement un conflit d'espace de noms entre les dépendances (dans le pire des cas, les différentes versions). Il y a aussi quelque chose à penser.

5) De bons systèmes de test exécutent des tests indépendants en parallèle. Maintenant, cela peut être fait en lançant l'interpréteur plusieurs fois avec différents fichiers de démarrage, où différents cas de test sont connectés. Mais je pense que nous devons intégrer cela dans l'interpréteur lui-même avec une détection automatique de ce qui peut être lancé en parallèle.

PS D'une part, puisque c'est mon "métier", il serait logique de mettre un post dans le hub "Je suis PR". Par contre, je ne fais pas de relations publiques, je ne cherche aucun gain commercial, juste un outil que je me suis fait, j'ai décidé de "peigner" et de le publier publiquement.

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


All Articles