使用 GoogleTest 测试私有方法的最佳方法是啥? [关闭]

Posted

技术标签:

【中文标题】使用 GoogleTest 测试私有方法的最佳方法是啥? [关闭]【英文标题】:What is the best way of testing private methods with GoogleTest? [closed]使用 GoogleTest 测试私有方法的最佳方法是什么? [关闭] 【发布时间】:2018-05-01 10:40:36 【问题描述】:

我想使用 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。

还有其他方法可以测试私有方法吗?每个选项的优缺点是什么?

【问题讨论】:

一般来说,您不应该直接对私有方法进行单元测试。相反,您应该为调用方法编写更多测试,以涵盖各种边缘情况。 我已经更新了问题和我的答案,使其不再那么“基于意见”。我认为它应该保持开放,因为可能还有其他未列举的解决这个问题的方法。此外,OP表示他们发现答案很有用。 OP 也可能会发现另一个有用的答案。 @SebastianLenartowicz 不过,直接测试私有(或受保护!)方法通常更简单、更清晰。来自班级用户的 POV 的封装可能与来自班级测试人员的 POV 不同。单元测试是细粒度的。如果代码足够复杂,可以分离成自己的函数(私有或非私有),那么它应该有自己的单元测试。 【参考方案1】:

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

选项 4:

考虑重构您的代码,以便您要测试的部分在另一个类中是公开的。通常,当您想测试一个类的私有方法时,这是一个糟糕设计的标志。我看到的最常见的(反)模式之一是 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 类以遵循单一职责原则(Single Responsibility Principle)呢?例如,我们似乎将解析器、标记器和评估器塞进了一个类。把这些责任分开不是更好吗?最重要的是,如果您创建一个Tokenizer 类,那么它的公共方法将是HasMoreTokens()GetNextTokens()RuleEvaluator 类可以有一个 Tokenizer 对象作为成员。现在,我们可以保持与上面相同的测试,只是我们测试的是Tokenizer 类而不是RuleEvaluator 类。

这是它在 UML 中的样子:

请注意,这种新设计增加了模块化,因此您可以在系统的其他部分重用这些类(在不能重用之前,私有方法根​​据定义是不可重用的)。这是分解 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 类的可能实现:

假设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");

好吧,现在假设需求发生了变化,您现在需要用“,”而不是空格来解析。自然地,你会期望一个测试会失败,但是当你测试私有函数时痛苦会增加。 IMO,谷歌测试不应该允许 FRIEND_TEST。这几乎从来都不是你想做的。 Michael Feathers 将FRIEND_TEST 之类的东西称为“摸索工具”,因为它试图触碰别人的私处。

我建议尽可能避免使用选项 1 和 2,因为它通常会导致“测试重复”,因此,当需求发生变化时,更多的测试会中断。将它们用作最后​​的手段。 选项 1 和 2 是目前“测试私有方法”的最快方法(以最快的速度实施),但从长远来看,它们确实会损害生产力。

PIMPL 也可以,但它仍然允许一些非常糟糕的设计。小心点。

我推荐选项 4(重构为更小的可测试组件)作为正确的起点,但有时您真正想要的是选项 5(通过公共接口测试私有函数)。

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

附言至于软件中的一切,答案是视情况而定。没有一种尺寸适合所有人。解决您的问题的选项将取决于您的具体情况。

【讨论】:

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

以上是关于使用 GoogleTest 测试私有方法的最佳方法是啥? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

googletest--测试控制

如何在 Googletest 中运行两个不同的测试

在 PHP 中保持 MySQL 凭据私有的最佳方法是啥?

在 Objective-C 中定义私有变量的最佳方法

方法调用公共/私有成员或方法最佳实践 - C#.NET

GoogleTest初探