web-dev-qa-db-fra.com

Valeur de l'utilisation de méthodes privées par rapport à tous les publics dans les classes pour les logiciels internes pour faciliter les tests unitaires

Voici un squelette d'une classe que j'ai construite qui parcourt et déduplique les données - c'est en C # mais les principes de la question ne sont pas spécifiques au langage.

public static void DedupeFile(FileContents fc)
{
    BuildNameKeys(fc);
    SetExactDuplicates(fc);
    FuzzyMatching(fc);
}

// algorithm to calculate fuzzy similarity between surname strings
public static bool SurnameMatch(string surname1, string surname2)

// algorithm to calculate fuzzy similarity between forename strings
public static bool ForenameMatch(string forename1, string forename2)

// algorithm to calculate fuzzy similarity between title strings
public static bool TitleMatch(string title1, string title2)

// used by above fn to recognise that "Mr" isn't the same as "Ms" etc
public static bool MrAndMrs(string title1, string title2)

// gives each row a unique key based on name
public static void BuildNameKeys(FileContents fc)

// loops round data to find exact duplicates 
public static void SetExactDuplicates(FileContents fc)

// threads looping round file to find fuzzy duplicates
public static void FuzzyMatching(FileContents fc, int maxParallels = 32)

Maintenant, en utilisation réelle, seule la première fonction doit être publique. Tout le reste n'est utilisé qu'à l'intérieur de cette classe et nulle part ailleurs.

Strictement, cela signifie qu'ils doivent bien sûr être privés. Cependant, je les ai laissés publics pour faciliter les tests unitaires. Certaines personnes me diront sans doute que je devrais les tester via l'interface publique, mais c'est en partie pourquoi j'ai choisi cette classe: c'est un excellent exemple de l'endroit où cette approche devient maladroite. Les fonctions d'appariement flou sont d'excellents candidats pour les tests unitaires, et pourtant un test sur cette fonction "publique" unique serait presque inutile.

Cette classe ne sera jamais utilisée en dehors d'une petite équipe dans ce bureau, et je ne pense pas que la compréhension structurelle apportée en rendant les autres méthodes privées vaille la peine supplémentaire de compresser mes tests avec du code pour accéder directement aux méthodes privées.

Cette approche "tout public" est-elle raisonnable pour les cours de logiciels internes? Ou existe-t-il une meilleure approche?

Je suis conscient qu'il y a déjà une question sur Comment testez-vous les méthodes privées? , mais cette question est de savoir s'il existe des scénarios où il vaut la peine de contourner ces techniques au profit de simplement laisser les méthodes publiques.

EDIT: Pour ceux qui sont intéressés, j'ai ajouté le code complet sur CodeReviewSE car la restructuration de cette classe semblait une trop belle opportunité d'apprentissage à manquer.

30
Bob Tway

Je les ai laissés publics pour faciliter les tests unitaires

Facilité de écriture ces tests, peut-être. Mais vous associez alors étroitement cette classe à un tas de tests qui interagissent avec son fonctionnement interne. Cela se traduit par des tests fragiles: ils se cassent probablement dès que vous apportez des modifications au code. Cela crée un véritable casse-tête de maintenance, qui entraîne souvent la suppression des tests par les gens, car ils deviennent plus problématiques qu'ils n'en valent la peine.

N'oubliez pas que les tests unitaires ne signifient pas "tester le plus petit morceau de code possible". C'est un test d'une unité fonctionnelle, c'est-à-dire pour un ensemble d'entrées dans une partie du système, je m'attends à ces résultats. Cette unité peut être une méthode statique, une classe ou un groupe de classes au sein d'un assembly. En ciblant uniquement les API publiques, vous imitez le comportement du système et vos tests deviennent ainsi moins couplés et plus robustes.

Donc, rendez les méthodes privées, moquez le "DTO" FileContents "entier" et testez uniquement la seule vraie méthode publique. Ce sera plus de travail au début, mais avec le temps, vous récolterez les avantages de créer des tests utiles comme celui-ci.

64
David Arno

Je m'attendrais normalement à ce que vous exerciez les fonctions de membre privé via l'interface publique. Dans ce cas, j'écrirais différents tests pour alimenter différents contextes de fichiers, avec différents ensembles de données présents afin d'exercer ces méthodes.

