Aller au contenu
AFUP AFUP Day 2024 Baromètre Planète PHP PUFA
 

Tester à travers OpenAPI, ou comment valider votre documentation !

Description

Aujourd'hui les APIs sont partout, les documenter correctement est un prérequis pour qu'elles soient utilisées correctement. Cependant un problème reste présent quand on a affaire à de la doc : comment garantir qu'elle est à jour ou que le code l'implémente correctement ?

J'ai rencontré ces problèmes sur un projet où nous utilisons OpenAPI. Malgré la volonté d'avoir une documentation à jour et propre, les utilisateurs et utilisatrices rencontraient des problèmes. J'ai commencé à chercher un moyen de valider cette documentation pour capturer les erreurs avant les utilisateurs et utilisatrices. OpenAPI est devenu le moyen le plus puissant de décrire une API HTTP aujourd'hui. L'écosystème est très riche avec de nombreux générateurs (code et documentation). L'avantage de ce format est qu'il est très technique.

Dans ce talk, je vais vous présenter mes recherches et comment nous avons finalement mise en place des tests automatiques basés sur la documentation. Combiné à un système d'intégration continue, il détecte les erreurs et nous laisse les résoudre avant de passer en production. La documentation permet de vérifier que l'API fonctionne et l'API permet de vérifier que la documentation est à jour.

Aujourd'hui toute l'équipe est plus confiante sur notre code et notre documentation. Toutes ces validations ont également aidé les devs qui n'étaient pas à l'aise avec l'écriture de la documentation à prendre confiance et être sereins lors de modifications…

Conférence donnée lors du Forum PHP 2022, ayant eu lieu les 13 et 14 octobre 2022.

Informations complémentaires

Vidéo

Le speaker

Stéphane HULARD

Depuis 2006, Stéphane baigne dans le web et son écosystème. Après 10 ans en tant que consultant et formateur indépendant, il a démarré une nouvelle aventure et il dirige CH Studio pour accompagner les entreprises à réaliser leurs projets de création de logiciels sur le web. Il apprécie particulièrement travailler sur des projets legacy pour accompagner les équipes à les reprendre en main et il s'obstine à la mise en place des méthodes d'ingénierie logicielle sur le web (intégration et déploiement continu, tests unitaires, documentation...). Chez CH Studio, ils télétravaillent à 100% aujourd'hui ce qui permet quand la situation l'autorise de vivre un peu à l'étranger pour Stéphane.

Verbatim

_ Bonjour à tous. Merci de m'avoir rejoint. Nous allons parler de OpenAPI. Il y a une mini slide avec mon nom et deux ou trois petites choses, après, je vous raconterai une histoire sur un projet sur lequel nous travaillons depuis quatre ans. Nous devons le maintenir, nous essayons de faire les choses correctement. Pour essayer d'éviter de se retrouver à galérer au quotidien. Il y aura beaucoup de codes sur la fin. S'il y a des choses que vous voulez explorer ensuite, il y aura le temps. Je suis là toute la journée. Je dors ici ce soir.

Je suis Stéphane Hulard. Je suis directeur technique sur CH Studio. Nous sommes une société basée en Isère. Nous sommes dans le développement de projets PHP. Je suis formateur de temps en temps. Quand j'ai des demandes qui interviennent par-ci par-là. J'essaie de contribuer dans des moments comme ça et sur des projets open source quand j'ai le temps. Pas autant que je le voudrais, et ça permet d'avoir de l'ouverture sur ce qui est possible de faire sur pas mal d'outils et de pratiques. Qui ne sont pas forcément dans notre quotidien quand nous sommes dans le rush des projets.

Cette conférence a été faite pour développer un outil. Il y a un petit peu de conférence driven-design. J'ai promis que je ferai l'outil. Il a été développé plus tard que ce qui était prévu. Je vous présente le résultat de ces explorations. On va voir ce que ça donne.

OpenAPI, avant d'aller plus loin, un sondage. Avez-vous déjà utilisé ou rédigé une doc d'OpenAPI ? Nous allons faire un petit tour. Des mains ne se sont pas levées. Pour savoir l'historique. Montrer un exemple qui nous servira de support pour le reste de la présentation. Ça permet de documenter les API.

