TDD: se moquer des objets étroitement couplés

10

Parfois, les objets doivent simplement être étroitement couplés. Par exemple, une CsvFileclasse devra probablement travailler étroitement avec la CsvRecordclasse (ou l' ICsvRecordinterface).

Cependant, d'après ce que j'ai appris dans le passé, l'un des principes fondamentaux du développement piloté par les tests est «Ne testez jamais plus d'une classe à la fois». Cela signifie que vous devez utiliser des ICsvRecordfaux ou des talons plutôt que des instances réelles de CsvRecord.

Cependant, après avoir essayé cette approche, j'ai remarqué que se moquer de la CsvRecordclasse peut devenir un peu velu. Ce qui m'amène à l'une des deux conclusions:

  1. Il est difficile d'écrire des tests unitaires! C'est une odeur de code! Refactor!
  2. Se moquer de chaque dépendance est tout simplement déraisonnable.

Lorsque j'ai remplacé mes simulacres par des CsvRecordinstances réelles , les choses se sont beaucoup mieux déroulées. En cherchant les pensées des autres, je suis tombé sur ce billet de blog , qui semble soutenir le n ° 2 ci-dessus. Pour les objets qui sont naturellement étroitement couplés, nous ne devrions pas nous soucier autant de la moquerie.

Suis-je loin de la piste? Y a-t-il des inconvénients à l'hypothèse n ° 2 ci-dessus? Dois-je réellement penser à refactoriser mon design?

Phil
la source
1
Je pense que c'est une idée fausse que «l'unité» dans les «tests unitaires» doit nécessairement être une classe. Je pense que votre exemple montre un cas où il peut être préférable que ces deux classes forment une unité. Mais ne vous méprenez pas, je suis totalement d'accord avec la réponse de Robert Harvey.
Doc Brown

Réponses:

11

Si vous avez vraiment besoin d'une coordination entre ces deux classes, écrivez une CsvCoordinatorclasse qui encapsule vos deux classes et testez cela.

Cependant, je conteste la notion qui CsvRecordn'est pas testable indépendamment. CsvRecordest fondamentalement une classe DTO , n'est-ce pas? C'est juste une collection de champs, avec peut-être quelques méthodes d'assistance. Et CsvRecordpeut être utilisé dans d'autres contextes en plus CsvFile; vous pouvez avoir une collection ou un tableau de CsvRecords, par exemple.

Testez d' CsvRecordabord. Assurez-vous qu'il passe tous ses tests. Ensuite, allez-y et utilisez-le CsvRecordavec votre CsvFileclasse pendant le test. Utilisez-le comme talon / maquette pré-testé; remplissez-le avec les données de test pertinentes, transmettez-le à CsvFileet écrivez vos cas de test par rapport à cela.

Robert Harvey
la source
1
Oui, CsvRecord est très certainement testable indépendamment. Le problème est que si quelque chose se casse dans CsvRecord, cela entraînera l'échec des tests CsvData. Mais je ne pense pas que ce soit un problème majeur.
Phil
1
Je pense que vous voulez que cela se produise. :)
Robert Harvey
1
@RobertHarvey: en théorie, cela pourrait devenir un problème si CsvRecord et CsvFile deviennent des classes assez complexes, et si un test échoue pour CsvFile, maintenant vous ne savez pas immédiatement s'il s'agit d'un problème dans CsvFile ou CsvRecord. Mais je suppose que c'est plus un cas hypothétique - si j'avais la tâche de programmer de telles classes pour un programme du monde réel, je le ferais exactement comme vous le décrivez.
Doc Brown
2
@Phil: Si se CsvRecordcasse, alors évidemment CsvDataéchoue; mais c'est OK, parce que vous testez d' CsvRecordabord, et si cela échoue, vos CsvFiletests n'ont aucun sens. Vous pouvez toujours distinguer les erreurs dans CsvRecordet dans CsvFile.
tdammers
5

La raison de tester une classe à la fois est que vous ne voulez pas que les tests d'une classe aient des dépendances sur le comportement d'une deuxième classe. Cela signifie que si votre test pour la classe A exerce l'une des fonctionnalités de la classe B, vous devez vous moquer de la classe B pour supprimer la dépendance à une fonctionnalité particulière au sein de la classe B.

Une classe comme CsvRecordme semble que c'est principalement pour le stockage de données - ce n'est pas une classe avec trop de fonctionnalités qui lui est propre. Autrement dit, il peut avoir des constructeurs, des getters, des setters, mais pas de méthodes avec une réelle logique substantielle. Bien sûr, je suppose ici - peut-être que vous avez écrit une classe appelée CsvRecordqui fait de nombreux calculs complexes.

Mais s'il CsvRecordn'a pas de logique propre, il n'y a rien à gagner à s'en moquer. C'est vraiment juste la vieille maxime - "ne pas se moquer des objets de valeur" .

Ainsi, lorsque vous envisagez de vous moquer d'une classe particulière (pour un test d'une classe différente), vous devez prendre en compte la quantité de sa propre logique que cette classe possède et la quantité de cette logique qui sera exécutée au cours de votre test.

