Quelle est la meilleure façon de tester des méthodes privées avec GoogleTest? [fermé]

j'aimerais tester quelques méthodes privées en utilisant GoogleTest.

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

GoogleTest permet un couple de façons de faire ceci.

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éclarer un montage d'essai en tant qu'ami de la classe et définir les accessoires dans le montage:

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 Pimpl idiome .

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

y at-il d'autres moyens de tester les méthodes privées? Quels sont les avantages et les inconvénients de chaque option?

15
demandé sur Vadim Kotov 2017-11-17 18:40:48

1 réponses

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

Option 4:

pensez à modifier votre code pour que la partie que vous voulez tester soit publique dans une autre classe. Généralement, quand vous êtes tenté de tester une classe privée de méthode, c'est un signe de mauvaise conception. L'un des paterns (anti)les plus communs 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). Il pourrait ressembler à quelque chose comme ceci:

RuleEvaluator (stolen from Michael Feathers)

par exemple, vous pourriez vouloir tester GetNextToken() en l'appelant sur une chaîne de caractères successivement et en voyant qu'il renvoie le résultat attendu. Une fonction comme ceci ne mandat d'un test: ce comportement n'est pas trivial, surtout si vos règles de tokenizing sont complexes. Imaginons que ce n'est pas si complexe, et que nous voulons juste des jetons délimités par l'espace. Donc vous écrivez un test, peut-être qu'il ressemble à quelque chose comme 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);
}

ça a l'air sympa. Nous voulons nous assurer de maintenir ce comportement que nous devons apporter des modifications. Mais GetNextToken() est une fonction privée ! On ne peut donc pas tester de cette façon, parce qu'il ne compile même pas . Mais qu'en est-il de changer la classe RuleEvaluator pour suivre le principe de la responsabilité unique (principe de la Responsabilité Unique)? Par exemple, il semble que nous ayons un analyseur, un tokenizer et un évaluateur bloqué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 , alors les méthodes publiques seraient HasMoreTokens() et GetNextTokens() . La classe RuleEvaluator pourrait avoir Tokenizer objet en 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 il pourrait ressembler dans UML:

Refactored RuleEvaluator class

notez que ce nouveau design augmente la modularité, de sorte que vous pouvez potentiellement réutiliser ces classes dans d'autres parties de votre système (avant que vous ne pouviez pas, privé les méthodes ne sont pas réutilisables par définition). C'est le principal avantage de briser L'Évaluateurrègle vers le bas, avec une intelligibilité accrue/localité.

le test serait extrêmement similaire, sauf qu'il compilerait cette fois-ci 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

ne testez pas les fonctions privées. Parfois, ils ils ne valent pas la peine d'être testés car ils seront testés par l'intermédiaire de l'interface publique. Beaucoup de fois ce que je vois est des tests qui ressemblent très similaire, mais tester deux fonctions/méthodes différentes. Ce qui finit par se produire est que lorsque les exigences changent (et ils le font toujours), vous avez maintenant 2 tests cassés au lieu de 1. Et si vous avez vraiment testé toutes vos méthodes privées, vous pourriez avoir plus comme 10 tests cassés au lieu de 1. en bref, tester les fonctions privées (en utilisant FRIEND_TEST ou les rendre publiques) qui pourraient autrement être testées par l'intermédiaire d'une interface publique provoquent une duplication des tests . Tu ne veux vraiment pas de ça, parce que rien ne fait plus mal que ta suite de test qui te ralentit. Il est censé réduire le temps de développement et les coûts de maintenance! Si vous testez des méthodes privées qui sont autrement testées par une interface publique, la suite de test peut très bien faire le contraire, et augmenter activement les coûts de maintenance et augmenter le développement temps. Quand vous faites une fonction privée publique, ou si vous utilisez quelque chose comme FRIEND_TEST , vous finirez généralement par le regretter.

envisager la mise en œuvre suivante de la classe Tokenizer :

Possible impl of Tokenizer

disons que SplitUpByDelimiter() est responsable de retourner un std::vector<std::string> tel que chaque élément dans le vecteur est un jeton. En outre, nous allons simplement disons que GetNextToken() est simplement un itérateur sur 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, disons que les exigences changent, et vous êtes maintenant censé Parser par un", " au lieu d'un espace. Naturellement, vous allez vous attendre à un test pour briser, mais la douleur augmente quand vous testez des fonctions privées. IMO, google test ne doit pas permettre FRIEND_TEST. Il n'est presque jamais ce que vous voulez faire. Michael Feathers se réfère à des choses comme FRIEND_TEST comme un "tâtonnement outil", puisque c'est en essayant de toucher les parties intimes d'autrui.

je recommande d'éviter les options 1 et 2 lorsque vous le pouvez, car elles provoquent généralement une "duplication des tests", et par conséquent, beaucoup plus de tests que nécessaire seront interrompus lorsque les exigences changent. Les utiliser que comme un dernier recours. les options 1 et 2 sont les moyens les plus rapides pour "tester des méthodes privées" pour ici et maintenant (comme dans la plus rapide à mettre en œuvre), mais ils vont vraiment nuire à la productivité dans le long terme.

PIMPL peut avoir un sens aussi, mais il permet toujours un certain assez mauvais design. Être prudent avec elle.

je recommande L'Option 4 (refactoring into smaller testable components) comme 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).

voici la conférence pertinente sur les classes d'icebergs: https://www.youtube.com/watch?v=4cVZvoFGJTU

P. S. S. Comme pour tout dans le logiciel, la réponse est cela dépend . Il n'est pas one size fits all. L'option qui résout votre problème dépendra de vos circonstances spécifiques .

14
répondu Matt Messersmith 2017-11-18 15:54:25