For the complete documentation index, see llms.txt. This page is also available as Markdown.

Revisite des gestionnaires HTTP

Vous pouvez trouver tout le code ici

Ce livre contient déjà un chapitre sur le test d'un gestionnaire HTTP, mais celui-ci proposera une discussion plus large sur leur conception, afin qu'ils soient simples à tester.

Nous examinerons un exemple réel et comment nous pouvons améliorer sa conception en appliquant des principes tels que le principe de responsabilité unique et la séparation des préoccupations. Ces principes peuvent être réalisés en utilisant des interfaces et l'injection de dépendances. En faisant cela, nous montrerons que tester les gestionnaires est en réalité assez trivial.

Question courante dans la communauté Go illustrée

Tester les gestionnaires HTTP semble être une question récurrente dans la communauté Go, et je pense que cela révèle un problème plus large de malcompréhension de leur conception.

Très souvent, les difficultés que rencontrent les gens avec les tests proviennent de la conception de leur code plutôt que de l'écriture des tests elle-même. Comme je le souligne si souvent dans ce livre :

Si vos tests vous causent de la douleur, écoutez ce signal et réfléchissez à la conception de votre code.

Un exemple

Santosh Kumar m'a tweeté

Comment puis-je tester un gestionnaire http qui a une dépendance à mongodb ?

Voici le code

Énumérons toutes les choses que cette fonction unique doit faire :

  1. Écrire des réponses HTTP, envoyer des en-têtes, des codes d'état, etc.

  2. Décoder le corps de la requête en un User

  3. Se connecter à une base de données (et tous les détails autour de cela)

  4. Interroger la base de données et appliquer une logique métier en fonction du résultat

  5. Générer un mot de passe

  6. Insérer un enregistrement

C'est trop.

Qu'est-ce qu'un gestionnaire HTTP et que devrait-il faire ?

En oubliant les détails spécifiques à Go pour un moment, quelle que soit la langue dans laquelle j'ai travaillé, ce qui m'a toujours bien servi, c'est de penser à la séparation des préoccupations et au principe de responsabilité unique.

Cela peut être assez délicat à appliquer selon le problème que vous résolvez. Qu'est-ce qu'une responsabilité exactement ?

Les lignes peuvent devenir floues selon votre niveau d'abstraction de pensée, et parfois votre première intuition peut ne pas être la bonne.

Heureusement, avec les gestionnaires HTTP, j'ai une assez bonne idée de ce qu'ils devraient faire, quel que soit le projet sur lequel j'ai travaillé :

  1. Accepter une requête HTTP, l'analyser et la valider.

  2. Appeler un ServiceChose pour faire LogiqueMétierImportante avec les données que j'ai obtenues à l'étape 1.

  3. Envoyer une réponse HTTP appropriée en fonction de ce que ServiceChose renvoie.

Je ne dis pas que chaque gestionnaire HTTP jamais créé devrait avoir à peu près cette forme, mais 99 fois sur 100, c'est ce que je constate.

Lorsque vous séparez ces préoccupations :

  • Tester les gestionnaires devient un jeu d'enfant et se concentre sur un petit nombre de préoccupations.

  • Plus important encore, tester LogiqueMétierImportante n'a plus à se préoccuper de HTTP, vous pouvez tester la logique métier proprement.

  • Vous pouvez utiliser LogiqueMétierImportante dans d'autres contextes sans avoir à la modifier.

  • Si LogiqueMétierImportante change ce qu'elle fait, tant que l'interface reste la même, vous n'avez pas à changer vos gestionnaires.

Les gestionnaires de Go

http.HandlerFunc

Le type HandlerFunc est un adaptateur pour permettre l'utilisation de fonctions ordinaires comme gestionnaires HTTP.

type HandlerFunc func(ResponseWriter, *Request)

Lecteur, prenez une respiration et regardez le code ci-dessus. Qu'est-ce que vous remarquez ?

C'est une fonction qui prend des arguments

Il n'y a pas de magie de framework, pas d'annotations, pas de haricots magiques, rien.

C'est juste une fonction, et nous savons comment tester des fonctions.

Cela s'inscrit parfaitement dans le commentaire ci-dessus :

Exemple de test super basique

Pour tester notre fonction, nous l'appelons.

Pour notre test, nous passons un httptest.ResponseRecorder comme argument http.ResponseWriter, et notre fonction l'utilisera pour écrire la réponse HTTP. L'enregistreur enregistrera (ou espionnera) ce qui a été envoyé, et ensuite nous pourrons faire nos assertions.

Appeler un ServiceChose dans notre gestionnaire

Une plainte courante à propos des tutoriels TDD est qu'ils sont toujours "trop simples" et pas assez "réels". Ma réponse à cela est :

Ne serait-il pas agréable que tout votre code soit simple à lire et à tester comme les exemples que vous mentionnez ?