Je ne pense pas que vos tests devraient connaître ces méthodes privées. Je pense qu'ils font partie de l'implémentation, et votre test devrait porter sur le fonctionnement de votre composant, et non sur la façon dont il a été implémenté.

Ce cours ne sera jamais utilisé en dehors d'une petite équipe dans ce bureau

Vous pourriez être surpris de ce qui est réutilisé dès que vous le rendez public. Une alternative consiste à accepter que le rendre public entraînera la réutilisation, puis retirer les méthodes dans une classe d'utilité générale. De cette façon, vous pouvez tester ces méthodes séparément (puisque vous les avez publiées) et comme il s'agit d'une classe d'utilité publique, vous ne faites aucune supposition implicite qu'elles ne seront utilisées que dans le scénario this vous suis actuellement concentré sur.

37
Brian Agnew

Je pense que le problème vient de la conception. Mon intuition dit que vous avez écrit les tests après le code, ou que vous aviez déjà une implémentation complète à l'esprit lorsque vous avez commencé à écrire les tests, puis que vous avez simplement corrigé les tests pour les adapter à la conception.

Je pense que ce type de problème peut être évité en se souvenant du court cycle TDD consistant à faire une petite assertion, puis à la faire passer de la manière la plus simple possible.

Vous dites qu'il est difficile d'exercer toutes les méthodes privées via des méthodes publiques. Cela indique probablement que votre classe en fait trop. Si vous ne pouvez pas le construire en ajoutant test après test et en voyant simplement les exigences remplies, puis en le refactorisant en code maintenable et lisible, alors vous n'avez pas assez de connaissances sur l'entité pour l'implémenter, ou c'est trop complexe ( ouais ouais, c'est une généralisation, je sais qu'ils sont mauvais).

En regardant les noms de vos méthodes, il me semble que cette classe a beaucoup trop de responsabilités. Il a plusieurs implémentations d'algorithmes, il gère les threads, il peut même lire des fichiers sur le disque. Divisez votre travail en morceaux réutilisables gérables. Les algorithmes de correspondance/validation peuvent facilement être injectés (en tant que stratégies, plus préférablement imo, en tant que délégués), la gestion des threads devrait probablement se produire à un niveau supérieur, etc.

Lorsque vous n'avez plus une grande classe compliquée avec des milliards de responsabilités (enfin, plus ou moins), les tests deviennent presque triviaux.

9
sara

Je suis d'accord avec @BrianAgnew et @kai, mais j'aimerais ajouter plus qu'un commentaire.

Alors qu'un IDedupeFiler (ou autre) doit être testé via son interface publique, l'OP a décidé qu'il est utile de tester les sous-routines individuelles. Indépendamment de la taille du fichier ou du nombre de lignes (qui n'est qu'un nombre approximatif de proxy pour les responsabilités de classe), l'OP a décidé qu'il y a trop de complexité à tester depuis le haut de cette classe.

C'est une bonne chose d'ailleurs, l'une des raisons pour lesquelles des gens comme TDD est que la nécessité de tester oblige le codeur à adapter (et améliorer) leur conception. Il est valable de souligner que les premiers tests sont écrits au plus tôt ce processus se produit, mais le PO n'est pas du tout sur la voie des décisions de conception non testables et le refactoring ne sera pas onéreux.

La question semble être de savoir si l'OP devrait (1) rendre les méthodes publiques et atteindre la testabilité avec moins de refactoring, ou s'il devrait faire autre chose. Je suis d'accord avec @kai et je dis que l'OP devrait (2) divisez cette classe en morceaux isolés, testables séparément.

(1) rompt l'encapsulation et rend moins immédiatement évidente l'utilisation et l'interface publique de la classe. Je pense que l'OP reconnaît que ce n'est pas le meilleur choix de conception OOP dans leur question. (2) signifie plus de classes, mais je ne pense pas que ce soit vraiment un problème, et cela fournit testabilité sans compromis de conception.

Si vous n'êtes vraiment pas d'accord avec moi sur le fait que vos sous-méthodes représentent des préoccupations distinctes et testables séparément, alors n'essayez pas de creuser dans la classe pour les tester. Exercez-les à travers votre meilleure méthode publique. La difficulté avec laquelle vous le trouverez sera un bon indicateur de la pertinence de ce choix.

