web-dev-qa-db-fra.com

Qu'y a-t-il de mal à faire d'un test unitaire un ami de la classe qu'il teste?

En C++, j'ai souvent fait d'une classe de test unitaire un ami de la classe que je teste. Je fais cela parce que je ressens parfois le besoin d'écrire un test unitaire pour une méthode privée, ou peut-être que je veux accéder à un membre privé afin que je puisse configurer plus facilement l'état de l'objet afin que je puisse le tester. Pour moi, cela aide à préserver l'encapsulation et l'abstraction car je ne modifie pas l'interface publique ou protégée de la classe.

Si j'achète une bibliothèque tierce, je ne voudrais pas que son interface publique soit polluée par un tas de méthodes publiques que je n'ai pas besoin de connaître simplement parce que le fournisseur voulait faire des tests unitaires!

Je ne veux pas non plus avoir à m'inquiéter d'un groupe de membres protégés dont je n'ai pas besoin de savoir si j'hérite d'une classe.

C'est pourquoi je dis qu'il préserve l'abstraction et l'encapsulation.

À mon nouveau travail, ils ne veulent pas utiliser de classes d'amis, même pour les tests unitaires. Ils disent parce que la classe ne doit rien "savoir" sur les tests et que vous ne voulez pas de couplage étroit entre la classe et son test.

Quelqu'un peut-il m'expliquer davantage ces raisons afin que je puisse mieux comprendre? Je ne vois tout simplement pas pourquoi utiliser un ami pour les tests unitaires est mauvais.

57
cchampion

Idéalement, vous ne devriez pas du tout avoir besoin de tester des méthodes privées. Tout un consommateur de votre classe devrait se soucier de l'interface publique, c'est donc ce que vous devez tester. Si une méthode privée a un bogue, elle devrait être interceptée par un test unitaire qui invoque une méthode publique sur la classe qui finit par appeler la méthode privée boguée. Si un bogue parvient à passer, cela indique que vos cas de test ne reflètent pas entièrement le contrat que vous souhaitez que votre classe implémente. La solution à ce problème est presque certainement de tester les méthodes publiques avec plus d'examen, de ne pas faire creuser vos cas de test dans les détails d'implémentation de la classe.

Encore une fois, c'est le cas idéal. Dans le monde réel, les choses peuvent ne pas toujours être aussi claires, et avoir une classe de test unitaire être un ami de la classe qu'elle teste peut être acceptable, voire souhaitable. Pourtant, ce n'est probablement pas quelque chose que vous voulez faire tout le temps. Si cela semble arriver assez souvent, cela pourrait indiquer que vos classes sont trop grandes et/ou exécutent trop de tâches. Si tel est le cas, les subdiviser davantage en refactorisant des ensembles complexes de méthodes privées en classes séparées devrait aider à supprimer le besoin de tests unitaires pour connaître les détails de l'implémentation.

57
bcat

Vous devriez considérer qu'il existe différents styles et méthodes à tester: Test de la boîte noire teste uniquement l'interface publique (en traitant la classe comme une boîte noire). Si vous avez une classe de base abstraite, vous pouvez même utiliser les mêmes tests sur toutes vos implémentations.

Si vous utilisez White box testing , vous pouvez même regarder les détails de l'implémentation. Non seulement sur les méthodes privées d'une classe, mais sur le type d'instructions conditionnelles incluses (c'est-à-dire si vous souhaitez augmenter la couverture de vos conditions car vous savez que les conditions étaient difficiles à coder). Dans les tests en boîte blanche, vous avez certainement un "couplage élevé" entre les classes/implémentation et les tests, ce qui est nécessaire car vous voulez tester l'implémentation et non l'interface.

Comme bcat l'a souligné, il est souvent utile d'utiliser la composition et plus de classes plus petites au lieu de nombreuses méthodes privées. Cela simplifie les tests en boîte blanche car vous pouvez spécifier plus facilement les cas de test pour obtenir une bonne couverture de test.

12
Philipp

Je pense que Bcat a donné une très bonne réponse, mais je voudrais exposer le cas exceptionnel auquel il fait allusion

