我应该测试私有方法还是只测试公共方法?

411

我已经阅读了有关如何测试私有方法的这篇帖子。通常情况下,我不会测试私有方法,因为我一直认为仅测试从对象外部调用的公共方法会更快。您会测试私有方法吗?我应该始终测试它们吗?


1
“我应该测试私有助手吗?”是的。“我应该直接测试私有助手吗?”这取决于情况,通常如果您可以通过公共接口轻松测试它们,为什么要直接测试它们呢?如果通过公共接口测试所有助手的各个方面变得复杂,那么该组件是否已经超出了作为单个单位存在的时限? - Mihai Danila
31个回答

394

我不对私有方法进行单元测试。私有方法是一个应该对类的使用者隐藏的实现细节。测试私有方法会破坏封装性。

如果我发现这个私有方法很庞大、复杂或者非常重要以至于需要自己的测试,我会把它放到另一个类中并在那里将其公开(方法对象)。然后,我可以轻松地测试之前被私有化但现在已经存在于自己类中的公共方法。


111
我不同意。理想情况下,在编写函数之前,您应该先编写一个快速测试。考虑典型的输入以及输出结果将是什么。编写测试(不应花费您超过几秒钟的时间),并编写代码,直到它通过测试为止。在私有方法中也没有放弃这种工作风格的理由。 - Frank
294
说私有方法不需要测试就好像说只要汽车开得好,引擎里面是什么并不重要。但是,如果知道里面的某些电缆开始松动了,即使用户没有察觉到任何问题,也会感到很放心吧?当然,您可以将所有内容都公开,但这有什么意义呢?您总是需要一些私有方法。 - Frank
46
“私有方法是一个实现细节,应该对类的用户隐藏。”但是,测试用例真的和“常规”(运行时)用户处于同一侧吗? ;) - mlvljr
52
将想要测试的任何内容提取到另一个类中的危险在于,您可能会过度设计产品并拥有数百万个永远不会被重复使用的可重用组件。 - occulus
64
将一段代码比作汽车是错误的,代码不会随着时间 '变差',它是 永恒 的。如果你仅通过测试公共接口来确定其是否 '看起来正常',则你对公共代码的测试是不充分的。在这种情况下,无论你如何努力,单独测试私有方法也无法使整个测试完成。要全面地集中精力测试你的公共代码,利用代码内部工作的知识来创建正确的场景。 - rustyx
显示剩余35条评论

340

测试的目的是什么?

迄今为止,大多数回答都表示私有方法是实现细节,只要公共接口经过充分测试并正常工作,就不会(或者至少不应该)有影响。如果您进行测试的唯一目的是确保公共接口工作,那么这是完全正确的。

就我个人而言,我的主要代码测试用途是确保未来的代码更改不会导致问题,并且在出现问题时帮助我的调试工作。我发现像测试公共接口一样彻底地测试私有方法(如果不是更加彻底!)可以进一步实现这个目的。

考虑一下:您有一个公共方法A,它调用私有方法B。A和B都使用方法C。C被更改(也许是您自己,也许是供应商),导致A开始失败其测试。即使它是私有的,拥有B的测试是否有用,这样您就知道问题是在A对C的使用、B对C的使用还是两者都存在问题。

测试私有方法还可以在公共接口的测试覆盖率不完整的情况下增加价值。虽然我们通常希望避免这种情况,但单元测试的效率取决于测试是否能够发现错误以及这些测试的开发和维护成本。在某些情况下,100%的测试覆盖率的好处可能被认为不足以证明这些测试的成本,从而产生公共接口测试覆盖的空白。在这种情况下,对私有方法进行有针对性的测试可以是代码库中非常有效的补充。