Dawood ibn Kareem
la source
+1. Tout test dont le résultat dépend de l'exactitude du comportement de plusieurs objets est un test d'intégration et non un test unitaire. Vous devez vous moquer de l'un de ces objets pour obtenir un véritable test unitaire. Cela ne s'applique cependant pas aux objets sans réel comportement - avec uniquement des getters et setters par exemple.
guillaume31
1

N ° 2 est très bien. Les choses peuvent être et devraient être étroitement liées si leurs concepts sont étroitement liés. Cela devrait être rare et généralement évité, mais dans l'exemple que vous avez fourni, cela a du sens.

Telastyn
la source
0

Les classes "couplées" sont mutuellement dépendantes les unes des autres. Cela ne devrait pas être le cas dans ce que vous décrivez - un CsvRecord ne devrait pas vraiment se soucier du CsvFile le contenant, donc la dépendance ne va que dans un sens. C'est bien, et ce n'est pas un couplage serré.

Après tout, si une classe contient le nom de chaîne variable, vous ne prétendriez pas qu'elle est étroitement couplée à String, n'est-ce pas?

Donc, testez le CsvRecord pour son comportement souhaité.

Ensuite, utilisez un cadre moqueur (Mockito est génial) pour tester si votre unité interagit correctement avec les objets dont elle dépend correctement. Le comportement que vous souhaitez tester, en réalité, est que CsvFile gère les CsvRcords de la manière attendue. Le fonctionnement interne de CvsRecord ne devrait pas avoir d'importance - c'est la façon dont CvsFile communique avec lui.

Enfin, TDD ne concerne pas uniquement les tests unitaires. Vous pouvez certainement (et devriez) commencer par des tests fonctionnels qui examinent le comportement fonctionnel du fonctionnement de vos composants plus volumineux, c'est-à-dire votre histoire utilisateur ou votre scénario. Vos tests unitaires fixent les attentes et vérifient les pièces, les tests fonctionnels font de même pour l'ensemble.

Matthew Flynn
la source
1
-1, un couplage étroit ne signifie pas nécessairement des dépendances cycliques, c'est une idée fausse. Dans l'exemple, CsvFile est étroitement couplé à CsvRecord(mais pas l'inverse). L'OP demande si c'est une bonne idée de tester CsvFileen le découplant de CsvRecordvia un ICsvRecord, et non l'inverse.
Doc Brown
2
@DocBrown: Le fait que le couplage soit serré ou non CsvFiledépend de la façon dont cela dépend du fonctionnement interne de CsvRecord, c'est-à-dire de la quantité d'hypothèses que le fichier a sur l'enregistrement. Les interfaces aident à documenter et à appliquer ces hypothèses (ou plutôt l'absence d'autres hypothèses), mais la quantité de couplage reste la même, sauf qu'avec une interface, vous pouvez connecter une classe d'enregistrement différente CsvFile. Présenter l'interface juste pour que vous puissiez dire que vous avez un couplage réduit est idiot.
tdammers
0

Il y a vraiment deux questions ici. Le premier est s'il existe des situations où se moquer d'un objet est déconseillé. C'est sans aucun doute vrai, comme le montrent les autres excellentes réponses. La deuxième question est de savoir si votre cas particulier fait partie de ces situations. Sur cette question, je ne suis pas convaincu.

La raison la plus courante pour ne pas se moquer d'une classe est probablement si c'est une classe de valeur. Cependant, vous devez regarder la raison derrière la règle. Ce n'est pas parce que la classe raillée sera mauvaise en quelque sorte, c'est parce qu'elle sera essentiellement identique à l'original. Si tel était le cas, votre test unitaire ne serait pas plus simple en utilisant la classe d'origine.

Il se peut très bien que votre code soit l'une des rares exceptions où le refactoring n'aiderait pas, mais vous ne devriez le déclarer tel qu'après que les efforts de refactoring diligents n'aient pas fonctionné. Même les développeurs chevronnés peuvent avoir du mal à trouver des alternatives à leur propre conception. Si vous ne pouvez penser à aucun moyen de l'améliorer, demandez à une personne expérimentée de lui donner un second regard.

La plupart des gens semblent supposer que vous êtes CsvRecordune classe de valeur. Essayez d'en faire un. Rendez-le immuable si vous le pouvez. Si vous avez deux objets avec des pointeurs l'un sur l'autre, supprimez l'un d'entre eux et découvrez comment le faire fonctionner. Recherchez des endroits pour diviser les classes et les fonctions. Le meilleur endroit pour diviser une classe ne correspond pas toujours à la disposition physique du fichier. Essayez d'inverser la relation parent / enfant des classes. Peut-être avez-vous besoin d'une classe distincte pour lire et écrire des fichiers csv. Peut-être avez-vous besoin de classes distinctes pour gérer les E / S de fichiers et l'interface avec les couches supérieures. Il y a beaucoup de choses à essayer avant de le déclarer non réfractaire.

Karl Bielefeldt
la source