Quand utiliser les interfaces (tests unitaires, IoC?)


17

Je soupçonne que j'ai fait une erreur d'écolier ici, et je cherche des éclaircissements. Beaucoup de classes dans ma solution (C #) - j'ose dire la majorité - j'ai fini par écrire une interface correspondante pour. Par exemple, une interface "ICalculator" et une classe "Calculator" qui l'implémente, même si je ne remplacerai probablement jamais cette calculatrice par une implémentation différente. De plus, la plupart de ces classes résident dans le même projet que leurs dépendances - elles n'ont vraiment besoin d'être internal, mais ont fini par être publicun effet secondaire de la mise en œuvre de leurs interfaces respectives.

Je pense que cette pratique de créer des interfaces pour tout découle de quelques mensonges: -

1) Je pensais à l'origine qu'une interface était nécessaire pour créer des simulations de test unitaire (j'utilise Moq), mais j'ai depuis découvert qu'une classe peut être moquée si ses membres le sont virtual, et elle a un constructeur sans paramètre (corrigez-moi si J'ai tort).

2) Je pensais à l'origine qu'une interface était nécessaire pour enregistrer une classe avec le framework IoC (Castle Windsor), par exemple

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

alors qu'en fait je pouvais simplement enregistrer le type concret contre lui-même:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) L'utilisation d'interfaces, par exemple les paramètres du constructeur pour l'injection de dépendances, entraîne un "couplage lâche".

Alors suis-je devenu fou avec les interfaces?! Je connais les scénarios où vous utiliseriez "normalement" une interface, par exemple en exposant une API publique, ou pour des choses comme la fonctionnalité "enfichable". Ma solution a un petit nombre de classes qui correspondent à de tels cas d'utilisation, mais je me demande si toutes les autres interfaces ne sont pas nécessaires et devraient être supprimées? En ce qui concerne le point 3) ci-dessus, ne violerai-je pas le "couplage lâche" si je le faisais?

Edit : - Je suis juste en train de jouer avec Moq, et il semble que les méthodes soient publiques et virtuelles, et aient un constructeur public sans paramètre, pour pouvoir se moquer d'eux. Il semble donc que je ne puisse pas avoir de classes internes alors?


Je suis presque sûr que C # ne vous oblige pas à rendre une classe publique afin d'implémenter une interface, même si l'interface est publique ...
vaughandroid

1
Vous n'êtes pas censé tester des membres privés . Cela peut être considéré comme un indicateur de l'anti-modèle de la classe divine. Si vous ressentez le besoin de tester les fonctionnalités privées de manière unitaire, c'est généralement une bonne idée d'encapsuler cette fonctionnalité dans sa propre classe.
Spoike

@Spoike, le projet en question est un projet d'interface utilisateur, où il semblait naturel de rendre toutes les classes internes, mais elles ont encore besoin de tests unitaires? J'ai lu ailleurs sur le fait de ne pas avoir besoin de tester les méthodes internes, mais je n'ai jamais compris les raisons.
Andrew Stephens

Demandez-vous d'abord quelle est l'unité testée. Est-ce la classe entière ou une méthode? Cette unité doit être publique afin d'être testée correctement. Dans les projets d'interface utilisateur, je sépare généralement la logique testable en contrôleurs / modèles de vue / services qui ont tous des méthodes publiques. Les vues / widgets réels sont câblés aux contrôleurs / modèles de vue / services et testés par des tests de fumée réguliers (c'est-à-dire démarrer l'application et commencer à cliquer). Vous pouvez avoir des scénarios qui devraient tester la fonctionnalité sous-jacente et avoir des tests unitaires sur les widgets conduit à des tests fragiles et doit être fait avec soin.
Spoike

@Spoike, rendez-vous donc vos méthodes VM publiques juste pour les rendre accessibles à nos tests unitaires? C'est peut-être là que je me trompe, essayant de faire en sorte que tout soit interne. Je pense que mes mains pourraient être liées avec Moq, ce qui semble exiger que les classes (pas seulement les méthodes) soient publiques afin de se moquer d'eux (voir mon commentaire sur la réponse de Telastyn).
Andrew Stephens

