web-dev-qa-db-fra.com

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 a test fixture en tant qu'ami de la classe et définissez les accesseurs dans cette fixture:

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 idiome 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?

28
Carlos Perez-Lopez

Il y a au moins deux autres options. Je vais énumérer d'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 (stolen from Michael Feathers)

Par exemple, vous pouvez tester GetNextToken() en l'appelant successivement sur une chaîne et en vérifiant qu'elle renvoie le résultat attendu. Une fonction comme celle-ci fait 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::string input_string = "1 2 test bar";
    RuleEvaluator re = RuleEvaluator(input_string);
    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 joli. 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 compilera 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 analyseur syntaxique, un tokenizer et un évaluateur intégrés dans une classe. Ne vaudrait-il pas mieux séparer ces responsabilités? En plus de cela, si vous créez une classe Tokenizer, ses méthodes publiques seraient HasMoreTokens() et GetNextTokens(). La classe RuleEvaluator pourrait avoir un objet Tokenizer en tant que membre. Maintenant, nous pouvons garder le même test que ci-dessus, sauf que nous testons la classe Tokenizer à la place de la classe RuleEvaluator.

Voici à quoi cela pourrait ressembler dans UML:

Refactored RuleEvaluator class

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 décomposer RuleEvaluator, ainsi qu’une compréhension et une localisation accrues.

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

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    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

Il suffit de ne pas tester 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, ce que je vois, ce sont des tests qui ont l’air très similaires, mais qui testent 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 provoquerait une 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 suivante de la classe Tokenizer:

Possible impl of Tokenizer

Disons que SplitUpByDelimiter() est responsable du renvoi d'un std::vector<std::string> Tel que chaque élément du vecteur soit un jeton. De plus, disons simplement que GetNextToken() est simplement un itérateur de ce vecteur. Donc, vos tests pourraient ressembler à ceci:

TEST(Tokenizer, canParseSpaceDelimtedTokens)
{
    std::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    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::string input_string = "1 2 test bar";
    Tokenizer tokenizer = Tokenizer(input_string);
    std::vector<std::string> 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 analyser un "," au lieu d'un espace. Naturellement, vous allez vous attendre à un test, mais la douleur augmente lorsque vous testez des fonctions privées. OMI, le test de Google ne devrait pas autoriser FRIEND_TEST. Ce n'est presque jamais ce que vous voulez faire. Michael Feathers se réfère à des choses comme FRIEND_TEST Comme à un "outil à tâtons", car il essaie de toucher les parties intimes de quelqu'un d'autre.

Je vous 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 nécessaire sera annulé 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 ici et 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. Faites attention 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 est l'option 5 (tester les fonctions privées via l'interface publique).

P.S. Voici le cours pertinent sur les classes d'iceberg: https://www.youtube.com/watch?v=4cVZvoFGJT

P.S.S. En ce qui concerne tout dans le logiciel, la réponse est ça dépend. Il n'y a pas de taille unique. L'option qui résout votre problème dépendra de vos circonstances spécifiques.

31
Matt Messersmith