web-dev-qa-db-ja.com

GoogleTestでプライベートメソッドをテストする最良の方法は何ですか?

GoogleTestを使用していくつかのプライベートメソッドをテストしたいと思います。

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

GoogleTestでは、これを行うためのいくつかの方法が許可されています。

オプション1

FRIEND_TESTの場合:

class Foo
{
private:
    FRIEND_TEST(Foo, barReturnsZero);
    int bar(...);
}

TEST(Foo, barReturnsZero)
{
    Foo foo;
    EXPECT_EQ(foo.bar(...), 0);
}

これは、プロダクションソースファイルに「gtest/gtest.h」を含めることを意味します。

オプション2

テストフィクスチャをクラスのフレンドとして宣言し、フィクスチャでアクセッサを定義します:

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);
}

オプション3

Pimplイディオム

詳細: Googleテスト:上級ガイド

プライベートメソッドをテストする他の方法はありますか?各オプションの長所と短所は何ですか?

28

少なくとも2つのオプションがあります。特定の状況を説明することで考慮すべき他のオプションをいくつかリストします。

オプション4:

テストする部分が別のクラスで公開されるように、コードをリファクタリングすることを検討してください。通常、クラスのプライベートメソッドをテストしたいときは、デザインが悪いことを示しています。私が見る最も一般的な(アンチ)パターンの1つは、Michael Feathersが「Iceberg」クラスと呼ぶものです。 「Iceberg」クラスには1つのパブリックメソッドがあり、残りはプライベートです(そのため、プライベートメソッドをテストするのは魅力的です)。次のようになります。

RuleEvaluator (stolen from Michael Feathers)

たとえば、GetNextToken()を文字列で連続して呼び出して、期待される結果が返されることを確認して、テストすることができます。このような関数doesはテストを保証します。特にトークン化ルールが複雑な場合、その動作は簡単ではありません。それほど複雑ではないふりをしましょう。スペースで区切られたトークンでロープを張るだけです。したがって、テストを作成すると、次のようになります。

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);
}

まあ、それは実際にはかなり素敵に見えます。変更を行ったときにこの動作を維持することを確認したいと思います。しかし、GetNextToken()private関数です!したがって、このようにテストすることはできませんコンパイルすらしないため。しかし、単一責任の原則(単一責任の原則)に従うようにRuleEvaluatorクラスを変更するのはどうでしょうか。たとえば、パーサー、トークナイザー、およびエバリュエーターが1つのクラスに詰め込まれているようです。これらの責任を分離する方が良いと思いませんか?さらに、Tokenizerクラスを作成すると、そのパブリックメソッドはHasMoreTokens()およびGetNextTokens()になります。 RuleEvaluatorクラスには、Tokenizerオブジェクトをメンバーとして含めることができます。これで、Tokenizerクラスの代わりにRuleEvaluatorクラスをテストすることを除いて、上記と同じテストを維持できます。

UMLでは次のようになります。

Refactored RuleEvaluator class

この新しい設計によりモジュール性が向上するため、システムの他の部分でこれらのクラスを再利用できる可能性があることに注意してください(定義できない場合、プライベートメソッドは再利用できません)。これは、RuleEvaluatorを分類することの主な利点であり、理解度/局所性が向上します。

テストは非常によく似ていますが、GetNextToken()メソッドが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);
}

オプション5

プライベート関数をテストしないでください。パブリックインターフェイスを介してテストされるため、テストする価値がない場合もあります。多くの場合、テストはveryに似ていますが、2つの異なる関数/メソッドをテストします。最終的に起こるのは、要件が変更されると(常に変更される)、1ではなく2つのテストが壊れていることです。そして、すべてのプライベートメソッドを実際にテストした場合、1ではなく10のテストが壊れている可能性があります要するに、パブリックインターフェイスを介してテストされる可能性のあるプライベート関数をテストする(FRIEND_TESTを使用するか、パブリックにする)と、テストの重複が発生します。テストスイートがあなたを遅くする以上に痛いものはないので、あなたは本当にこれを望んでいません。開発時間を短縮し、メンテナンスコストを削減することになっています!他の方法でパブリックインターフェイスを介してテストされるプライベートメソッドをテストする場合、テストスイートはその逆を非常にうまく実行し、メンテナンスコストと開発時間を積極的に増やします。プライベート関数をパブリックにするとき、またはFRIEND_TESTのようなものを使用する場合、通常は後悔することになります。

Tokenizerクラスの次の可能な実装を検討してください。

Possible impl of Tokenizer

SplitUpByDelimiter()std::vector<std::string>を返し、ベクター内の各要素がトークンになるようにします。さらに、GetNextToken()はこのベクトルの単なる反復子であるとだけ言ってみましょう。したがって、テストは次のようになります。

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");
}

さて、要件が変わったとしましょう。スペースではなく「、」で解析することが期待されています。当然、1つのテストが失敗することを期待しますが、プライベート関数をテストすると痛みが増します。 IMO、GoogleテストではFRIEND_TESTを許可しないでください。やりたいことはほとんどありません。 Michael FeathersはFRIEND_TESTのようなものを「模索ツール」と呼んでいます。なぜなら、それは誰かのプライベートな部分に触れようとしているからです。

オプション1と2はできる限り避けることをお勧めします。これは通常「テストの重複」を引き起こし、その結果、要件が変わると必要以上のテストが壊れてしまうためです。それらを最後の手段として使用してください。オプション1および2は、現在および最速の「プライベートメソッドをテストする」ための最速の方法ですが(実装が最速です)、生産性を大幅に低下させます。長い目で見れば。

PIMPLも理にかなっていますが、それでもかなり悪い設計が可能です。それに注意してください。

開始する適切な場所として、オプション4(より小さなテスト可能なコンポーネントにリファクタリングすること)をお勧めしますが、場合によっては本当に必要なのはオプション5(パブリックインターフェイスを介してプライベート関数をテストすることです)

追伸氷山クラスに関する関連する講義は次のとおりです。 https://www.youtube.com/watch?v=4cVZvoFGJT

追伸ソフトウェアのすべてに関して、答えは依存するです。すべてに適合するサイズはありません。問題を解決するオプションは、specific状況によって異なります。

31