En 2009, c'est arrivé avec des gens qui étaient dans une entreprise et qui se posaient des questions sur comment fiabiliser leur documentation. C'est Swagger. Le premier nom de l'outil tel qu'il est arrivé. Ça a pris un peu d'ampleur en 2011. Il a continué son chemin. Au bout d'un moment, ça a été récupéré par une entreprise qui s'appelle smartgrids. Si vous utilisez des logiciels en ligne, vous voyez leur signature en haut à gauche de l'écran. Ils ont transmis le projet swagger à une initiative qui était sur le projet de Linux Foundation. C'est géré par une documentation ouverte. Il y a de la documentation qui est publiée sur github. Tout ce qui est des réflexions avancées, c'est accessible de manière open. C'est intéressant. Voir la direction que ça prend et ce qu'il sera possible de faire de même. Les travers qu'ils ont rencontrés.

La toute première version, c'était la version 3.0. C'est la version 3.1. Qu'est-ce que je fais pour déclarer si une donnée est nulle ou pas ? Ça permet de situer où nous en sommes aujourd'hui.

C'est le nom qui va nous servir pour la suite.

Pourquoi OpenAPI ? Les personnes se sont posées la question à un moment donné. Comment je peux fiabiliser ma documentation ? Comment je peux décrire cette API pour pouvoir communiquer ? Avec des humains. Mais surtout avec les machines. On a besoin de décrire ce qui va être possible. Comment je peux communiquer entre machines en utilisant ces différentes possibilités de mon API ?Aujourd'hui, si on prend du recul sur le Web, tous les systèmes qui utilisent PHP, on utilise une requête qui va être utilisée par le serveur, ça va générer une réponse. Il y a des API techniques. Mais OpenAPI, ça permet de décrire le comportement du serveur HTTP et ce que je peux faire avec. C'est quoi le format des requêtes qui est utilisé et possible. C'est quoi le format des réponses qui sera récupérable ?

Aujourd'hui, beaucoup de développeurs travaillent sur des applications qui contiennent un petit bout d'API. C'est sympa de savoir à l'avance c'est quoi le format de données qui est accepté. Ces applications, à travers OpenAPI, elles ont un format technique, c'était écrit en YAML et en Json. Mais YAML, c'est facile à manipuler. Vous devez en écrire régulièrement.

OpenAPI ne vient pas juste avec un certain lot... la documentation est une spécification. Ils ont posé la question pour savoir comment faire les choses. Bien. Comment je peux avoir une documentation tapée et qui va me servir le mieux possible dans mon quotidien. Design-first : j'utilise ma documentation pour me poser les questions en amont. Les différentes données, les noms des propriétés, etc. Le deuxième point : Single Source of Truth. La personne qui utilise API Platform, si vous utilisez les annotations qui permettent de générer le fichier qui va bien, elle fait partie de votre code. N'exportez pas ce fichier sur le Web complètement à l'extérieur pour avoir la version intégrée à votre code et une version externe et vous ne savez pas laquelle est à jour et laquelle ne l'est pas. Soit vous automatisez et vous devez garantir que les versions sont cohérentes, soit vous gardez un seul point de contact avec les deux versions.

De notre côté, nous avons choisi de l'écrire à la main. J'aime écrire manuel au quotidien.

Dernière chose, nous tentons d'avoir du contrôle de version sur notre documentation. Suivre l'évolution, avoir une trace des modifications appliquées.

Un exemple. C'est du YAML. Très simple. Ça reprend les principaux points importants par rapport à ce que je vais vous présenter ensuite. C'est très simplifié. Nous retrouvons les informations les plus critiques. Nous avons des points d'entrée principaux dans notre documentation. Nous voyons : Infos, serveurs, paths, components. L'info, la documentation générale. Comme j'appelle mon API. Serveurs, c'est intéressant, parce que ça permet de décrire des serveurs qui, lorsqu'ils sont accessibles, on pourra faire des vraies requêtes. Pour tester, je ne suis pas obligé de montrer toute une stack avec le serveur HTTP pour tourner le projet. Si vous utilisez un générateur de doc, vous pouvez avoir un petit bouton "try", je fais un appel d'API qui va bien. Je peux voir le résultat dans l'interface de la doc. Paths, la partie la plus importante. Les différentes routes accessibles. le mot-clé de la ref. Vous pouvez split OpenAPI en plusieurs fichiers. C'est même conseillé. Pour vous servir des différents fichiers comme des petits blocs de Lego. J'assemble différentes réponses et différents schémas de données pour pouvoir répondre à mon besoin. Vous allez pouvoir utiliser différentes réponses à plusieurs endroits de votre application.