82
问题在于这些“未来的代码更改”通常意味着重构某个类的内部工作方式。这种情况经常发生,因此编写测试会对重构带来阻碍。 - Tim Frey
42
此外,如果您不断更改单元测试,则测试失去了一致性,甚至可能会在单元测试本身中引入错误。 - 17 of 26
9
如果测试和实现同步修改(似乎应该如此),那么就会遇到更少的问题。 - mlvljr
14
@Sauronlord,测试私有方法的原因是,如果只测试公共方法,当测试失败时我们无法直接知道失败的根本原因。它可能在testDoSomething()testDoSomethingPrivate()中的任意一个。这会使测试变得不太有价值。这里有更多关于测试私有方法的原因:http://stackoverflow.com/questions/34571/how-to-test-a-class-that-has-private-methods-fields-or-inner-classes#comment9624053_34571。 - Pacerier
4
在测试代码和拥有持续自动化测试过程之间也存在区别。显然,您应确保您的私有方法有效,但您不应该编写与私有方法耦合的测试,因为它并不属于软件的使用情况。 - Didier A.
显示剩余8条评论

176

我倾向于遵循Dave Thomas和Andy Hunt在他们的书Pragmatic Unit Testing中的建议:

一般情况下,出于测试目的,您不想破坏任何封装(正如妈妈曾经说过的“不要暴露你的私人部位!”)。大多数时候,您应该能够通过执行其公共方法来测试类。如果有重要功能隐藏在私有或受保护的访问后面,这可能是另一个正在努力挣扎的类的警告标志。

但是有时我会忍不住测试私有方法,因为它给了我一种安心的感觉,即我正在构建一个完全健壮的程序。


13
建议禁用针对私有方法的单元测试。它们会增加代码耦合,给未来重构工作带来负担,甚至有时会妨碍功能添加或修改。在实现它们时编写测试是有益的,因为这是您自动断言实现有效性的一种方式,但保留测试作为回归测试并没有好处。 - Didier A.

105

我不喜欢测试私有功能的原因有几个,主要如下:

  1. 通常当你想要测试一个类的私有方法时,这是一种设计上的问题。
  2. 你可以通过公共接口进行测试(这是你想要测试它们的方式,因为客户端将调用/使用它们)。你可能会看到所有私有方法都通过了测试,从而产生虚假的安全感。但通过公共接口测试私有函数上的边界情况会更好/更安全。
  3. 通过测试私有方法,你面临着严重的测试重复风险(测试看起来/感觉非常相似)。当需求改变时,比必要的更多的测试将会失败。这也可能使你难以重构,因为你的测试套件...这是最后的讽刺,因为测试套件在帮助你安全地重新设计和重构!

我将使用具体的例子来解释每个原因。其实2)和3)有些密切相关,所以他们的例子是类似的,尽管我认为它们是你不应该测试私有方法的分别原因。

当然,有时测试私有方法是适当的,但重要的是要意识到上述缺点。稍后我会详细介绍。

我还会在最后详细介绍为什么TDD不是测试私有方法的有效借口。

通过重构摆脱糟糕的设计

我看到最常见的(反)模式之一是Michael Feathers所称的“冰山”类(如果你不知道Michael Feathers是谁,去购买/阅读他的书“遗留代码有效工作”。如果你是专业软件工程师/开发人员,他是值得了解的人)。还有其他(反)模式会导致这个问题出现,但这是我遇到的最常见的一个。 “冰山”类只有一个公共方法,其余都是私有的(这就是为什么测试私有方法很诱人的原因)。它被称为“冰山”类,因为通常有一个孤立的公共方法,但其余功能以私有方法的形式隐藏在水下。 它可能看起来像这样:

Rule Evaluator

例如,您可能想通过连续调用GetNextToken()并查看其返回预期结果来测试它。像这样的函数确实需要进行测试:该行为并不是微不足道的,特别是如果您的标记化规则很复杂。假设它并不是那么复杂,我们只想捆绑由空格分隔的标记。因此,您编写了一个测试,也许看起来像这样(一些语言无关的伪代码,希望思路清晰):
TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    re = RuleEvaluator(input_string);

    ASSERT re.GetNextToken() IS "1";
    ASSERT re.GetNextToken() IS "2";
    ASSERT re.GetNextToken() IS "test";
    ASSERT re.GetNextToken() IS "bar";
    ASSERT re.HasMoreTokens() IS FALSE;
}

