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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/47354280/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 15:42:18  来源:igfitidea点击:

What is the best way of testing private methods with GoogleTest?

c++unit-testingprivategoogletest

提问by Carlos Perez-Lopez

I would like to test some private methods using GoogleTest.

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

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

GoogleTest allows a couple of ways of doing this.

GoogleTest 允许使用几种方法来执行此操作。

OPTION 1

选项1

With FRIEND_TEST:

使用FRIEND_TEST

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

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

This implies to include "gtest/gtest.h" in the production source file.

这意味着在生产源文件中包含“gtest/gtest.h”。

OPTION 2

选项 2

Declare a test fixtureas a friend to the class and define accessors in the fixture:

声明一个测试夹具作为类的朋友并在夹具中定义访问器:

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

选项 3

The Pimpl idiom.

PIMPL方法

For details: Google Test: Advanced guide.

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

Are there any other ways to test private methods? What are some pros and cons of each option?

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

采纳答案by Matt Messersmith

There are at least two more options. I'll list out some other options you should consider by explaining a certain situation.

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

Option 4:

选项 4:

Consider refactoring your code so that the part you want to test is public in another class. Typically when you're tempted to test a class's private method, it's a sign of bad design. One of the most common (anti)paterns that I see is what Michael Feathers calls an "Iceberg" class. "Iceberg" classes have one public method, and the rest are private (which is why it's tempting to test the private methods). It might look something like this:

考虑重构您的代码,以便您要测试的部分在另一个类中是公开的。通常,当您想测试一个类的私有方法时,这是一个糟糕设计的迹象。我看到的最常见的(反)模式之一是 Michael Feathers 所说的“冰山”类。“冰山”类有一个公共方法,其余的都是私有的(这就是测试私有方法很诱人的原因)。它可能看起来像这样:

RuleEvaluator (stolen from Michael Feathers)

RuleEvaluator(从 Michael Feathers 窃取)

For example, you might want to test GetNextToken()by calling it on a string successively and seeing that it returns the expected result. A function like this doeswarrant a test: that behavior isn't trivial, especially if your tokenizing rules are complex. Let's pretend it's not all that complex, and we just want to rope in tokens delimited by space. So you write a test, maybe it looks something like this:

例如,您可能希望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);
}

Well, that actually looks pretty nice. We'd want to make sure we maintain this behavior as we make changes. But GetNextToken()is a privatefunction! So we can't test it like this, because it wont even compile. But what about changing the RuleEvaluatorclass to follow the Single Responsibility Principle (Single Responsibility Principle)? For instance, we seem to have a parser, tokenizer, and evaluator jammed into one class. Wouldn't it be better to just separate those responsibilities? On top of that, if you create a Tokenizerclass, then it's public methods would be HasMoreTokens()and GetNextTokens(). The RuleEvaluatorclass could have a Tokenizerobject as a member. Now, we can keep the same test as above, except we are testing the Tokenizerclass instead of the RuleEvaluatorclass.

嗯,这实际上看起来很不错。我们希望确保在进行更改时保持这种行为。但是GetNextToken()私有函数!所以我们不能像这样测试它,因为它甚至不会编译。但是如何更改RuleEvaluator类以遵循单一职责原则(Single Responsibility Principle)呢?例如,我们似乎将解析器、分词器和求值器集中在一个类中。把这些责任分开不是更好吗?最重要的是,如果您创建一个Tokenizer类,那么它的公共方法将是HasMoreTokens()and GetNextTokens()。该RuleEvaluator班可以有一个Tokenizer对象作为成员。现在,我们可以保持与上面相同的测试,除了我们测试的是Tokenizer类而不是RuleEvaluator班级。

Here's what it might look like in UML:

下面是它在 UML 中的样子:

Refactored RuleEvaluator class

重构的 RuleEvaluator 类

Note that this new design increases modularity, so you could potentially re-use these classes in other parts of your system (before you couldn't, private methods aren't reusable by definition). This is main advantage of breaking the RuleEvaluator down, along with increased understandability/locality.

请注意,这种新设计增加了模块化,因此您可能会在系统的其他部分重用这些类(在此之前,私有方法根​​据定义是不可重用的)。这是分解 RuleEvaluator 的主要优点,同时提高了可理解性/局部性。

The test would look extremely similar, except it would actually compile this time since the GetNextToken()method is now public on the Tokenizerclass:

该测试看起来非常相似,但这次实际上会编译,因为该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);
}

Option 5

选项 5

Just don't test the private functions. Sometimes they aren't worth testing because they will be tested through the public interface. A lot of times what I see is tests that look verysimilar, but test two different functions/methods. What ends up happening is that when requirements change (and they always do), you now have 2 broken tests instead of 1. And if you really tested all your private methods, you might have more like 10 broken tests instead of 1. In short, testing private functions (by using FRIEND_TESTor making them public) that could otherwise be tested through a public interface cause test duplication. You really don't want this, because nothing hurts more than your test suite slowing you down. It's supposed to decrease development time and decrease maintenance costs! If you test private methods that are otherwise tested through a public interface, the test suite may very well do the opposite, and actively increase maintenance costs and increase development time. When you make a private function public, or if you use something like FRIEND_TEST, you'll usually end up regretting it.

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

Consider the following possible implementation of the Tokenizerclass:

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

Possible impl of Tokenizer

Tokenizer 的可能实现

Let's say that SplitUpByDelimiter()is responsible for returning a std::vector<std::string>such that each element in the vector is a token. Furthermore, let's just say that GetNextToken()is simply an iterator over this vector. So your tests might look this:

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

Well, now let's say the requirements change, and you're now expected to parse by a "," instead of a space. Naturally, you're going to expect one test to break, but the pain increases when you test private functions. IMO, google test should not allow FRIEND_TEST. It is almost never what you want to do. Michael Feathers refers to things like FRIEND_TESTas a "groping tool", since it's trying to touch someone else's private parts.

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

I recommend avoiding option 1 and 2 when you can, as it typically causes "test duplication", and as a consequence, many more tests than necessary will break when requirements change. Use them as a last resort.Option 1 and 2 are the fastest ways to "test private methods" for here and now (as in the fastest to implement), but they'll really hurt productivity in the long run.

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

PIMPL can make sense too, but it still allows for some pretty bad design. Be careful with it.

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

I'd recommend Option 4 (refactoring into smaller testable components) as the right place to start, but sometimes what you really want is Option 5 (testing the private functions through the public interface).

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

P.S. Here's the relevant lecture about iceberg classes: https://www.youtube.com/watch?v=4cVZvoFGJTU

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

P.S.S. As for everything in software, the answer is it depends. There is no one size fits all. The option that solves your problem will depend on your specificcircumstances.

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