Le dernier point, components. Je peux décrire des choses qui vont être annexes. Des schémas d'accès à la sécurité. Je peux me connecter sur mon API en utilisant un header qui s'appelle sso_uid. Le détail, vous pourrez le voir en ligne. La référence est complète. C'est en anglais. Il y a beaucoup de choses à découvrir.

Je mets en surveillance la partie qui nous intéresse le plus. Je décris ensuite le contenu du fichier que je référence juste au-dessus qui s'appelle : users/me.yaml. C'est la description de mon point d'API. À l'intérieur, j'ai plusieurs possibilités. Nous sommes en HTTP. Je vais avoir un verbe qui correspond ici à "get" et tout un tas d'informations qu'on décrit. Qu'est-ce que je peux faire en appelant cette API en "get". Ce qui est important, c'est qu'on retrouve... Décidément... ** la partie sécurité. Une méthode que j'ai spécifiée dans le fichier précédent. On retrouve la partie réponse. Je fais un appel. Je récupère une réponse. C'est ce qui nous intéresse ici.

Pour la plupart des éléments, nous avons un élément des descriptions, des sommaires. Nous avons beaucoup d'informations textuelles.

Je dis que si ma réponse est de type 200 et le type d'application Json, ça va me renvoyer à des données qui sont définies dans le fichier suivant. Nous avons le #/user. Je peux référencer un élément qui est présent dans ce fichier. Il y a des relations directes entre les références. C'est très souple.

Le schéma, ça commence à arriver dans le sujet de la conférence. On parle de structure de données. Comment je fais pour garantir que ces données respectent le format que j'ai décrit ? Je dis que notre user contient trois propriétés essentiellement. Je vais définir des types de contenus dans ces trois propriétés. J'ai une string qui est de type date. Je vais avoir aussi des informations requises ici. Dans mon objet user. J'ai un name et un mail. Si je n'ai pas ces deux informations, ça pose un problème.

C'était un tour d'horizon. Je vais vous raconter la petite histoire du projet sur lequel nous sommes en train de travailler. C'est un projet d'API. C'est la source de données présentes. C'est consolidé par un front-end. Nous avons la souplesse de faire des modifications sans que ça impacte personne d'autre que nous-mêmes. Si nous faisons les modifications dans le front et dans le back... c'est important que ces deux sujets soient bien séparés. Nous avons des développeurs qui travaillent d'un côté et de l'autre. Nous ne sommes pas 50. Certains font les deux. Nous essayons de nous mettre d'accord rapidement pour savoir le format sur lequel nous allons échanger pour les deux parties. Cette API, il faut qu'elle soit correctement documentée.

Je dis tout cela comme si ça fonctionnait super bien. Comme si tout était nickel. C'est encore en cours. Mais le chemin que nous avons parcouru pour l'instant, je juge que ça nous a vraiment aidés à améliorer la collaboration entre les personnes. Ça nous aide à valoriser notre travail. La documentation des API au début. Nous l'écrivions en macdonne*. Au final, c'était bien. Mais au bout de deux semaines, quand nous avions modifié quelque chose de côté, rien ne nous indiquait que nous nous étions trompés. Nous nous sommes retrouvés avec de la documentation obsolète mélangée avec de la documentation à jour. C'était n'importe quoi. Quand nous avons commencé à avoir ces incohérences, nous nous sommes dit : nous avons utilisé un langage qui ne va pas. Pourquoi ne pas tenter un autre outil ? La bonne volonté, c'était super. Mais il fallait des process plus précis pour travailler efficacement.