好的,这看起来非常不错。我们需要确保在进行更改时保持这种行为。但是GetNextToken()是一个私有函数!所以我们不能像这样测试它,因为它甚至不会编译(假设我们使用一些实际执行公共/私有的语言,而不是像Python这样的某些脚本语言)。但是,如果将RuleEvaluator类更改为遵循单一职责原则(Single Responsibility Principle),怎么样呢?例如,我们似乎将解析器、标记化器和评估器挤入了一个类中。仅将这些责任分离开来是否更好?此外,如果创建一个Tokenizer类,则其公共方法将为HasMoreTokens()GetNextTokens()RuleEvaluator类可以将Tokenizer对象作为成员。现在,我们可以保持与上面相同的测试,只是我们正在测试Tokenizer类而不是RuleEvaluator类。

以下是UML中的示例:

Rule Evaluator Refactored

请注意,这种新设计增加了模块化,因此您可以在系统的其他部分重新使用这些类(在此之前不行,私有方法根据定义不可重用)。这是将RuleEvaluator拆分的主要优点,以及增加了可理解性/局部性。
测试看起来非常相似,只是这一次它实际上会编译,因为GetNextToken()方法现在在Tokenizer类上是公共的:
TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS FALSE;
}

通过公共接口测试私有组件并避免测试重复
即使您认为无法将问题分解为更少的模块化组件(如果您尝试一下,95%的时间都可以做到),也可以通过公共接口测试私有函数。许多时候,私有成员不值得测试,因为它们将通过公共接口进行测试。很多时候,我看到的是非常相似的测试,但测试了两个不同的函数/方法。结果是,当要求发生变化(它们总是会发生变化)时,现在有两个损坏的测试,而不是一个。如果您真的测试了所有私有方法,您可能会有10个以上损坏的测试,而不是1个。简而言之,测试本应通过公共接口测试的私有函数(使用FRIEND_TEST或使其公共或使用反射)可能会导致测试重复。您真的不希望这样,因为没有什么比测试套件减慢您的速度更糟糕的了。它应该缩短开发时间并降低维护成本!如果测试通过公共接口测试的私有方法,则测试套件可能会产生相反的效果,并积极增加维护成本和开发时间。当您将私有函数公开或者使用类似于FRIEND_TEST和/或反射的东西时,通常最终会后悔的。
考虑以下可能的Tokenizer类实现:

enter image description here

假设SplitUpByDelimiter()负责返回一个数组,使得该数组中的每个元素都是令牌。此外,假设GetNextToken()只是该向量上的一个迭代器。因此,您的公共测试可能如下所示:

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS false;
}

让我们假设我们有一个被 Michael Feather 称为“ groping tool”的工具。这是一种让您触摸他人私处的工具。例如,googletest 的 FRIEND_TEST 或语言支持的反射机制。

TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);
    result_array = tokenizer.SplitUpByDelimiter(" ");

    ASSERT result.size() IS 4;
    ASSERT result[0] IS "1";
    ASSERT result[1] IS "2";
    ASSERT result[2] IS "test";
    ASSERT result[3] IS "bar";
}

好的,现在假设需求发生变化,分词变得更加复杂。您决定简单的字符串分隔符不足以胜任,需要一个Delimiter类来处理这个任务。自然地,您会期望一个测试失败,但当您测试私有函数时,这种痛苦会增加。

何时测试私有方法是合适的?