C'est l'un des plus grands défis auxquels nous sommes confrontés, mais nous devons continuer à y travailler. Il est possible (bien que pas nécessairement facile) de concevoir du code de manière à ce qu'il soit simple à lire et à tester si nous pratiquons et appliquons de bons principes d'ingénierie logicielle.

En récapitulant ce que fait le gestionnaire précédent :

  1. Écrire des réponses HTTP, envoyer des en-têtes, des codes d'état, etc.

  2. Décoder le corps de la requête en un User

  3. Se connecter à une base de données (et tous les détails autour de cela)

  4. Interroger la base de données et appliquer une logique métier en fonction du résultat

  5. Générer un mot de passe

  6. Insérer un enregistrement

En prenant l'idée d'une séparation des préoccupations plus idéale, je voudrais que ce soit plutôt comme :

  1. Décoder le corps de la requête en un User

  2. Appeler un UserService.Register(user) (c'est notre ServiceChose)

  3. S'il y a une erreur, agir en conséquence (l'exemple envoie toujours un 400 BadRequest, ce qui je pense n'est pas correct), je vais juste avoir un gestionnaire global de 500 Internal Server Error pour l'instant. Je dois souligner que renvoyer 500 pour toutes les erreurs fait une API terrible ! Plus tard, nous pourrons rendre la gestion des erreurs plus sophistiquée, peut-être avec des types d'erreur.

  4. S'il n'y a pas d'erreur, 201 Created avec l'ID comme corps de la réponse (à nouveau par souci de concision/paresse)

Pour des raisons de brièveté, je ne vais pas revoir le processus TDD habituel, consultez tous les autres chapitres pour des exemples.

Nouvelle conception

Notre méthode RegisterUser correspond à la forme de http.HandlerFunc, donc nous sommes prêts. Nous l'avons attachée comme méthode à un nouveau type UserServer qui contient une dépendance à un UserService qui est capturé comme une interface.

Les interfaces sont un moyen fantastique de s'assurer que nos préoccupations HTTP sont découplées de toute implémentation spécifique ; nous pouvons simplement appeler la méthode sur la dépendance, et nous n'avons pas à nous soucier de comment un utilisateur est enregistré.

Si vous souhaitez explorer cette approche plus en détail en suivant le TDD, lisez le chapitre Injection de Dépendances et le chapitre Serveur HTTP de la section "Construire une application".

Maintenant que nous nous sommes découplés de tout détail d'implémentation spécifique concernant l'enregistrement, l'écriture du code pour notre gestionnaire est simple et suit les responsabilités décrites précédemment.

Les tests !

Cette simplicité se reflète dans nos tests.

Maintenant que notre gestionnaire n'est pas couplé à une implémentation spécifique de stockage, il est trivial pour nous d'écrire un MockUserService pour nous aider à écrire des tests unitaires simples et rapides qui exercent les responsabilités spécifiques qu'il a.

Et le code de la base de données ? Vous trichez !

Tout ceci est très délibéré. Nous ne voulons pas que les gestionnaires HTTP se préoccupent de notre logique métier, de nos bases de données, de nos connexions, etc.

En faisant cela, nous avons libéré le gestionnaire des détails compliqués, nous avons aussi facilité le test de notre couche de persistance et de notre logique métier car elle n'est plus couplée à des détails HTTP non pertinents.

Tout ce que nous devons faire maintenant, c'est implémenter notre UserService en utilisant la base de données que nous voulons utiliser

Nous pouvons tester cela séparément et une fois que nous sommes satisfaits dans main, nous pouvons assembler ces deux unités pour notre application fonctionnelle.

Une conception plus robuste et extensible avec peu d'effort

Ces principes non seulement nous facilitent la vie à court terme, mais rendent également le système plus facile à étendre à l'avenir.

Il ne serait pas surprenant que dans les futures itérations de ce système, nous souhaitions envoyer à l'utilisateur un e-mail de confirmation d'inscription.

Avec l'ancienne conception, nous aurions dû modifier le gestionnaire et les tests environnants. C'est souvent ainsi que des parties de code deviennent impossibles à maintenir, de plus en plus de fonctionnalités s'infiltrent car c'est déjà conçu de cette façon ; pour que le "gestionnaire HTTP" gère... tout !

En séparant les préoccupations à l'aide d'une interface, nous n'avons pas besoin de modifier le gestionnaire du tout car il ne se préoccupe pas de la logique métier autour de l'inscription.

Conclusion

Tester les gestionnaires HTTP de Go n'est pas un défi, mais concevoir un bon logiciel peut l'être !

Les gens font l'erreur de penser que les gestionnaires HTTP sont spéciaux et abandonnent les bonnes pratiques d'ingénierie logicielle lorsqu'ils les écrivent, ce qui rend ensuite leur test difficile.

Répétons-le encore une fois ; les gestionnaires http de Go sont juste des fonctions. Si vous les écrivez comme vous écririez d'autres fonctions, avec des responsabilités claires et une bonne séparation des préoccupations, vous n'aurez aucun problème à les tester, et votre base de code sera plus saine.

Mis à jour