Parfois, les objets doivent simplement être étroitement couplés. Par exemple, une CsvFile
classe devra probablement travailler étroitement avec la CsvRecord
classe (ou l' ICsvRecord
interface).
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 ICsvRecord
faux 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 CsvRecord
classe peut devenir un peu velu. Ce qui m'amène à l'une des deux conclusions:
- Il est difficile d'écrire des tests unitaires! C'est une odeur de code! Refactor!
- Se moquer de chaque dépendance est tout simplement déraisonnable.
Lorsque j'ai remplacé mes simulacres par des CsvRecord
instances 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?
Réponses:
Si vous avez vraiment besoin d'une coordination entre ces deux classes, écrivez une
CsvCoordinator
classe qui encapsule vos deux classes et testez cela.Cependant, je conteste la notion qui
CsvRecord
n'est pas testable indépendamment.CsvRecord
est fondamentalement une classe DTO , n'est-ce pas? C'est juste une collection de champs, avec peut-être quelques méthodes d'assistance. EtCsvRecord
peut être utilisé dans d'autres contextes en plusCsvFile
; vous pouvez avoir une collection ou un tableau deCsvRecord
s, par exemple.Testez d'
CsvRecord
abord. Assurez-vous qu'il passe tous ses tests. Ensuite, allez-y et utilisez-leCsvRecord
avec votreCsvFile
classe pendant le test. Utilisez-le comme talon / maquette pré-testé; remplissez-le avec les données de test pertinentes, transmettez-le àCsvFile
et écrivez vos cas de test par rapport à cela.la source
CsvRecord
casse, alors évidemmentCsvData
échoue; mais c'est OK, parce que vous testez d'CsvRecord
abord, et si cela échoue, vosCsvFile
tests n'ont aucun sens. Vous pouvez toujours distinguer les erreurs dansCsvRecord
et dansCsvFile
.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
CsvRecord
me 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éeCsvRecord
qui fait de nombreux calculs complexes.Mais s'il
CsvRecord
n'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.
la source
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.
la source
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.
la source
CsvFile
est étroitement couplé àCsvRecord
(mais pas l'inverse). L'OP demande si c'est une bonne idée de testerCsvFile
en le découplant deCsvRecord
via unICsvRecord
, et non l'inverse.CsvFile
dépend de la façon dont cela dépend du fonctionnement interne deCsvRecord
, 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érenteCsvFile
. Présenter l'interface juste pour que vous puissiez dire que vous avez un couplage réduit est idiot.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
CsvRecord
une 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.la source