Quel est le meilleur moyen de tester des méthodes privées avec GoogleTest?

J’aimerais tester des méthodes privées à l’aide de GoogleTest.

class Foo { private: int bar(...) } 

GoogleTest permet deux manières de procéder.

OPTION 1

Avec FRIEND_TEST :

 class Foo { private: FRIEND_TEST(Foo, barReturnsZero); int bar(...); } TEST(Foo, barReturnsZero) { Foo foo; EXPECT_EQ(foo.bar(...), 0); } 

Cela implique d’inclure “gtest / gtest.h” dans le fichier source de production.

OPTION 2

Déclarez un appareil de test en tant qu’ami de la classe et définissez les accesseurs dans l’appareil:

 class Foo { friend class FooTest; private: int bar(...); } class FooTest : public ::testing::Test { protected: int bar(...) { foo.bar(...); } private: Foo foo; } TEST_F(FooTest, barReturnsZero) { EXPECT_EQ(bar(...), 0); } 

OPTION 3

Le langage Pimpl .

Pour plus de détails: Google Test: Guide avancé .

Existe-t-il d’autres méthodes pour tester les méthodes privées? Quels sont les avantages et les inconvénients de chaque option?

Il y a au moins deux autres options. Je vais énumérer quelques autres options que vous devriez envisager en expliquant une situation donnée.

Option 4:

Pensez à refactoriser votre code afin que la partie que vous souhaitez tester soit publique dans une autre classe. Généralement, lorsque vous êtes tenté de tester la méthode privée d’une classe, c’est un signe de mauvaise conception. L’un des antinomiques les plus courants que je vois est ce que Michael Feathers appelle une classe “Iceberg”. Les classes “iceberg” ont une méthode publique et les autres sont privées (c’est pourquoi il est tentant de tester les méthodes privées). Cela pourrait ressembler à quelque chose comme ça:

RuleEvaluator (volé à Michael Feathers)

Par exemple, vous pouvez tester GetNextToken() en l’appelant successivement sur une chaîne et en GetNextToken() qu’elle renvoie le résultat attendu. Une fonction comme celle- ci justifie un test: ce comportement n’est pas anodin, surtout si vos règles de tokenizing sont complexes. Imaginons que ce n’est pas si complexe et que nous voulons simplement utiliser des jetons délimités par de l’espace. Donc, vous écrivez un test, peut-être qu’il ressemble à ceci:

 TEST(RuleEvaluator, canParseSpaceDelimtedTokens) { std::ssortingng input_ssortingng = "1 2 test bar"; RuleEvaluator re = RuleEvaluator(input_ssortingng); EXPECT_EQ(re.GetNextToken(), "1"); EXPECT_EQ(re.GetNextToken(), "2"); EXPECT_EQ(re.GetNextToken(), "test"); EXPECT_EQ(re.GetNextToken(), "bar"); EXPECT_EQ(re.HasMoreTokens(), false); } 

Eh bien, ça a l’air plutôt sympa. Nous voudrions nous assurer de conserver ce comportement lorsque nous apportons des changements. Mais GetNextToken() est une fonction privée ! Nous ne pouvons donc pas le tester comme ceci, car il ne comstackra même pas . Mais qu’en est-il de changer la classe RuleEvaluator pour qu’elle respecte le principe de responsabilité unique (principe de responsabilité unique)? Par exemple, nous semblons avoir un parsingur syntaxique, un générateur de jetons et un évaluateur intégrés dans une classe. Ne vaudrait-il pas mieux séparer ces responsabilités? De plus, si vous créez une classe Tokenizer , les méthodes publiques utilisées sont HasMoreTokens() et GetNextTokens() . La classe RuleEvaluator peut avoir un object Tokenizer tant que membre. Maintenant, nous pouvons garder le même test que ci-dessus, sauf que nous testons la classe Tokenizer au lieu de la classe RuleEvaluator .

Voici à quoi cela pourrait ressembler en UML:

Classe Refactored RuleEvaluator

Notez que cette nouvelle conception augmente la modularité, de sorte que vous pouvez potentiellement réutiliser ces classes dans d’autres parties de votre système (auparavant, les méthodes privées ne sont pas réutilisables par définition). C’est le principal avantage de la décomposition de RuleEvaluator, ainsi qu’une compréhension et une localisation accrues.

Le test serait extrêmement similaire, sauf qu’il serait compilé cette fois puisque la méthode GetNextToken() est maintenant publique sur la classe Tokenizer :

 TEST(Tokenizer, canParseSpaceDelimtedTokens) { std::ssortingng input_ssortingng = "1 2 test bar"; Tokenizer tokenizer = Tokenizer(input_ssortingng); EXPECT_EQ(tokenizer.GetNextToken(), "1"); EXPECT_EQ(tokenizer.GetNextToken(), "2"); EXPECT_EQ(tokenizer.GetNextToken(), "test"); EXPECT_EQ(tokenizer.GetNextToken(), "bar"); EXPECT_EQ(tokenizer.HasMoreTokens(), false); } 

