Aujourd’hui, j’aimerais vous démontrer que le TDD est applicable aussi pour driver l’implémentation de requêtes HTTP vers des apis externes, et le parsing de leur réponse.
Évidemment, pour des soucis de vitesse d’exécution de test, et donc de qualité du feedback, je ne vais pas faire de vraies appels à l’API.
L’objectif est donc de tester l’implémentation d’un contrat d’API externe, en mémoire, avec des données fixes, pour ne pas avoir de tests fragiles.
Impossible en TDD vous dites ? Vous allez voir ;)
La première étape du TDD : THINK
Avant de se lancer tête la première dans le code, il faut toujours se poser deux minutes pour comprendre ce que l’on veut faire.
Dans mon cas je veux :
faire un appel à l’API de substack pour récupérer les derniers subscribers inscrits à ma newslettr
extraire uniquement l’email des subscribers de la réponse (pour ensuite les sauvegarder dans une base de données, mais c’est en dehors du scope de cet article)
D’un point de vue technique, je sais que je vais avoir besoin de :
connaître l’url et le format de l’API substack à utiliser
comprendre le format de la réponse substack pour en extraire uniquement les emails.
Bien qu’il s’agisse ici d’un code d’intégration (car on communique avec le monde extérieur), le TDD peut nous aider à “driver” notre code, et nous donner une indication claire que nous avons terminé 💪
“Mais un test d’intégration, par définition, est plus lent ! Comment profiter du feedback rapide qu’est censé apporté le TDD dans ce cas précis ?”
C’est une réflexion totalement valide ! Et ici, nous allons utiliser un petit utilitaire que j’aime beaucoup, qui permet de “simuler” des requêtes HTTP préparées, en mémoire, donc avec un feedback instantané :)
C’est parti !
Révéler l’intention clairement à travers le nom du test
Quand je rédige un test, j’aime commencer par l’assertion. Cela permet de ne pas se focaliser sur des détails d’implémentation, et d’avoir directement sous les yeux notre objectif final : notre définition of done.
Voici à quoi cela pourrait ressembler dans notre cas :
Les points importants à noter ici :
L’intitulé du test expose clairement notre intention, sans exposer de détails techniques
La variable
subscribers
n’existe évidemment pas encore. J’expose ici le “contrat” de sortie du comportement que je souhaite tester : peu importe comment cela va être implémenté, je veux avoir en sortie ce format de données.
Écrire le minimum de code pour faire passer le test
Cette étape est souvent celle qui rend le plus confus les débutants du TDD. Qu’est-ce que veut bien pouvoir dire “écrire le minimum de code pour faire passer le test” ?
Cette étape à principalement deux objectifs :
faire passer le test le plus rapidement possible au vert
nous faire avancer vers le design cible du code
Je dois préciser ce dernier point. Faire avancer vers le design cible ne veut pas dire que c’est à cette étape que l’on doit forcément écrire toutes les abstractions, au contraire. Cette étape va permettre l’apparition de ces éléments lors de la phase de refactoring.
MAIS ! Parce qu’il y a toujours un “mais” ;) Quand on commence à être plus à l’aise avec la notion de design testable, on peut prendre des raccourcis.
Comment savoir que prendre ce raccourci ne revient pas à nous tirer une balle dans le pied ? Eh bien tout simplement en suivant une heuristique simple : si vous passez plus de X minutes à faire passer votre test au vert, c’est que vous êtes “bloqués”, et donc que vous avez pris un raccourci trop important. X minutes étant par exemple 1 minute, 5, ou 10. Ça dépend de vous et du temps que vous vous accordez et avec lequel vous êtes le plus à l’aise :).
Revenons à nos moutons ! La prochaine étape est de faire apparaître la variable subscribers
qui n'existe pas encore.
Cette variable doit contenir le résultat de la récupération des derniers subscribers. Je ne veux aucun détail d’implémentation dans mon test, je veux que celui-ci puisse se lire facilement et que l’on comprenne son intention. Voici ce que je peux écrire :
On pourrait prétendre, à raison, que j’ai quand même introduit deux informations sur des détails d’implémentation ici :
la notion de “gateway” en appelant ma prochaine variable non encore définie “
substackGateway
”la notion d’asynchrone avec le mot clé
await
Comme toujours, il s’agit ici d’être pragmatique. Le comportement que je teste ici est intrinsèquement asynchrone, et je sais que la donnée doit être récupérée depuis substack, donc depuis l’extérieur. Ici, l’asynchrone et la notion de “gateway” font donc partie intégrante du comportement à tester (même si rien n’oblige à appeler cela un gateway mais c’est plutôt dans le lingo habituel).
La variable substackGateway
n’étant pas définie, c’est notre prochaine étape. On pourrait par exemple écrire à la ligne juste au dessus :
const substackGateway = new SubstackGateway()
(le fait d’utiliser un “new” EST un détail d’implémentation dont on pourrait se passer, mais ici on va considérer qu’on s’autorise ce détail).
La prochaine étape est donc de définir cette classe :
Comme vous pouvez le voir, je retourne “en dur” les données qu’attendent le test, pour le faire passer au vert le plus simplement possible.
Exposer clairement l’état initial dans le test
Comme vous le savez certainement, un test doit s’articuler autour de 3 parties :
arrange
act
assert
Nous nous sommes occupés des deux derniers éléments pour l’instant. Il reste maintenant à mettre en place le “arrange”.
L’intérêt de ces trois étapes est de comprendre dès la première lecture tout ce dont on a besoin de savoir pour comprendre le comportement testé.
Quand on lit un test, on doit donc facilement pouvoir se dire : “Ok, étant donné ces données initiales, quand on effectue telle action, alors le résultat final est bien celui attendu”.
Pour ne pas rentrer dans les détails d’implémentation immédiatement, on pourrait écrire quelque chose de ce style :
Le test passe immédiatement au rouge, puisque la fonction substackWillRespondWith()
n’existe pas encore.
En ajoutant cette fonction, on rend le test beaucoup plus lisible et compréhensible pour n’importe qui. Notez que je n’ai pas introduit le modèle de réponse réelle de substack ! Ici, c’est un détail d’implémentation, ça polluerait le test pour rien et rendrait sa compréhension plus compliquée.
Notre test expose ici clairement tout ce que l’on doit savoir pour comprendre le comportement testé.
Il est maintenant temps d’introduire la notion de requête HTTP
Driver l’implémentation d’appels HTTP avec nock
Notre test est au vert, notre comportement est clairement exprimé, mais avons-nous terminé pour autant ? Bien sûr que non, puisque nous renvoyons la donnée directement dans la classe SubstackGateway.
Quelle est la prochaine étape ici ?
En pur TDD, il faudrait ajouter un nouveau test pour forcer l’écriture d’un code plus proche de celui de production.
La raison pour laquelle on doit rajouter un test, c’est pour forcer la “spécialisation” du code de production. À mesure que les tests deviennent plus spécifiques, le code devient plus générique.
Dans ce cas précis, la “spécialisation” que l’on cherche à voir apparaître est la notion d’appel HTTP. Il ne sert à rien de chercher à faire apparaître cette notion d’appel HTTP d’elle-même, à travers d’éventuels tests. On SAIT qu’on doit faire un appel HTTP dans ce cas précis, donc on peut s’autoriser à écrire directement du code de test qui va nous forcer à devoir faire un appel HTTP !
C’est notre prochaine étape ! Voici comment on pourrait implémenter la fonction substackWillRespondWith
Petite explication rapide sur nock. Nock permet, entre autre, d’intercepter des appels HTTP sur des routes “paramétrées” en amont.
Le code ci-dessus va donc “intercepter” un appel GET HTTP à la route https://example.com. Il va retourner une réponse 200 qui contient la liste des subscribers.
On peut maintenant supprimer la réponse en dur dans SubstackGateway pour que le test repasse au rouge, et qu’on implémente l’appel HTTP (pour les puristes, il faudrait en théorie ici créer un autre test, avec des jeux de données différents, pour arriver à cette étape petit à petit, mais il faut savoir être pragmatique, et parfois prendre des raccourcis sur la théorie comme je l’ai expliqué plus haut).
J’utilise axios, mais vous pouvez utiliser ce que vous voulez, c’est un détail d’implémentation. :)
“Mais pourquoi utiliser une route bidon genre example.com et pas directement la vraie route de substack, avec la vraie forme de la réponse ?”
Très bonne question ! Et la réponse est toujours la même : les baby steps !
Rappelez-vous que tout l’intérêt du TDD c’est d’être guidé, d’avoir un feedback rapide, et donc de ne pas sauter d’étapes.
Juste avant, nous étions à l’étape de retourner les données en dur. Pour arriver au résultat final, il y a encore beaucoup d’étapes :
faire un vrai appel HTTP
appeler l’API avec les bons paramètres dans la requête
parser la réponse pour obtenir le résultat souhaité
Cela fait beaucoup d’étapes à faire d’un coup ! On s’est donc concentrés dans un premier temps sur l’appel HTTP, dans le cas le plus simple possible.
La prochaine étape est de se concentrer sur faire le bon appel à cette API.
Driver l’envoi de la bonne requête HTTP
Pour information, l’api (non officielle) de substack pour récupérer les subscribers est de la forme :
POST https://craftacademy.substack.com/api/v1/subscriber-stats
Avec en body :
{ limit: 50, offset: 0, filters: { order_by_desc_nulls_last: ‘subscription_created_at’ } }
Commençons par se laisser guider grâce à TDD dans l’implémentation de la bonne URL, et avec un POST et non plus un GET :
Pour en finir avec le format de la requête, ajoutons maintenant le body attendu :
Comme vous pouvez le voir, nock permet ici de vraiment intercepter l’appel seulement si le bon body de la request est présent ! Ce qui est génial, car ça nous force à implémenter correctement l’appel :
Il nous reste maintenant à “driver” le parsing de la réponse !
Driver le parsing de la réponse Substack
La réponse de l’API de substack est immense, il y a énormément de données, je vous épargne les détails ici, sachez juste que ce qui nous intéresse c’est le champ subscribers
de la réponse, qui est un tableau qui expose des objets concernant les subscribers. Ce qui nous intéresse dans ces objets subscribers c’est l’email, exposé sous le propriété “user_email_address”.
C’est donc ce que l’on doit tester !
Maintenant que l’on renvoie une réponse qui a la forme de la vraie réponse de substack, il faut donc modifier notre gateway pour qu’elle parse correctement la réponse :
“Petite question : pourquoi ne pas directement passer un objet correspondant au format de réponse substack directement dans la fonction substackWillRespondWith ? Ça éviterait de faire de la logique dans le test, avec le “map” qui augmente la complexité cyclomatique du test qui est censé restée normalement à 1”
C’est très juste ! Mais je privilégie toujours la lisibilité du test sur les détails d’implémentation. Ici, cela n’impacterait pas tellement la lisibilité, donc on pourrait se le permettre. Mais de manière générale je préfère éviter, pour que le test soit lisible sans “bruit” pour un humain. On a besoin de savoir ici que substack va répondre avec des emails, la façon dont substack va répondre en détail on s’en fiche pour comprendre le test !
Et voilà ! Nous avons implémenté un gateway vers Substack extrêmement simple, en se laissant guider par le feedback instantanée apporté par le TDD :)
Limites de cette approche
Cette approche nous a permis de développer le SubstackGateway, mais rien nous assure que cela va vraiment fonctionner une fois en production !
Je vous ai passé ici les détails de l’authentification, mais un des problèmes qui pourrait arriver est tout simplement le changement de l’API par exemple ! Ce test nous permet d’être sûr que notre implémentation communique correctement avec les contrats actuels de Substack (contrat au sens interface d’API).
Il nous faut donc en plus un vrai test d’intégration, qui fera un vrai appel à Substack pour vérifier que le contrat de l’API côté substack n’a pas changé. Ce faisant, ce test d’intégration peut faire office de contract test, mais c’est un autre sujet :)
Comme d’habitude, si vous avez des questions, n’hésitez pas à les poser en réponse à cet article :)
Happy Coding !
Pierre.
Merci, ce serait plus clair d'écrire dans cet ordre
assert
act
arrange.
Faire échouer un test>le faire passer>clean (c'est l'ordre chronologique)
De plus mais bon ça c'est selon les goûts. Personnellement je n'aime pas être dépendant de librairies extérieures si je peux facilement m'en passer. Et mocker soi-même les requêtes sans nock et axios est aussi possible. Mais bon c'est mon côté psychorigide peut-être.