Réponses:


7

En général, si vous créez une interface qui n'a qu'un seul implémenteur, vous écrivez simplement deux fois et perdez votre temps. Les interfaces à elles seules ne fournissent pas de couplage lâche si elles sont étroitement couplées à une seule implémentation ... code.

Je considérerais cela comme une odeur de code si presque toutes vos classes ont des interfaces. Cela signifie qu'ils travaillent presque tous ensemble d'une manière ou d'une autre. Je soupçonnerais que les classes en font trop (car il n'y a pas de classes auxiliaires) ou que vous en avez trop abstrait (oh, je veux une interface de fournisseur pour la gravité car cela pourrait changer!).


1
Merci pour la réponse. Comme mentionné, il y a quelques raisons erronées pour lesquelles j'ai fait ce que j'ai fait, même si je savais que la plupart des interfaces n'auraient jamais plus d'une implémentation. Une partie du problème est que j'utilise le framework Moq qui est idéal pour se moquer des interfaces, mais pour se moquer d'une classe, elle doit être publique, avoir un ctr public sans paramètre et les méthodes à moquer doivent être `` publiques virtuelles '').
Andrew Stephens

La plupart des classes pour lesquelles je teste les unités sont internes, et je ne veux pas les rendre publiques juste pour les rendre testables (bien que vous puissiez dire, c'est pour cela que j'ai écrit toutes ces interfaces). Si je me débarrasse de mes interfaces, je devrai probablement trouver un nouveau cadre de simulation!
Andrew Stephens

1
@AndrewStephens - tout le monde n'a pas besoin d'être moqué. Si les classes sont suffisamment solides (ou étroitement couplées) pour ne pas avoir besoin d'interfaces, elles sont suffisamment solides pour être utilisées telles quelles dans les tests unitaires. Le temps / la complexité qui les déplace ne vaut pas les avantages des tests.
Telastyn

-1, car souvent vous ne savez pas combien d'implémenteurs vous avez besoin lorsque vous écrivez le code pour la première fois. Si vous utilisez une interface depuis le début, vous n'avez pas à changer de client lorsque vous écrivez des implémenteurs supplémentaires.
Wilbert

1
@wilbert - bien sûr, une petite interface est plus lisible que la classe, mais vous ne choisissez pas l'une ou l'autre, vous avez soit la classe, soit une classe et une interface. Et vous lisez peut-être trop ma réponse. Je ne dis pas de ne jamais créer d'interfaces pour une seule implémentation - la plupart devraient en fait, car la plupart des interactions devraient nécessiter cette flexibilité. Mais relisez la question. Faire une interface pour tout, c'est juste cultiver le fret. L'IoC n'est pas une raison. Les moqueries ne sont pas une raison. Les exigences sont une raison pour mettre en œuvre cette flexibilité.
Telastyn

4

1) Même si les classes concrètes sont moquables, cela nécessite toujours de créer des membres virtualet de fournir un constructeur sans paramètre, qui peut être envahissant et indésirable. Vous (ou les nouveaux membres de votre équipe) allez bientôt vous retrouver à ajouter systématiquement virtualdes constructeurs sans paramètres dans chaque nouvelle classe sans arrière-pensée, simplement parce que "c'est comme ça que ça marche".

Une interface est IMO une bien meilleure idée lorsque vous devez vous moquer d'une dépendance, car elle vous permet de différer la mise en œuvre jusqu'à ce que vous en ayez vraiment besoin, et permet un contrat clair et clair de la dépendance.

3) Comment est-ce un mensonge?

