Test unitaire d'un client API et de wrappers

14

J'ai tourné en rond en essayant de trouver la meilleure façon de tester à l'unité une bibliothèque cliente API que je développe. La bibliothèque a une Clientclasse qui a essentiellement un mappage 1: 1 avec l'API, et une Wrapperclasse supplémentaire qui fournit une interface plus conviviale par-dessus le Client.

Wrapper --> Client --> External API

J'ai d'abord écrit un tas de tests contre les deux Clientet Wrapper, en fait, je teste simplement qu'ils transmettent aux fonctions appropriées de tout ce qui fonctionne ( Wrapperfonctionne Client, etClient fonctionne sur une connexion HTTP). J'ai commencé à me sentir mal à l'aise, cependant, car j'ai l'impression de tester l'implémentation de ces classes plutôt que l'interface. En théorie, je pourrais changer les classes pour avoir une autre implémentation parfaitement valide, mais mes tests échoueraient car les fonctions que je m'attendais à appeler ne sont pas appelées. Cela ressemble à des tests fragiles pour moi.

Après cela, j'ai pensé à l'interface des classes. Les tests devraient vérifier que les classes font réellement le travail qu'elles sont censées faire, plutôt que la façon dont elles le font. Alors, comment puis-je faire cela? La première chose qui me vient à l'esprit est de bloquer les demandes d'API externes. Cependant, je suis nerveux à l'idée de simplifier à l'excès le service externe. De nombreux exemples d'API tronqués que j'ai vus donnent des réponses prédéfinies, ce qui semble être un moyen très simple de tester uniquement que votre code s'exécute correctement contre votre fausse API. L'alternative est de se moquer du service, ce qui est tout simplement irréalisable, et devrait être tenu à jour chaque fois que le service réel change - cela ressemble à une surpuissance et à une perte de temps.

Enfin, je lis ceci d' une autre réponse sur les programmeurs SE :

Le travail d'un client API distant consiste à émettre certains appels - ni plus, ni moins. Par conséquent, son test doit vérifier qu'il émet ces appels - ni plus, ni moins.

Et maintenant, je suis plus ou moins convaincu - lors des tests Client, tout ce que j'ai besoin de tester est qu'il fait les bonnes requêtes à l'API (Bien sûr, il y a toujours la possibilité que l'API change mais mes tests continuent de passer - mais c'est où les tests d'intégration seraient utiles). Comme il ne Clients'agit que d'un mappage 1: 1 avec l'API, ma préoccupation avant de passer d'une implémentation valide à une autre ne s'applique pas vraiment - il n'y a qu'une seule implémentation valide pour chaque méthode de Client.

Cependant, je suis toujours coincé avec la Wrapperclasse. Je vois les options suivantes:

  1. Je stoppe la Clientclasse et teste simplement que les méthodes appropriées sont appelées. De cette façon, je fais la même chose que ci-dessus, mais en traitant le Clientcomme un remplaçant pour l'API. Cela me ramène là où j'ai commencé. Encore une fois, cela me donne la sensation inconfortable de tester la mise en œuvre, pas l'interface. Le Wrapperpourrait très bien être implémenté en utilisant un client complètement différent.

  2. Je crée une maquette Client. Maintenant, je dois décider jusqu'où aller avec la moquerie - créer une maquette complète du service demanderait beaucoup d'efforts (plus de travail que ce qui est allé dans la bibliothèque elle-même). L'API elle-même est simple, mais le service est assez complexe (c'est essentiellement une banque de données avec des opérations sur ces données). Et encore une fois, je devrai garder ma maquette en phase avec le réel Client.

  3. Je teste simplement que les requêtes HTTP appropriées sont en cours. Cela signifie que Wrappersera appelé via un Clientobjet réel pour effectuer ces requêtes HTTP, donc je ne le teste pas réellement de manière isolée. Cela en fait un peu un test unitaire terrible.

Je ne suis donc pas particulièrement satisfait de ces solutions. Qu'est-ce que tu ferais? Y a-t-il une bonne façon de procéder?

Joseph Mansfield
la source
J'ai tendance à éviter les tests unitaires dans ces scénarios où une bibliothèque tierce fait la plupart du travail de grognement et j'ai simplement un wrapper (principalement parce que je n'ai aucune idée de comment le faire d'une manière qui teste quelque chose de vraiment significatif). En général, je fais des tests d'intégration dans ces cas, éventuellement avec un service fictif. Peut-être que quelqu'un sait comment faire un test unitaire vraiment significatif pour ceux-ci - j'ai tendance à prioriser les composants les plus critiques du système sous notre contrôle. Ici, la partie critique est hors de notre contrôle. :-(

Réponses:

10

TLDR : Malgré la difficulté, vous devez bloquer le service et l'utiliser pour les tests d'unité client.


Je ne suis pas certain que "le travail d'un client d'API distant consiste à émettre certains appels, ni plus, ni moins ...", à moins que l'API ne soit constituée que de points de terminaison qui renvoient toujours un statut fixe et ne consomment ni ne produisent toutes les données. Ce ne serait pas l'API la plus utile ...

Vous souhaitez également vérifier que le client envoie non seulement les demandes correctes, mais qu'il gère correctement le contenu des réponses, les erreurs, les redirections, etc. Et testez tous ces cas.

Comme vous le constatez, vous devriez avoir des tests d'intégration qui couvrent la pile complète de wrapper -> client -> service -> DB et au-delà, mais pour répondre à votre question principale, sauf si vous avez un environnement où les tests d'intégration peuvent être exécutés dans le cadre de chaque CI construit sans beaucoup de maux de tête (bases de données de test partagées, etc.), vous devriez investir du temps dans la création d'un stub de l'API.

Le stub vous permettra de créer une implémentation fonctionnelle du service, mais sans avoir à implémenter de couche en dessous du service lui-même.

Vous pouvez envisager d'utiliser une solution basée sur DI pour y parvenir, avec une implémentation du modèle de référentiel sous les ressources REST:

  • Remplacez tout le code fonctionnel dans les gestionnaires REST par des appels à un IWwhatRepository.
  • Créez un ProductionWwhatRepository avec le code qui a été extrait des ressources REST et un TestWwhatRespository qui renvoie des réponses prédéfinies à utiliser pendant les tests unitaires.
  • Utilisez le conteneur DI pour injecter la DLL / classe ProductionWwhatRepository ou TestWwhatRepository, etc., selon la configuration.

Quoi qu'il en soit, à moins que le stubbing et / ou la refactorisation du service ne soit hors de question, politiquement ou pratiquement, j'entreprendrais probablement quelque chose de similaire à ce qui précède. Si ce n'est pas possible, je m'assurerais d'avoir une très bonne couverture d'intégration et de les exécuter aussi souvent que possible compte tenu de votre configuration de test disponible.

HTH

Dan1701
la source