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.

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
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 :
Écrire des réponses HTTP, envoyer des en-têtes, des codes d'état, etc.
Décoder le corps de la requête en un
UserSe connecter à une base de données (et tous les détails autour de cela)
Interroger la base de données et appliquer une logique métier en fonction du résultat
Générer un mot de passe
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é :
Accepter une requête HTTP, l'analyser et la valider.
Appeler un
ServiceChosepour faireLogiqueMétierImportanteavec les données que j'ai obtenues à l'étape 1.Envoyer une réponse
HTTPappropriée en fonction de ce queServiceChoserenvoie.
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étierImportanten'a plus à se préoccuper deHTTP, vous pouvez tester la logique métier proprement.Vous pouvez utiliser
LogiqueMétierImportantedans d'autres contextes sans avoir à la modifier.Si
LogiqueMétierImportantechange ce qu'elle fait, tant que l'interface reste la même, vous n'avez pas à changer vos gestionnaires.
Les gestionnaires de Go
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 :
Elle prend un
http.Requestqui n'est qu'un ensemble de données que nous devons inspecter, analyser et valider.
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
ServiceChose dans notre gestionnaireUne 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 :
Écrire des réponses HTTP, envoyer des en-têtes, des codes d'état, etc.
Décoder le corps de la requête en un
UserSe connecter à une base de données (et tous les détails autour de cela)
Interroger la base de données et appliquer une logique métier en fonction du résultat
Générer un mot de passe
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 :
Décoder le corps de la requête en un
UserAppeler un
UserService.Register(user)(c'est notreServiceChose)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 de500 Internal Server Errorpour l'instant. Je dois souligner que renvoyer500pour 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.S'il n'y a pas d'erreur,
201 Createdavec 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