Nous nous sommes intéressés à OpenAPI. Ça nous permet d'avoir un langage descriptif du fonctionnement de notre API. Avoir tout un tas d'outils. Au début, c'était surtout parce que je trouvais les interfaces de visualisation tellement cool. Si je pouvais livrer aux clients ce petit bout de HTML avec l'URL et les exemples de réponse, c'était magique. Ils avaient l'impression qu'on avait un truc incroyable. Alors qu'on avait seulement écrit "diamètre" et lancé une commande. "C'est bien beau d'avoir cela, mais si nous essayons d'avancer un peu plus et d'avoir un peu de logique pour nous aider à valider notre travail, ce sera encore mieux." Nous avons fait le choix d'écrire cette doc à la main. Peut-être que nous y reviendrons plus tard. Je parlais d'annotations dans le deep platform. Il y a plein de solutions. Nous ne sommes pas forcés d'utiliser une méthode ou une autre. Le présent et ce qui est possible.

Nous commencions à écrire de l'OpenAPI à la main. Nous avions notre pipeline continu qui nous générait... on peut livrer aux clients. Ça n'a pas changé le problème. Le macdonne* était super. Le YAML était mieux. J'oublie de mettre à jour le YAML, ça ne change pas le problème. La documentation n'a de sens que si elle reflète l'état actuel. Ça fait écho à ce qu'on a entendu hier. La documentation, si elle est là, mais qu'elle ne vit pas correctement, c'est un vrai problème. Une API, c'est une suite d'outils. Il y a plein de choses dans l'écosystème. Pour faire du mocking et de la génération. Grâce à la création technique, je peux créer un faux serveur, de fausses requêtes sans trop de problèmes. Nous voulions de vrais tests sur notre application. Ne pas créer quelque chose qui soit complètement valide. Mais valide dans aucun contexte concret.

Il y a plein de choses qui sont magnifiques dans cet écosystème. JoliCode a créé un truc pour générer plein de codes. Nous ne sommes pas par rapport à ce qu'on voulait comme besoin. Nous voulions faire de la validation. J'ai trouvé un package qui s'appelle league/openapi-psr7-validator. Son rôle était de dire que je m'assure que ce que je trouve en paramètre... que c'est du bon type. Je vous laisse aller voir le code. Pour voir que ce n'est pas si facile dans l'implémentation. YAML décrit exactement ce qu'on attend. Nous avons déjà une première étape de franchie. Je peux valider que les requêtes qui rentrent sont vraiment propres.

Ça va peut-être à l'encontre de certaines idées. J'adore les PSRs HTTP. C'est un écosystème qui me faisait vibrer. Il utilise le PSR 7, tout ce qui va être autour de l'interopérabilité. Il est conçu comme un middleware. On peut l'utiliser sur n'importe quel Framework. J'ai une requête qui entre en question, je peux la valider. Il est agnostic, que vous fassiez du Laravel ou du Symfony. Ça peut marcher si on utilise PSR

  1. Il y a une librairie qui est très complète.

J'ai commencé à m'intéresser à ce truc en disant : Je vais essayer de plugger cela dans notre écosystème de projet. Un projet Symfony avec des tests PHP. Quelque chose qui est assez standard dans l'écosystème aujourd'hui. Pour la plupart d'entre vous puisque nous sommes en France.

L'idée générale, c'était de s'appuyer sur un schéma qui ressemble globalement à cela. J'ai une requête. Je la valide. J'ai un client HTTP. C'est bien de valider les requêtes et les réponses. Je ne vais pas le faire sur la prod parce que si ça pète, ça va me faire une belle jambe. Je veux m'assurer que ça roule. J'ai une réponse. Elle va être validée. S'il y a un problème, j'ai envie de récupérer les erreurs de validation. Les remonter à l'utilisateur. Dans l'objectif, ce serait un développeur. Que ce soit corrigé avant de déployer. Le fait de charger les requêtes réelles en prod, c'est déconseillé, c'est lourd. Avec YAML, beaucoup de validations.

Je suis toujours un grand fan de PSR. Nous avons PSR 7 avec les messages. Nous avons des requêtes et des réponses. "Peut-être que ce que j'aimerais, c'est embarquer tout cela dans une suite de tests PHP unique. Je ne veux pas être dans le cadre d'un serveur qui existe pour de vrai. Que j'appelle pour vrai avec un appel HTTP et une vraie réponse." Si j'ai à faire rouler tout cela directement de façon très technique, ce serait encore mieux.

Ensuite, nous avons les Factories. Je peux créer des requêtes et des réponses. De créer des corps de requête, des URL, etc. Nous avons le PSR 18 pour le client.

