J'ai une classe avec une méthode publique. Il a d'autres méthodes qui "aident" à l'objectif de la méthode publique. La méthode publique doit être testée. Cependant, je veux également tester de manière unitaire les méthodes privées.
Il serait impossible pour mes tests unitaires d'appeler des méthodes privées et de les tester isolément, elles doivent donc être publiques.
Le problème ici est que je me retrouve avec du code qui, je pense, a été écrit avec un manque de respect pour la convention OOP, uniquement pour pouvoir le tester à l'unité.
Ai-je raison de modifier l'accès des méthodes uniquement à la "testabilité"? Ou devrais-je repenser et refactoriser la structure dans son ensemble pour séparer la fonctionnalité des méthodes privées (malgré le fait qu'elles ne font pas grand-chose et peuvent parfois ressembler à de petites méthodes d'assistance)?
Oui, c'est une très mauvaise pratique - vous laissez vos outils prendre des décisions de conception pour vous.
Je pense que le principal problème ici est que vous essayez de traiter chaque méthode individuelle comme une unité. C'est généralement la cause de tous les problèmes de test unitaire. À l'exception de certains cas où votre méthode est très complexe et nécessite beaucoup de tests, vous devez traiter vos classes comme des unités. Martin Fowler traite même les classes connexes comme une unité (parfois).
Par conséquent, vous devez instancier une classe, puis appeler la ou les méthodes sur son interface publique telles qu'elles seraient utilisées. Cela vous donne vos exemples pour votre documentation et garantit qu'elle fonctionne comme le tout est prévu. Vous devriez essayer ici de tester les méthodes privées en appelant les méthodes publiques - et si une méthode privée n'est jamais appelée, alors pourquoi l'avez-vous toujours dans le code?
Si vous avez des méthodes d'assistance, il y a de fortes chances que votre seule et grande méthode en fasse trop. Le fractionnement en méthodes plus petites, puis le déplacement de ces méthodes dans des classes distinctes avec des interfaces publiques garde la classe avec la grande méthode responsable d'une seule chose et d'une seule chose (voir Principe de responsabilité unique). Ce passage aux classes séparées crée automatiquement une structure testable car vos méthodes doivent être rendues publiques.
n exemple
Prenez une classe BankAccount qui effectue des calculs budgétaires en consultant la liste des dépenses qu'elle contient. Une dépense a une catégorie (sports, alimentation, voiture, ...), un montant et une date. Vous pouvez avoir une méthode sur BankAccount appelée "CalculateExpenditureFor" prenant une date de début et de fin et une catégorie. Cette méthode filtrerait les dépenses pour n'avoir que des dépenses correspondant à la date et à la catégorie, puis procéderait à la somme des montants. Prenez note du nombre de "ands" dont j'ai besoin pour exprimer cette exigence! C'est une indication claire que votre méthode en fait trop.
Cette méthode fera en réalité deux choses: filtrer les dépenses et résumer les montants. Imaginez maintenant que votre BankAccount contient une classe ExpenditureLedger, qui contient les dépenses (un "grand livre" est un livre ou un fichier contenant une liste de transactions financières, d'où le nom). Vous pourriez alors avoir une méthode FilterExpenditures sur le ExpenditureLedger qui prend en charge le filtrage par date et catégorie. La liste des dépenses qui en résulte peut ensuite être utilisée par le compte bancaire pour comptabiliser les montants.
Dans ce scénario, vous disposez d'une méthode publique sur ExpenditureLedger qui peut être testée (vous alimentez les dépenses du grand livre et vérifiez le résultat). Vous pouvez également vérifier la méthode BankAccount en lui fournissant un ExpenditureLedger (de préférence simulé) afin que vous puissiez tester le code qui résume les montants retournés par ExpenditureLedger.
Rendre les méthodes publiques - oui, c'est une mauvaise pratique. Les rendre internes - cela dépend.
Au lieu de rendre publiques toutes les méthodes à tester et au lieu de repenser complètement vos classes, la solution la plus pragmatique est parfois de rendre les méthodes en jeu "internes" et d'utiliser l'attribut "InternalsVisibleTo" pour permettre à vos tests unitaires d'y accéder. Cela peut ne pas conduire à la conception idéale en premier lieu, mais parfois il suffit de faire avancer les choses, et tester le code "interne" est souvent mieux que de ne pas avoir de tests du tout ou d'introduire accidentellement de nouveaux bogues en refactorisant ces méthodes privées vers classes distinctes sans avoir écrit de tests unitaires auparavant.
Vous ne devriez pas avoir à modifier vos tests unitaires si vous avez uniquement modifié les détails de l'implémentation interne et non l'API. Le fait que vos anciens tests fonctionnent toujours après un changement est la preuve que vos changements n'ont pas cassé les choses. Vous pouvez consulter le journal de validation, voir que rien dans le code de test n'a changé et vous sentir rassuré (tant que vos tests ont été bons en premier lieu).
Maintenant, vous proposez une conception qui oblige à modifier vos tests lorsque vous modifiez l'implémentation. Vous détruisez cette confiance. Il y aura toujours un risque que des modifications aient été apportées aux véritables tests d'API publics au cours des tests des modifications d'implémentation.
Soit votre hiérarchie d'objet est incorrecte et ces méthodes doivent être publiques sur un autre objet, soit ce sont des détails d'implémentation de votre objet et vous perdez du temps à les tester (et vous risquez de fragiliser l'ensemble de votre conception). Testez le contrat.
Une question importante est "Pourquoi voulez-vous les tester?" Si ces méthodes sont au bon endroit et ne sont pas pertinentes pour le code externe, vous devez cesser d'être un monstre de contrôle, secouer votre TOC et faire confiance au système. Si ne pas les tester est vraiment un problème, votre hiérarchie d'objet actuelle (ou la conception de votre API) est incorrecte.