Dans le monde réel, les choses peuvent ne pas toujours être aussi claires, et avoir une classe de test unitaire être un ami de la classe qu'elle teste peut être acceptable, voire souhaitable.

Je travaille dans une entreprise avec une grande base de code héritée, qui a deux problèmes qui contribuent tous deux à rendre souhaitable un test unitaire ami.

  • Nous souffrons de fonctions et de classes extrêmement volumineuses qui nécessitent une refactorisation, mais pour refactoriser, il est utile d'avoir des tests.
  • Une grande partie de notre code dépend de l'accès à la base de données, qui, pour diverses raisons, ne doit pas être intégré aux tests unitaires.

Dans certains cas, la moquerie est utile pour atténuer ce dernier problème, mais très souvent, cela conduit simplement à une conception inutilement complexe (héritages de classes où aucune ne serait autrement nécessaire), alors que l'on pourrait très simplement refactoriser le code de la manière suivante:

class Foo{
public:
     some_db_accessing_method(){
         // some line(s) of code with db dependance.

         // a bunch of code which is the real meat of the function

         // maybe a little more db access.
     }
}

Nous avons maintenant la situation où la viande de la fonction doit être refactorisée, nous aimerions donc un test unitaire. Il ne devrait pas être exposé publiquement. Maintenant, il y a une merveilleuse techniche appelée moquerie qui pourrait être utilisée dans cette situation, mais le fait est que dans ce cas, une simulation est exagérée. Cela m'obligerait à augmenter la complexité de la conception avec une héritage inutile.

Une approche beaucoup plus pragmatique serait de faire quelque chose comme ceci:

 class Foo{
 public: 
     some_db_accessing_method(){
         // db code as before
         unit_testable_meat(data_we_got_from_db);
         // maybe more db code.    
 }
 private:
     unit_testable_meat(...);
 }

Ce dernier me donne tous les avantages dont j'ai besoin des tests unitaires, y compris en me donnant ce précieux filet de sécurité pour attraper les erreurs produites lorsque je refactore le code dans la viande. Afin de le tester unitaire, je dois être ami avec une classe UnitTest, mais je dirais fortement que c'est bien mieux qu'une hiérarchie de code autrement inutile juste pour me permettre d'utiliser un Mock.

Je pense que cela devrait devenir un idiome, et je pense que c'est une solution pragmatique appropriée pour augmenter le retour sur investissement des tests unitaires.

9
Spacemoose

Comme bcat l'a suggéré, dans la mesure du possible, vous devez trouver les bogues en utilisant l'interface publique elle-même. Mais si vous voulez faire des choses comme imprimer des variables privées et comparer avec le résultat attendu, etc. (utile pour que les développeurs déboguent facilement les problèmes), vous pouvez faire de la classe UnitTest un ami de la classe à tester. Mais vous devrez peut-être l'ajouter sous une macro comme ci-dessous.

class Myclass
{
#if defined(UNIT_TEST)
    friend class UnitTest;
#endif
};

Activez l'indicateur UNIT_TEST uniquement lorsque des tests unitaires sont requis. Pour les autres versions, vous devez désactiver cet indicateur.

5
bjskishore123

Je ne vois rien de mal à utiliser une classe de test unitaire ami dans de nombreux cas. Oui, décomposer une grande classe en classes plus petites est parfois une meilleure façon de procéder. Je pense que les gens sont un peu trop pressés de rejeter l'utilisation du mot-clé ami pour quelque chose comme ça - ce n'est peut-être pas une conception orientée objet idéale, mais je peux sacrifier un peu d'idéalisme pour une meilleure couverture de test si c'est ce dont j'ai vraiment besoin.

4
Dave K

En règle générale, vous ne testez que l'interface publique afin d'être libre de reconcevoir et de refactoriser l'implémentation. L'ajout de cas de test pour les membres privés définit une exigence et une restriction sur l'implémentation de votre classe.

1
Inverse

Protégez les fonctions que vous souhaitez tester. Maintenant, dans votre fichier de test unitaire, créez une classe dérivée. Créez des fonctions d'encapsulation publiques qui appellent vos fonctions protégées de classe en cours de test.

0
klaas-jan