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

时间:2017-11-17 15:40:48

标签: c++ unit-testing private 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 Test: Advanced guide

还有其他方法可以测试私有方法吗?每个选项有哪些优点和缺点?

1 个答案:

答案 0 :(得分:24)

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

选项4:

考虑重构代码,以便您要测试的部分在另一个类中是公共的。通常,当您试图测试一个类的私有方法时,它就是设计糟糕的标志。我看到的最常见的(反)模式之一是Michael Feathers所说的“冰山”#34;类。 "冰山"类有一个公共方法,其余的是私有的(这就是为什么它很容易测试私有方法)。它可能看起来像这样:

RuleEvaluator (stolen from 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);
}

嗯,实际上看起来很不错。我们希望确保在进行更改时保持此行为。但是GetNextToken()是一个私有函数!所以我们不能像这样测试它,因为它甚至不会编译。但是如何改变RuleEvaluator课程以遵循单一责任原则(单一责任原则)?例如,我们似乎有一个解析器,标记器和评估器卡在一个类中。将这些责任分开是不是更好?最重要的是,如果您创建了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

不要测试私人功能。有时他们不值得测试,因为他们将通过公共界面进行测试。很多时候我看到的是看起来很像非常的测试,但测试了两种不同的函数/方法。最终发生的事情是,当需求发生变化时(他们总是会这样做),你现在有2个破坏的测试而不是1.如果你真的测试了所有的私有方法,你可能会有更多的10个破坏的测试而不是1个。 >简而言之,测试私有函数(通过使用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");
}

嗯,现在让我们说需求会发生变化,现在您需要通过&#34;&#34;来解析。而不是空间。当然,你会期望一个测试中断,但是当你测试私有函数时疼痛会增加。 IMO,谷歌测试不应该允许FRIEND_TEST。这几乎不是你想要做的。迈克尔·费瑟斯(Michael Feathers)将诸如FRIEND_TEST之类的东西称为“摸索工具”,因为它试图触摸别人的私人部分。

我建议尽可能避免使用选项1和2,因为它通常会导致&#34;测试重复&#34;,因此,当需求发生变化时,需要的测试将超过必要的数量。使用它们作为最后的手段。选项1和2是测试私人方法的最快方法&#34;对于现在和现在(以最快的方式实施),但从长远来看,它们确实会损害生产力。

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

我建议将选项4(重构为较小的可测试组件)作为正确的起点,但有时您真正想要的是选项5(通过公共接口测试私有功能)。

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

P.S.S。对于软件中的所有内容,答案是取决于。没有一种尺寸适合所有人。解决问题的选项取决于您的特定情况。