Ça fait beaucoup de boiler plate code. Ce paquet permet de transformer une requête Symfony en requête PSR 7. Il permet de faire l'inverse. Avec que la réponse PSR 7, on peut la transformer en Symfony. Nous sommes capables de faire le travail dans les deux sens. Le package de validation, il parle le langage PSR 7. Nous pouvons faire le bris entre les deux.

Le http Kernel de Symfony, il a une méthode crumble décrite ici. Il prend la requête et me retourne une réponse. Je peux lui demander d'exécuter n'importe quelle requête que j'ai construite pour exécuter la valeur. Sans passer à HTTP. Nous utilisons des client interfaces qui viennent de PSR. Ce n'est pas important.

Nous commençons à monter petit à petit un petit pock. "Nous arrivons à brancher ce package qui s'appelle ligue OpenAPI validation PSR7." On commençait à voir des erreurs. Des points API où il n'y avait pas de contraintes, très faciles. Il y avait de l'intérêt à continuer dans cette direction.

Qu'est-ce qu'on peut voir avec quelle histoire d'implémentation ? Nous avons un builder. Ça vient de la librairie OpenAPI. Il me charge le schéma YAML. Je lui donne le chemin vers mon fichier YAML de configuration. Les $ref sont référés. Je lui donne le fichier. S'il arrive à l'ouvrir, ça roule. Je récupère un objet riquest validator. Je passe la requête en parallèle. Je passe la requête, j'ai une erreur, je peux le traiter.

HTTP client, j'ai ma réponse, je vais le valider et je génère un objet qui s'appelle réponse validator.

Il faut dire que la réponse est liée à un certain URL. Il faut qu'il arrive à le retrouver à l'intérieur du schéma. C'est le point d'entrée.

Je reviens à mon idée générale. Nous sommes capables de faire pas mal de petites choses déjà. Gérer la première étape de validation, la dernière étape de validation, nous ne savons pas trop encore comment on fait. Pour générer de vraies requêtes. Nous avons un carnet client qui peut être utilisé. Globalement, si nous sommes capables de mettre tout cela ensemble, nous pouvons mettre cela en fichier test. Ce ne sera peut-être pas très joli, j'aurai un vrai contrôle sur ce qui se passe. Mais pour l'instant, une requête, c'est rien. Une réponse, on peut la percer, mais la requête, en entrée, il faut créer les cas de tests. Qu'est-ce que je valide ? Nous sommes allés plus loin : on veut essayer d'intégrer des tests de métier. Qu'est-ce que je veux tester ? Générer une requête pour accéder à tel URL.

Nous avons créé un outil. C'est Raven. Nous n'avons pas trouvé un autre parlant. C'est le petit délire. Nous nous sommes dit : Ce n'est pas OpenAPI. On va valider par rapport à la documentation. On va utiliser de vraies données pour tester. On va vérifier le comportement de l'API. Toute logique de validation est déléguée à cette librairie du PHP League qui est bien maintenue et solide. On va construire la requête. Fakerphp, c'est bien utilisé dans l'écosystème. L'intérêt de Faker, il va nous permettre de créer des pré-valideurs. "Va me chercher le vrai identifiant de l'utilisateur. Celui qui est généré à la volée quand je démarre dans mes fixtures. Avec des caractères et des tirets aléatoires." Nous avons cette possibilité de résoudre les données pour faire quelque chose. De très lié à ce que nous faisons au quotidien. Nous allons pouvoir ajouter des contraintes de validation. C'est la petite couche : Être capable de dire si j'appelle l'API... je vais avoir décrit que l'API peut me renvoyer une 200 ou une 400. Quand je fais l'appel avec une requête valide, si on me renvoie une 400, le valideur est content. La réponse existe. Elle est validée et correcte. Mais ce qu'on veut ajouter, c'est que cette requête précise, on s'attend à ce qu'elle envoie une 200. "Je vais générer des requêtes avec des contraintes de validations qui vont être utilisées pour pouvoir faire les tests."

