使用GoogleTest测试私有方法的最佳方法是什么?

Car*_*pez 33 c++ unit-testing private googletest

我想使用GoogleTest测试一些私有方法.

class Foo
{
private:
    int bar(...)
}
Run Code Online (Sandbox Code Playgroud)

GoogleTest允许使用两种方法.

选项1

使用FRIEND_TEST:

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

TEST(Foo, barReturnsZero)
{
    Foo foo;
    EXPECT_EQ(foo.bar(...), 0);
}
Run Code Online (Sandbox Code Playgroud)

这意味着在生产源文件中包含"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);
}
Run Code Online (Sandbox Code Playgroud)

方案3

PIMPL方法.

有关详细信息:Google测试:高级指南.

有没有其他方法来测试私有方法?每种选择有哪些优缺点?

Mat*_*ith 33

还有至少两个选项.我将通过解释某种情况列出你应该考虑的其他一些选项.

选项4:

考虑重构代码,以便您要测试的部分在另一个类中是公共的.通常,当你想要测试一个类的私有方法时,它就是设计糟糕的标志.我看到的最常见的(反)模式之一是Michael Feathers所谓的"冰山"课程."Iceberg"类有一个公共方法,其余的都是私有的(这就是测试私有方法的原因).它可能看起来像这样:

RuleEvaluator(从Michael Feathers偷来的)

例如,您可能希望GetNextToken()通过在字符串上连续调用它并看到它返回预期结果来进行测试.像这样的函数确实需要进行测试:这种行为并不简单,特别是如果您的标记化规则很复杂.让我们假装它并不是那么复杂,我们只想把空间划分为令牌.所以你写了一个测试,也许它看起来像这样:

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);
}
Run Code Online (Sandbox Code Playgroud)

嗯,这实际上看起来很不错.我们希望确保在进行更改时保持这种行为.不过GetNextToken()是一个私人的功能!所以我们不能这样测试它,因为它甚至不会编译.但是如何改变RuleEvaluator课程以遵循单一责任原则(单一责任原则)?例如,我们似乎有一个解析器,标记器和评估器卡在一个类中.将这些责任分开是不是更好?最重要的是,如果你创建一个Tokenizer类,那么它的公共方法就是HasMoreTokens()GetNextTokens().该RuleEvaluator班可以有一个Tokenizer对象作为成员.现在,除了我们测试Tokenizer类而不是类之外,我们可以保持与上面相同的测试RuleEvaluator.

这是UML中的样子:

重构RuleEvaluator类

请注意,这种新设计增加了模块化,因此您可能会在系统的其他部分中重复使用这些类(在您不能之前,私有方法根​​据定义不可重用).这是打破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);
}
Run Code Online (Sandbox Code Playgroud)

选项5

只是不要测试私有功能.有时他们不值得测试,因为他们将通过公共界面进行测试.很多时候,我看到的测试看起来非常相似,但测试两种不同的功能/方法.最终发生的事情是,当需求发生变化时(他们总是这样做),你现在有2个破坏的测试而不是1.如果你真的测试了所有的私有方法,你可能会有更多像10个破坏的测试而不是1个.简而言之. ,测试私有函数(通过使用FRIEND_TEST或公开它们),否则可以通过公共接口进行测试导致测试重复.你真的不想要这个,因为没有什么比你的测试套件更让你失望的伤害.它应该减少开发时间并降低维护成本!如果您测试通过公共接口进行测试的私有方法,那么测试套件可能会做相反的事情,并积极地增加维护成本并增加开发时间.当你公开私人功能,或者你使用类似的东西时FRIEND_TEST,你通常会后悔.

考虑以下可能的Tokenizer类实现:

Tokenizer的可能impl

假设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");
}
Run Code Online (Sandbox Code Playgroud)

好吧,现在让我们说需求会发生变化,现在你需要用","而不是空格来解析.当然,你会期望一个测试中断,但是当你测试私有函数时疼痛会增加.IMO,谷歌测试不应该允许FRIEND_TEST.这几乎不是你想要做的.Michael Feathers指的是像FRIEND_TEST"摸索工具"这样的东西,因为它试图触摸别人的私人部分.

我建议尽可能避免使用选项1和2,因为它通常会导致"测试重复",因此,当需求发生变化时,会有许多不必要的测试会中断.使用它们作为最后的手段.选项1和2是在这里和现在"测试私有方法"的最快方法(如同最快实施),但从长远来看,它们确实会损害生产力.

PIMPL也有意义,但它仍然允许一些非常糟糕的设计.小心一点.

我建议将Option 4(重构为较小的可测试组件)作为正确的起点,但有时你真正想要的是Option 5(通过公共接口测试私有函数).

PS这是关于冰山课程的相关讲座:https://www.youtube.com/watch?v = 4cVZvoFGJTU

PSS对于软件中的一切,答案取决于它.没有一种尺寸适合所有人.解决问题的选项取决于您的具体情况.

  • 这是一个很好的答案,我已将该视频添加到我的观看列表中.然而,有一部分让我烦恼,即使它与你的观点完全相反:你可以在示例测试结束时添加一个'HasMoreTokens`返回`false`的检查(和`EXPECT_EQ(result.size(),4) )`for`canGenerateSpaceDelimitedTokens`)? (4认同)
  • @ mwm314非常感谢您广泛而详细的回答以及您编写本书的时间!这是我的第一个Stack Overflow问题,我认为像这样的答案确实有助于知识共享.我实际上同意你刚才所说的一切.你帮了我很多忙.再次感谢! (2认同)
  • 只是我对测试私有函数的争论的 2 美分:我更愿意测试我的私有函数,因为有时它们并不微不足道;此外,如果我正在使用旧代码,我想尽快添加测试来帮助我重构代码,即使我正在使用自己的代码,我也想测试私有方法,直到它们被重构为其他抽象类的公共方法。 (2认同)