Test d'unité d'une classe qui utilise DI sans tester sur les internes


12

J'ai une classe qui est refactorisée en 1 classe principale et 2 classes plus petites. Les classes principales utilisent la base de données (comme beaucoup de mes classes) et envoient un e-mail. La classe principale a donc un IPersonRepositoryet un IEmailRepositoryinjecté qui à son tour envoie aux 2 classes plus petites.

Maintenant, je veux tester unitaire la classe principale, et j'ai appris à ne pas uniter le fonctionnement interne de la classe, parce que nous devrions pouvoir changer le fonctionnement interne sans casser les tests unitaires.

Mais comme la classe utilise le IPersonRepositoryet un IEmailRepository, JE DOIS spécifier (mock / dummy) les résultats de certaines méthodes pour le IPersonRepository. La classe principale calcule certaines données sur la base des données existantes et les renvoie. Si je veux tester cela, je ne vois pas comment je peux écrire un test sans spécifier que le IPersonRepository.GetSavingsByCustomerIdretourne x. Mais ensuite, mon test unitaire «connaît» le fonctionnement interne, car il «sait» quelles méthodes se moquer et lesquelles ne le sont pas.

Comment puis-je tester une classe qui a injecté des dépendances, sans que le test connaisse les internes?

Contexte:

D'après mon expérience, de nombreux tests comme celui-ci créent des simulations pour les référentiels, puis fournissent les bonnes données pour les simulations ou testent si une méthode spécifique a été appelée pendant l'exécution. Quoi qu'il en soit, le test connaît les éléments internes.

Maintenant, j'ai vu une présentation sur la théorie (que j'ai entendue auparavant) selon laquelle le test ne devrait pas connaître l'implémentation. D'abord parce que vous ne testez pas comment cela fonctionne, mais aussi parce que lorsque vous modifiez maintenant l'implémentation, tous les tests unitaires échouent parce qu'ils «connaissent» l'implémentation. Bien que j'aime que le concept des tests ne soit pas au courant de l'implémentation, je ne sais pas comment l'accomplir.


1
Dès que votre classe principale attend un IPersonRepositoryobjet, cette interface et toutes les méthodes qu'elle décrit ne sont plus "internes", donc ce n'est pas vraiment un problème de test. Votre vraie question devrait être "comment puis-je refactoriser des classes en unités plus petites sans trop exposer en public". La réponse est "garder ces interfaces allégées" (en respectant le principe de séparation des interfaces, par exemple). C'est le point 2 à mon humble avis dans la réponse de @ DavidArno (je suppose qu'il n'est pas nécessaire que je répète cela dans une autre réponse).
Doc Brown

Réponses:


7

La classe principale calcule certaines données sur la base des données existantes et les renvoie. Si je veux tester cela, je ne vois pas comment je peux écrire un test sans spécifier que le IPersonRepository.GetSavingsByCustomerIdretourne x. Mais ensuite, mon test unitaire «connaît» le fonctionnement interne, car il «sait» quelles méthodes se moquer et lesquelles ne le sont pas.

Vous avez raison de dire qu'il s'agit d'une violation du principe «ne testez pas les composants internes» et qu'il est courant que les gens l'ignorent.

Comment puis-je tester une classe qui a injecté des dépendances, sans que le test connaisse les internes?

Il existe deux solutions que vous pouvez adopter pour contourner cette violation:

1) Fournissez une maquette complète de IPersonRepository. Votre approche actuellement décrite consiste à coupler la simulation au fonctionnement interne de la méthode testée en se moquant uniquement des méthodes qu'elle appellera. Si vous fournissez des maquettes pour toutes les méthodes de IPersonRepository, vous supprimez ce couplage. Le fonctionnement interne peut changer sans affecter la maquette, ce qui rend le test moins fragile.

Cette approche a l'avantage de garder le mécanisme DI simple, mais elle peut créer beaucoup de travail si votre interface définit de nombreuses méthodes.