在软件中没有“一刀切”的规定。有时候打破规则是可以的(实际上是理想的)。我强烈建议您尽可能不要测试私有功能。以下是我认为可以测试私有功能的两种情况:

  1. 我曾经广泛地使用遗留系统(这也是我为什么是Michael Feathers的粉丝),我可以肯定地说,有时候只测试私有功能是最安全的做法。它对于将"特性测试"纳入基线尤其有帮助。

  2. 您很匆忙,必须尽快完成任务。从长远来看,您不想测试私有方法。但是我会说,要解决设计问题通常需要一些时间进行重构。有时候你必须在一周内发货。没关系: 如果您认为使用 groping 工具测试私有方法是最快和最可靠的方法来完成工作,请执行快速而肮脏的操作并进行测试。但请理解,您所做的事情从长远来看是次优的,请考虑回过头来(或者如果它被遗忘了,但您后来看到了它,请修复它)。

可能还有其他情况是可以的。如果您认为这样做没问题,并且有充分的理由,请去做吧。没有人会阻止你。只需注意潜在的成本。

TDD的借口

顺带一提,我真的不喜欢有人用TDD作为测试私有方法的借口。我实践TDD,我不认为TDD会强制你这样做。您可以先为公共接口编写测试,然后编写代码以满足该接口。有时我会为公共接口编写一个测试,并通过编写一个或两个较小的私有方法来满足它(但我不直接测试私有方法,但我知道它们有效,否则我的公共测试将失败)。如果我需要测试该私有方法的边缘情况,我将编写大量测试,通过我的公共接口来测试它们。如果您无法找出如何触发边缘情况,则这是您需要将其重构为具有自己的公共方法的小组件的强烈信号。这表明您的私有函数正在做太多事情,超出了类的范围

有时候我会写一个测试,但发现它太大了,难以一口吃下,所以我会想:“嗯,等我有更多的 API 可以使用时,我再回来测试这个”(我会注释掉并记在脑后)。这就是我遇到过的许多开发人员开始为他们的私有功能编写测试,并以 TDD 为替罪羊的地方。他们说:“哦,我需要另一个测试,但为了编写那个测试,我需要这些私有方法。因此,既然我不能编写任何生产代码而不编写测试,我需要为私有方法编写测试。” 但他们真正需要做的是将其重构为更小和可重用的组件,而不是向当前类添加/测试许多私有方法。
注意:
我之前回答过一个类似的问题使用 GoogleTest 测试私有方法。我主要修改了那个答案,使其更具语言普适性。
P.S. 这里是 Michael Feathers 关于冰山类和 groping 工具的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU

4
我对将“您可以在系统的其他部分重新使用这些类”列为优点的问题是,有时我标记函数为私有的原因是因为我不希望它被系统的其他部分使用。这是一种语言特定的问题:理想情况下,这应该是私有于“模块”,但如果语言不支持(例如PHP),我的类表示模块而不是单元:私有方法是具有自己合同的可重用代码,但必须仅在该类内重用。 - IMSoP
我理解你的意思,但我喜欢 Python 社区处理这个问题的方式。如果你用一个前导下划线 _ 命名所谓的“私有”成员,它就会发出信号:“嘿,这是‘私有’的。你可以使用它,但要完全透明,它不是为了重复使用而设计的,只有在你真正知道自己在做什么时才能使用它。” 你可以在任何语言中采取同样的方法:将这些成员公开,但用前导下划线 _ 标记它们。或者也许这些函数确实应该是私有的,并且只通过公共接口进行测试(详见答案)。这是具体情况具体分析,没有通用规则。 - Matt Messersmith
2
我真的很喜欢这个答案。 - Hiro

67
我感觉有必要测试私有函数,因为我越来越多地遵循我们项目中最新的QA建议之一:

每个函数中不超过10个圈复杂度

现在该策略的执行会导致我的很多非常大的公共函数被分解成更多聚焦、命名更好的私有函数。
公共函数仍然存在(当然),但本质上被减少为调用所有那些私有的 "子函数"。

这其实很酷,因为调用栈现在更容易阅读了(与一个大函数内的 bug 不同,我现在有一个帮助我理解“如何到达那里”的先前函数名称的子子函数中的 bug)。