Je pense que les interfaces sont idéales pour définir des protocoles de messagerie entre des objets collaborateurs. Il peut y avoir des cas où leur besoin est discutable, comme avec les entités de domaine par exemple. Cependant, partout où vous avez un partenaire stable (c'est-à-dire une dépendance injectée par opposition à une référence transitoire) avec laquelle vous devez communiquer, vous devriez généralement envisager d'utiliser une interface.


3

L'idée de l'IoC est de rendre les types de béton remplaçables. Donc, même si (pour le moment) vous n'avez qu'une seule calculatrice qui implémente ICalculator, une deuxième implémentation pourrait dépendre uniquement de l'interface et non des détails d'implémentation de Calculator.

Par conséquent, la première version de votre enregistrement IoC-Container est la bonne et celle que vous devriez utiliser à l'avenir.

Si vous rendez vos classes publiques ou internes, ce n'est pas vraiment lié, et le moq-ing non plus.


2

Je serais vraiment plus inquiet du fait que vous semblez ressentir le besoin de "se moquer" de presque tous les types de votre projet.

Les doublons de test sont principalement utiles pour isoler votre code du monde extérieur (bibliothèques tierces, E / S disque, E / S réseau, etc.) ou de calculs très coûteux. Être trop libéral avec votre framework d'isolation (en avez-vous vraiment VRAIMENT besoin? Votre projet est-il si complexe?) Peut facilement conduire à des tests couplés à l'implémentation, et cela rend votre conception plus rigide. Si vous écrivez un tas de tests qui vérifient qu'une classe a appelé la méthode X et Y dans cet ordre, puis que vous avez passé les paramètres a, b et c à la méthode Z, obtenez-vous VRAIMENT de la valeur? Parfois, vous obtenez des tests comme celui-ci lorsque vous testez la journalisation ou l'interaction avec des bibliothèques tierces, mais cela devrait être l'exception, pas la règle.

Dès que votre framework d'isolement vous dit que vous devez rendre les choses publiques et que vous devez toujours avoir des constructeurs sans paramètres, il est temps de vous débarrasser de l'esquive, car à ce stade, votre framework vous force à écrire du mauvais (pire) code.

En parlant plus spécifiquement des interfaces, je trouve qu'elles sont utiles dans quelques cas clés:

  • Lorsque vous devez répartir des appels de méthode vers différents types, par exemple lorsque vous avez plusieurs implémentations (c'est la plus évidente, car la définition d'une interface dans la plupart des langages n'est qu'une collection de méthodes virtuelles)

  • Lorsque vous devez pouvoir isoler le reste de votre code d'une classe. Il s'agit de la manière normale de soustraire le système de fichiers et l'accès au réseau et la base de données et ainsi de suite de la logique de base de votre application.

  • Lorsque vous avez besoin d'une inversion de contrôle pour protéger les limites des applications. C'est l'un des moyens les plus puissants pour gérer les dépendances. Nous savons tous que les modules de haut niveau et les modules de bas niveau ne devraient pas se connaître, car ils changeront pour différentes raisons et vous NE VOULEZ PAS avoir de dépendances sur HttpContext dans votre modèle de domaine ou sur un cadre de rendu Pdf dans votre bibliothèque de multiplication matricielle . Faire en sorte que vos classes de limites implémentent des interfaces (même si elles ne sont jamais remplacées) aide à desserrer le couplage entre les couches et réduit considérablement le risque de dépendances s'infiltrant à travers les couches des types connaissant trop d'autres types.


1

Je penche pour l'idée que, si une logique est privée, la mise en œuvre de cette logique ne devrait préoccuper personne, et en tant que telle ne devrait pas être testée. Si une logique est suffisamment complexe pour que vous vouliez la tester, cette logique a probablement un rôle distinct et devrait donc être publique. Par exemple, si j'extrais du code dans une méthode d'assistance privée, je trouve que c'est généralement quelque chose qui pourrait tout aussi bien se trouver dans une classe publique distincte (un formateur, un analyseur, un mappeur, peu importe).
En ce qui concerne les interfaces, je laisse le besoin d'avoir à écraser ou à me moquer de la classe me conduire. Des trucs comme des repos, je veux généralement pouvoir piquer ou me moquer. Un mappeur dans un test d'intégration, (généralement) pas tellement.

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.