Option 5

Juste ne testez pas les fonctions privées. Parfois, ils ne valent pas la peine d’être testés car ils seront testés via l’interface publique. Souvent, je vois des tests très similaires, mais deux fonctions / méthodes différentes. En fin de compte, lorsque les exigences changent (et elles le font toujours), vous avez maintenant 2 tests cassés au lieu de 1. Et si vous testiez réellement toutes vos méthodes privées, vous pourriez en avoir davantage comme 10 tests cassés au lieu de 1. En bref , tester des fonctions privées (en utilisant FRIEND_TEST ou en les rendant publiques) qui pourraient autrement être testées via une interface publique provoque la duplication de test . Vous ne voulez vraiment pas cela, car rien ne fait plus mal que votre suite de tests vous ralentit. Il est supposé réduire le temps de développement et les coûts de maintenance! Si vous testez des méthodes privées qui sont par ailleurs testées via une interface publique, la suite de tests peut très bien faire le contraire, augmenter activement les coûts de maintenance et augmenter le temps de développement. Lorsque vous rendez une fonction privée publique ou si vous utilisez quelque chose comme FRIEND_TEST , vous finirez généralement par le regretter.

Considérez l’implémentation possible suivante de la classe Tokenizer :

Implication possible de Tokenizer

Supposons que SplitUpByDelimiter() est responsable du retour d’un std::vector tel que chaque élément du vecteur soit un jeton. De plus, disons simplement que GetNextToken() est simplement un iterator de ce vecteur. Donc, vos tests pourraient ressembler à ceci:

 TEST(Tokenizer, canParseSpaceDelimtedTokens) { std::ssortingng input_ssortingng = "1 2 test bar"; Tokenizer tokenizer = Tokenizer(input_ssortingng); EXPECT_EQ(tokenizer.GetNextToken(), "1"); EXPECT_EQ(tokenizer.GetNextToken(), "2"); EXPECT_EQ(tokenizer.GetNextToken(), "test"); EXPECT_EQ(tokenizer.GetNextToken(), "bar"); EXPECT_EQ(tokenizer.HasMoreTokens(), false); } // Pretend we have some class for a FRIEND_TEST TEST_F(TokenizerTest, canGenerateSpaceDelimtedTokens) { std::ssortingng input_ssortingng = "1 2 test bar"; Tokenizer tokenizer = Tokenizer(input_ssortingng); std::vector result = tokenizer.SplitUpByDelimiter(" "); EXPECT_EQ(result.size(), 4); EXPECT_EQ(result[0], "1"); EXPECT_EQ(result[1], "2"); EXPECT_EQ(result[2], "test"); EXPECT_EQ(result[3], "bar"); } 

Eh bien, maintenant, supposons que les exigences changent et que vous devez maintenant parsingr un “,” au lieu d’un espace. Naturellement, vous vous attendez à ce qu’un test se termine, mais la douleur augmente lorsque vous testez des fonctions privées. IMO, le test de Google ne devrait pas autoriser FRIEND_TEST. Ce n’est presque jamais ce que vous voulez faire. Michael Feathers FRIEND_TEST comme un “outil à tâtons”, car il tente de toucher les parties intimes de quelqu’un d’autre.

Je recommande d’éviter les options 1 et 2 lorsque vous le pouvez, car cela entraîne généralement une «duplication de test» et, par conséquent, un nombre de tests supérieur au nombre de tests nécessaires sera interrompu lorsque les exigences changeront. Utilisez-les en dernier recours. Les options 1 et 2 sont les moyens les plus rapides de “tester les méthodes privées” pour l’ici et le maintenant (comme pour les plus rapides à implémenter), mais elles vont vraiment nuire à la productivité à long terme.

PIMPL peut aussi avoir un sens, mais il permet tout de même une très mauvaise conception. Sois prudent avec ça.

Je recommanderais l’option 4 (refactorisation en composants plus petits testables) comme étant le bon endroit pour commencer, mais parfois ce que vous voulez vraiment, c’est l’option 5 (tester les fonctions privées via l’interface publique).

PS Voici la conférence pertinente sur les classes d’iceberg: https://www.youtube.com/watch?v=4cVZvoFGJTU

PSS En ce qui concerne tout dans le logiciel, la réponse est que cela dépend . Il n’y a pas de taille unique. L’option qui résout votre problème dépendra de votre situation particulière .