2) Ne pas injecter IPersonRepository, injecter la GetSavingsByCustomerIdméthode ou injecter une valeur d'épargne. Le problème avec l'injection d'implémentations d'interface entières est que vous injectez ensuite ("dites, ne demandez pas") un système "demandez, ne dites pas", mélangeant les deux approches. Si vous adoptez une approche "DI pur", la méthode doit être indiquée (dire) la méthode exacte à appeler si elle veut une valeur d'épargne, plutôt que de se voir attribuer un objet (qu'elle doit ensuite demander efficacement la méthode à appeler).

L'avantage de cette approche est qu'elle évite le besoin de simulations (au-delà des méthodes de test que vous injectez dans la méthode testée). L'inconvénient est que cela peut entraîner la modification des signatures de méthode en réponse aux exigences de changement de méthode.

Les deux approches ont leurs avantages et leurs inconvénients, alors choisissez celle qui convient le mieux à vos besoins.


1
J'aime vraiment l'idée numéro 2. Je ne sais pas pourquoi je n'y avais pas pensé auparavant. Je crée des dépendances sur les IRepositories depuis quelques années maintenant sans jamais me demander pourquoi j'ai créé une dépendance sur un IRepository entier (qui dans de nombreux cas contiendra beaucoup de signatures de méthode) alors qu'il n'a qu'une dépendance sur une ou 2 méthodes. Donc, au lieu d'injecter un IRepository, je pourrais mieux injecter ou transmettre la (seule) signature de méthode nécessaire.
Michel

1

Mon approche consiste à créer des versions «fictives» des référentiels qui lisent à partir de fichiers simples contenant les données nécessaires.

Cela signifie que le test individuel ne «connaît» pas la configuration de la maquette, bien que le projet de test global fasse évidemment référence à la maquette et contienne les fichiers de configuration, etc.

Cela évite la configuration complexe des objets fantômes requis par les frameworks de simulation et vous permet d'utiliser les objets «simulés» dans des instances réelles de votre application pour les tests d'interface utilisateur et similaires.

Étant donné que la «maquette» est entièrement implémentée, plutôt que d'être spécifiquement configurée pour votre scénario de test, un changement d'implémentation, par exemple, permet de dire que GetSavingsForCustomer devrait désormais également supprimer le client. Ne cassera pas les tests (à moins bien sûr que cela casse vraiment les tests), vous n'avez qu'à mettre à jour votre implémentation simulée unique et tous vos tests s'exécuteront sans changer leur configuration


2
Cela conduit toujours à la situation où je crée une maquette avec des données pour exactement les méthodes avec lesquelles la classe testée travaille. J'ai donc toujours le problème que lorsque l'implémentation est modifiée, le test se cassera.
Michel

1
édité pour expliquer pourquoi mon approche est meilleure, bien que toujours pas parfaite pour le scénario, je pense que vous voulez dire
Ewan

1

Les tests unitaires sont généralement des tests en boîte blanche (vous avez accès au vrai code). Par conséquent, il est correct de connaître les internes dans une certaine mesure, mais pour les débutants, il est plus facile de ne pas le faire, car vous ne devriez pas tester le comportement interne (tel que "appeler la méthode a en premier, puis b puis a nouveau").

Injecter une maquette qui fournit des données est correct, car votre classe (unité) dépend de ces données externes (ou fournisseur de données). Cependant, vous devez vérifier les résultats (pas le moyen de les atteindre)! Par exemple, vous fournissez une instance Personne et vérifiez qu'un e-mail a été envoyé à la bonne adresse e-mail. Par exemple, en fournissant une maquette pour l'e-mail également, cette maquette ne fait que stocker l'adresse e-mail du destinataire pour un accès ultérieur par votre test. -code. (Je pense que Martin Fowler les appelle Stubs plutôt que Mocks, cependant)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.