使用 GoogleTest 测试私有方法的最佳方法是什么?
What is the best way of testing private methods with 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的友元并在夹具中定义访问器:
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
粉刺成语。
详情:Google Test: Advanced guide.
还有其他方法可以测试私有方法吗?每个选项的优点和缺点是什么?
至少还有两个选项。我将通过解释特定情况列出您应该考虑的其他一些选项。
选项 4:
考虑重构您的代码,以便您要测试的部分是另一个 class 中的 public。通常,当您想要测试 class 的私有方法时,这是糟糕设计的标志。我看到的最常见的(反)模式之一是 Michael Feathers 所说的 "Iceberg" class。 "Iceberg" classes 有一个 public 方法,其余的都是私有的(这就是为什么很想测试私有方法的原因)。它可能看起来像这样:
例如,您可能想通过连续调用字符串来测试 GetNextToken()
并查看它 returns 预期的结果。像这样的函数 确实 需要测试:该行为并非微不足道,尤其是当您的分词规则很复杂时。让我们假设它并没有那么复杂,我们只想加入由 space 分隔的标记。所以你写了一个测试,也许它看起来像这样:
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
class 以遵循单一职责原则(Single Responsibility Principle)呢?例如,我们似乎将解析器、分词器和求值器合而为一 class。将这些职责分开不是更好吗?最重要的是,如果您创建 Tokenizer
class,那么它的 public 方法将是 HasMoreTokens()
和 GetNextTokens()
。 RuleEvaluator
class 可以有一个 Tokenizer
对象作为成员。现在,我们可以保持与上面相同的测试,除了我们正在测试 Tokenizer
class 而不是 RuleEvaluator
class.
在 UML 中可能是这样的:
请注意,这种新设计增加了模块化,因此您可能会在系统的其他部分重复使用这些 classes(在您不能这样做之前,私有方法根据定义是不可重复使用的)。这是打破 RuleEvaluator 的主要优势,同时增加 understandability/locality.
测试看起来非常相似,除了这次它实际上会编译,因为 GetNextToken()
方法现在 public 在 Tokenizer
class:
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
只是不要测试私有函数。有时它们不值得测试,因为它们将通过 public 接口进行测试。很多时候我看到的是看起来 非常 相似的测试,但测试两个不同的 functions/methods。最终发生的事情是,当需求发生变化时(他们总是这样),你现在有 2 个失败的测试而不是 1 个。如果你真的测试了你所有的私有方法,你可能会有更多的 10 个失败的测试而不是 1 个。 简而言之,测试原本可以通过 public 接口测试的私有函数(通过使用 FRIEND_TEST
或使它们成为 public)会导致测试重复 。你真的不想要这个,因为没有什么比你的测试套件让你慢下来更有害的了。它应该减少开发时间并降低维护成本!如果您测试通过 public 接口以其他方式测试的私有方法,则测试套件很可能会做相反的事情,并主动增加维护成本并增加开发时间。当你做一个私有函数public,或者如果你使用类似FRIEND_TEST
的东西,你通常会后悔的。
考虑 Tokenizer
class 的以下可能实现:
假设 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");
}
好吧,现在让我们假设需求发生变化,您现在需要用“,”而不是 space 来解析。自然地,您会期望一个测试失败,但是当您测试私有函数时痛苦会增加。 IMO,google 测试不应允许 FRIEND_TEST。这几乎从来都不是您想要做的。 Michael Feathers 将 FRIEND_TEST
之类的东西称为 "groping tool",因为它试图触摸别人的私处。
我建议尽可能避免选项 1 和 2,因为它通常会导致 "test duplication",因此,当需求发生变化时,比必要的更多的测试将会中断。将它们作为最后的手段使用。 选项 1 和 2 是此时此地 "test private methods" 的最快方法(就像实施最快一样),但从长远来看它们确实会损害生产力 运行.
PIMPL 也很有意义,但它仍然允许进行一些非常糟糕的设计。小心点。
我推荐选项 4(重构为更小的可测试组件)作为正确的起点,但有时您真正想要的是选项 5(通过 public界面)。
P.S。这是关于冰山 classes 的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU
P.S.S.至于软件中的一切,答案是它取决于。没有一种尺寸适合所有人。解决您问题的选项将取决于您的具体情况。
我想使用 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的友元并在夹具中定义访问器:
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
粉刺成语。
详情:Google Test: Advanced guide.
还有其他方法可以测试私有方法吗?每个选项的优点和缺点是什么?
至少还有两个选项。我将通过解释特定情况列出您应该考虑的其他一些选项。
选项 4:
考虑重构您的代码,以便您要测试的部分是另一个 class 中的 public。通常,当您想要测试 class 的私有方法时,这是糟糕设计的标志。我看到的最常见的(反)模式之一是 Michael Feathers 所说的 "Iceberg" class。 "Iceberg" classes 有一个 public 方法,其余的都是私有的(这就是为什么很想测试私有方法的原因)。它可能看起来像这样:
例如,您可能想通过连续调用字符串来测试 GetNextToken()
并查看它 returns 预期的结果。像这样的函数 确实 需要测试:该行为并非微不足道,尤其是当您的分词规则很复杂时。让我们假设它并没有那么复杂,我们只想加入由 space 分隔的标记。所以你写了一个测试,也许它看起来像这样:
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
class 以遵循单一职责原则(Single Responsibility Principle)呢?例如,我们似乎将解析器、分词器和求值器合而为一 class。将这些职责分开不是更好吗?最重要的是,如果您创建 Tokenizer
class,那么它的 public 方法将是 HasMoreTokens()
和 GetNextTokens()
。 RuleEvaluator
class 可以有一个 Tokenizer
对象作为成员。现在,我们可以保持与上面相同的测试,除了我们正在测试 Tokenizer
class 而不是 RuleEvaluator
class.
在 UML 中可能是这样的:
请注意,这种新设计增加了模块化,因此您可能会在系统的其他部分重复使用这些 classes(在您不能这样做之前,私有方法根据定义是不可重复使用的)。这是打破 RuleEvaluator 的主要优势,同时增加 understandability/locality.
测试看起来非常相似,除了这次它实际上会编译,因为 GetNextToken()
方法现在 public 在 Tokenizer
class:
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
只是不要测试私有函数。有时它们不值得测试,因为它们将通过 public 接口进行测试。很多时候我看到的是看起来 非常 相似的测试,但测试两个不同的 functions/methods。最终发生的事情是,当需求发生变化时(他们总是这样),你现在有 2 个失败的测试而不是 1 个。如果你真的测试了你所有的私有方法,你可能会有更多的 10 个失败的测试而不是 1 个。 简而言之,测试原本可以通过 public 接口测试的私有函数(通过使用 FRIEND_TEST
或使它们成为 public)会导致测试重复 。你真的不想要这个,因为没有什么比你的测试套件让你慢下来更有害的了。它应该减少开发时间并降低维护成本!如果您测试通过 public 接口以其他方式测试的私有方法,则测试套件很可能会做相反的事情,并主动增加维护成本并增加开发时间。当你做一个私有函数public,或者如果你使用类似FRIEND_TEST
的东西,你通常会后悔的。
考虑 Tokenizer
class 的以下可能实现:
假设 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");
}
好吧,现在让我们假设需求发生变化,您现在需要用“,”而不是 space 来解析。自然地,您会期望一个测试失败,但是当您测试私有函数时痛苦会增加。 IMO,google 测试不应允许 FRIEND_TEST。这几乎从来都不是您想要做的。 Michael Feathers 将 FRIEND_TEST
之类的东西称为 "groping tool",因为它试图触摸别人的私处。
我建议尽可能避免选项 1 和 2,因为它通常会导致 "test duplication",因此,当需求发生变化时,比必要的更多的测试将会中断。将它们作为最后的手段使用。 选项 1 和 2 是此时此地 "test private methods" 的最快方法(就像实施最快一样),但从长远来看它们确实会损害生产力 运行.
PIMPL 也很有意义,但它仍然允许进行一些非常糟糕的设计。小心点。
我推荐选项 4(重构为更小的可测试组件)作为正确的起点,但有时您真正想要的是选项 5(通过 public界面)。
P.S。这是关于冰山 classes 的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU
P.S.S.至于软件中的一切,答案是它取决于。没有一种尺寸适合所有人。解决您问题的选项将取决于您的具体情况。