Nous allons avoir des trucs qui ressemblent à cela. Ce sont des tableaux principalement. J'aime bien YAML. On peut changer cela depuis YAML et l'utiliser directement avec un peu de logique autour. Ce qu'il est important de voir ici. Les petites lignes que j'ai mises en surbrillance. Notre URL ici va être dynamique. Le /ID entre accolades, l'identifiant va changer chaque fois que je lance l'application. Je veux pouvoir aller le générer automatiquement. Alice, pour ceux qui utilisent déjà ce genre d'outils, vous ne devriez pas être perdus. Les chevrons, les parenthèses, la possibilité de passer des paramètres. C'est le même délire. Nous nous appuyons sur ce qui existe. Ça nous permet de générer un corps de requête qui est très lié à ce qu'on cherche à faire.

J'ai appelé cela expectations, je m'attends que cet appel nous renvoie à un 403. Juste avant l'objet executor qui exécute du code. Il va ajouter des validations en plus : ma requête existe dans ma doc, elle est valide. Ma réponse existe dans ma doc, elle est valide. Je vais avoir des vérifications côté utilisateur. Le statuscode, c'est basique. C'est de dire : J'ai cette information dans mon Json de résultat qui contient cette valeur. Sur github, il n'y a pas cette information. C'est possible d'ajouter du niveau. Les expectations, on peut le laisser en ayant des interfaces.

On avance. Si on peut avoir de la couverture de code en utilisant les PHP unit, je peux voir le code qui a été couvert dans mes tests. Ce serait le pied. Nous commençons à implémenter cela.

Ça ressemble à cela. J'ai une méthode. Une data provider. J'ai mes tableaux, je déclare la requête. À l'intérieur de ma fonction, j'ai un bout de code qui ressemble à cela. La première partie, je charge mon schéma. Ça pourrait être externalisé. C'est un code d'exemple. Ne jugeons pas le code. J'initialise mon objet de tests. Il y a deux trucs qui vont être responsables d'initialiser les tests. Je construis ma requête. Les expectations qui vont avec. Je lance des tests. Si j'ai une exception pour l'envoyer, je capture le message de l'exception. En gros, j'ajoute une petite insertion PHP unit. Si je ne mets pas d'insertion, PHP Unit ne calcule pas le code coverage. Si mon message existe, c'est un problème. Je vais afficher le message. Ça permet d'avoir des trucs qui ressemblent à cela. J'ai fait exprès d'utiliser le message. Vous pouvez avoir le rapport de base avec les petits points et les F. Mais ça le met en lumière de façon plus lisible. Je vais avoir des erreurs comme cela qui vont popper dans mes tests. Je ne passe pas par browser kit, mais par Kernel Symfony. La validation a eu lieu, ça me renvoie à une erreur. Pareil avec un autre type d'erreur. J'ai une propriété qui est là et qui ne devrait pas être là. Je vais avoir les informations qui ont été renvoyées. On me dit qu'il manque la propriété data.attributes.email. Avec la petite phrase qui va bien.

Ce que je vous disais, cette histoire de conférence, la toute première version était terminée il y a une semaine. Il reste pas mal de boulot à faire pour avoir un truc plus pratique. Avoir plein de tests possibles. L'idée, c'est d'avoir plein de petits bouts de documentation qui sont testés et validés. Ça respecte le schéma, ça vérifie avec de vraies requêtes, ce sont des vérifications qui sont spécifiques au projet. C'est top.

Maintenant, vous pouvez tous tester votre application avec OpenAPI. Ça utilise des standards PSR parce que j'aime bien cela. Et vu qu'on est au niveau librairie, l'interopérabilité est vraiment importante. C'est élastique. On peut l'utiliser dans plusieurs contextes différents. Les contributions sont les bienvenues.

Comment démarrer ? La doc OpenAPI que nous avons vue, il y a un petit nombre de points d'API. Ce n'est pas grave. Il vaut mieux avancer un pas à la fois que trop vite. Tout ne doit pas être documenté. Si nous arrivons à documenter un point d'API, et que nous arrivons à le valider correctement, c'est top. On en ajoute un demain, et on réussit à avoir un workflow de testeur et de validation.

Préparez-vous au DDD. Documentation-Driven Design. C'est un mixte du DDD. c'est la réflexion autour de la documentation. Ce concept de la documentation driven design illustre bien les choses. Comment réussir à mieux collaborer entre le back et le front ? nous n'avons pas des API tordus. Ça nous permet de nous poser des questions au moment où c'est le plus opportun pour faire des choix. Après que les choix aient été faits. Ils sont intérinés et on les implémente.

C'est tout pour moi. Merci de votre attention.

(applaudissements)