6
Nathan Cooper

Je pense que vous pourriez bénéficier d'un léger changement dans la façon dont vous voyez les tests unitaires. Au lieu de les considérer comme un moyen de garantir que tous votre code fonctionne, pensez-y comme un moyen de garantir que votre interface publique fait ce que vous prétendez qu'elle fait.

En d'autres termes, ne vous inquiétez pas du tout de tester les composants internes - écrivez des tests unitaires qui prouvent que lorsque vous donnez vos entrées de classe X, vous obtenez des sorties Y - c'est tout. La façon dont il y parvient n'a aucune importance. En fait, les méthodes privées pourraient être totalement erronées, inutiles, redondantes, etc., et tant que l'interface publique fait ce qu'elle est censée faire, du point de vue du test unitaire, elle fonctionne.

C'est important parce que vous voulez pouvoir revenir en arrière et refactoriser ce code plus tard quand une nouvelle bibliothèque sort mieux qui le fait, ou vous vous rendez compte que vous faisiez un travail inutile, ou vous décidez simplement que la façon dont les choses sont nommées et organisées pourrait être plus clair. Si vos tests ne concernent que l'interface publique, vous êtes libre de le faire sans vous soucier de casser les choses.

Si vous modifiez votre façon de penser les tests unitaires et constatez qu'il est toujours difficile d'écrire le test pour une classe donnée, alors c'est un bon signe que votre conception a besoin de quelques améliorations. Peut-être que la classe devrait essayer d'en faire moins - peut-être que certaines de ces méthodes privées appartiennent vraiment à une nouvelle classe plus petite. Ou peut-être devez-vous utiliser l'injection de dépendances pour gérer les dépendances externes.

Dans ce cas, sans en savoir plus sur la façon dont vous effectuez cette déduplication, je suppose que les méthodes que vous souhaitez rendre publiques pourraient en fait être mieux en tant que classes distinctes ou en tant que méthodes publiques dans une bibliothèque d'utilitaires. De cette façon, vous pouvez définir l'interface pour chacun ainsi que sa gamme d'entrées autorisées et de sorties attendues, et les tester indépendamment du script de déduplication lui-même.

3
thomij

En plus des autres réponses, il y a un autre avantage à rendre ces fonctions privées et à les tester via l'interface publique.

Si vous collectez des mesures de couverture de code sur votre code, il devient plus facile de savoir quand une fonction n'est plus utilisée. Mais, si vous rendez toutes ces fonctions publiques et que vous leur faites des tests unitaires, ils auront toujours au moins le test unitaire les appelant même lorsque rien d'autre ne le fait.

Considérez cet exemple:

public void foo() {
    bar();
    baz();
}

public void bar() { ... }

public void baz() { ... }

Ensuite, vous effectuez des tests unitaires individuels pour foo, bar et baz. Ils seront donc tous appelés par votre framework de test unitaire, et ils auront tous une couverture de code indiquant qu'ils sont utilisés.

Considérez maintenant ce changement:

public void foo() {
    bar();
}

public void bar() { ... }

public void baz() { ... }

Étant donné que vos tests unitaires appellent toujours toutes ces fonctions, votre couverture de code va dire qu'elles sont toutes utilisées. Mais baz n'est plus appelé par aucune autre partie de votre logiciel que les tests unitaires. C'est effectivement du code mort.

L'aviez-vous écrit comme ceci:

public void foo() {
    bar();
}

private void bar() { ... }

private void baz() { ... }

Ensuite, vous perdriez 100% de votre couverture de code de baz lorsque foo a changé, et c'est votre signal pour le supprimer du code. OU, cela soulève un drapeau rouge parce que baz est en fait une opération super importante, et son omission provoque d'autres problèmes (espérons que si c'est le cas, alors d'autres tests unitaires l'attraperaient, mais peut-être pas).

3
Shaz

Je séparerais la fonction et les cas de test en classes séparées. L'un contient la fonction, l'autre contient les cas de test qui appelle la fonction et affirme si le résultat est égal à ce que vous attendez. Je ne suis pas en C mais en Java J'utiliserais Junit pour cela. Cela divise la logique et rend votre classe plus lisible. Vous n'avez pas non plus à vous soucier de votre problème.

0
bish