然而,现在似乎更容易直接对这些私有函数进行单元测试,并将大型公共函数的测试留给某种“集成”测试来处理场景。

只是我的看法。


2
回应@jop,我不觉得有必要将那些私有函数(由于一个大的过于复杂的公共函数被分割而创建)导出到另一个类中。我喜欢它们仍然与公共函数紧密耦合在同一个类中。但是仍然需要进行单元测试。 - VonC
3
我的经验是,那些私有方法只是被公共方法重复使用的实用方法。有时将原始类分成两个(或三个)更加内聚的类会更方便,将那些私有方法变为其自己类中的公共方法,从而可以进行测试。 - jop
7
不是的,在我的情况下,这些新的私有函数确实是公共函数所代表的更大算法的一部分。该函数被分成了较小的部分,它们不是工具函数,而是一个更大过程中的步骤。因此需要对它们进行单元测试(而不是一次性对整个算法进行单元测试)。 - VonC
对于那些对圈复杂度感兴趣的人,我在这个主题上添加了一个问题:https://dev59.com/d3VD5IYBdhLWcg3wDXF3 - VonC
糟糕,由于标题中的拼写错误,问题的URL发生了变化! https://dev59.com/d3VD5IYBdhLWcg3wDXF3 - VonC

54

我确实测试私有函数,因为虽然它们被公共方法测试过了,但在测试驱动开发(TDD)中,测试应用程序的最小部分是很好的实践。但是,在测试单元类中无法访问私有函数。以下是我们测试私有方法的方法。

为什么要使用私有方法?

私有函数主要存在于我们的类中,因为我们希望在公共方法中创建易读的代码。 我们不希望该类的用户直接调用这些方法,而是通过我们的公共方法间接调用它们。此外,我们也不希望在扩展类时更改它们的行为(在受保护的情况下),因此设置为私有。

在编码时,我们采用测试驱动设计(TDD)。这意味着有时我们会遇到需要测试但是是私有功能的情况。PhpUnit中无法测试私有函数,因为我们不能在测试类中访问它们(它们是私有的)。

我们认为有三种解决方案:

1. 通过公共方法测试私有函数

优点

  • 简单明了的单元测试(不需要任何“hack”)

缺点

  • 程序员需要理解公共方法,而他只想测试私有方法
  • 您没有测试应用程序的最小可测试部分

2. 如果私有函数如此重要,则创建一个新的独立类来处理它可能更好

优点

  • 您可以将其重构为一个新类,因为如果它很重要,其他类可能也需要使用它
  • 可测试的单元现在是一个公共方法,因此可以测试

缺点

  • 如果不需要,仅由包含该方法的类使用,则不应创建类
  • 可能会因为添加额外开销而导致性能损失

3. 更改访问修饰符为(final)protected

优点

  • 您正在测试应用程序的最小可测试部分。使用final protected时,该函数将不可覆盖(就像私有函数一样)
  • 没有性能损失
  • 没有额外的开销

缺点

  • 将私有访问更改为受保护,这意味着它可以被子类访问
  • 仍需要在测试类中使用Mock类

示例

class Detective {
  public function investigate() {}
  private function sleepWithSuspect($suspect) {}
}
Altered version:
class Detective {
  public function investigate() {}
  final protected function sleepWithSuspect($suspect) {}
}
In Test class:
class Mock_Detective extends Detective {

  public test_sleepWithSuspect($suspect) 
  {
    //this is now accessible, but still not overridable!
    $this->sleepWithSuspect($suspect);
  }
}

现在我们的测试单元可以调用test_sleepWithSuspect来测试我们之前的私有函数。


eddy147,我真的很喜欢通过模拟测试受保护方法的概念。谢谢!!! - Theodore R. Smith
17
我想指出TDD的原始描述中,在单元测试中,“单元”是“类”,而不是一个方法/函数。因此,当你提到“测试应用程序的最小部分”时,将最小可测试部分称为方法是错误的。如果你使用这种逻辑,那么你与其说是在讨论整个代码块,还不如说是在讨论单行代码。 - Matt Quigley
@Matt 一个工作单元可以指向一个类,也可以指向一个单独的方法。 - eddy147
7
@eddy147 单元测试起源于测试驱动开发中,其中单元被定义为一个类。由于互联网的发展,其语义已经扩展到涵盖了很多东西(比如:询问两个人关于单元测试和集成测试之间的区别,你会得到七种不同的答案)。TDD旨在通过遵循SOLID原则编写软件,包括单一职责,即一个类应该具有单一职责并且不能具有高循环复杂度。在TDD中,您同时编写类和单元测试。私有方法是封装的,没有相应的单元测试。 - Matt Quigley
1
当我们编写代码时,我们使用测试驱动开发(TDD)。这意味着有时我们会遇到一些私有功能并希望进行测试。我强烈反对这种说法,请看下面的回答以获取更多细节。TDD并不意味着你必须测试私有方法。你可以选择测试私有方法:这是你的选择,但这并不是TDD让你这样做的。 - Matt Messersmith

26

我认为最好只测试对象的公共接口。从外部世界的角度来看,只有公共接口的行为才是重要的,这也是你应该针对其进行单元测试的原因。

一旦你已经编写了一些可靠的对象单元测试,你不希望因为接口后面的实现更改而回去改变那些测试。在这种情况下,你破坏了单元测试的一致性。


22
如果你的私有方法不是通过调用公共方法来测试的,那么它在做什么呢?我指的是私有方法而不是受保护的方法或友元函数。

3
谢谢。这是一个令人惊讶的被低估的评论,尤其在近八年后,它仍然非常相关。 - Sauronlord
1
用同样的推理,人们可以主张只从用户界面(系统级测试)测试软件,因为软件中的每个功能都会以某种方式从那里执行。 - Dirk Herrmann

18

如果私有方法被良好定义(即具有可测试的函数且不打算随时间改变),那么是的。我会在有意义的情况下测试一切可测试的内容。

例如,加密库可能会使用一个私有方法隐藏其进行块加密并每次只加密8个字节的事实。我会为此编写单元测试 - 尽管它被隐藏,但它不打算改变,如果它确实出现了问题(例如由于未来性能提升引起),那么我想知道是私有函数出错了,而不仅仅是公共函数出错了。

这样可以加快后续调试速度。

-Adam


1
在这种情况下,将私有方法移动到另一个类中,然后将其设置为公共或公共静态,这样做不是更合理吗? - Tim Frey
如果您不测试私有成员函数,而公共接口的测试失败,那么您将得到与“某些东西已损坏”等同的结果,而没有任何关于该“某些东西”的想法。 +1 - Olumide

12

如果您正在进行测试驱动开发 (TDD),您将会对私有方法进行测试。


2
在重构时,您将提取私有方法 http://agiletips.blogspot.com/2008/11/testing-private-methods-tdd-and-test.html - Josh Johnson
5
不正确,您应该测试公共方法,一旦测试通过,则在“清理”步骤中将公共方法中的代码提取到私有方法中。在我看来,测试私有方法是一个不好的想法,因为这使得更改实现变得更加困难(如果有一天您想要改变做某事的方式,您应该能够更改它并运行所有测试,如果您的新做事方式是正确的,那么测试结果应该是通过的,我不想为此更改所有的私有测试)。 - Tesseract
1
@Tesseract,如果我可以多次点赞你的评论,我一定会这样做。 "...你应该能够更改它并运行所有测试,如果你的新方法是正确的,它们应该通过" 这就是单元测试的主要好处之一。它们使您能够自信地进行重构。您可以完全更改类的内部私有工作方式,并且(无需重写所有单元测试)确信您没有破坏任何内容,因为所有(现有的)单元测试(在您的公共接口上)仍然通过。 - Lee
不同意,请看下面的答案。 - Matt Messersmith

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接