ChatGPT解决这个技术问题 Extra ChatGPT

我应该测试私有方法还是只测试公共方法? [关闭]

关闭。这个问题是基于意见的。它目前不接受答案。想改进这个问题?更新问题,以便可以通过编辑这篇文章用事实和引用来回答它。 3年前关闭。改进这个问题

我已阅读有关如何测试私有方法的 this post。我通常不测试它们,因为我一直认为只测试将从对象外部调用的公共方法会更快。你测试私有方法吗?我应该总是测试它们吗?

“我应该测试私人佣工吗?”是的。 “我应该直接测试私人佣工吗?”这取决于,通常如果您可以通过公共接口轻松测试它们,为什么要直接测试它们?如果通过公共接口测试助手的所有方面变得复杂,那么组件是否已经超过了它作为单个单元的存在?

L
Luke Girvin

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

如果我发现私有方法非常庞大、复杂或重要到需要它自己的测试,我只需将它放在另一个类中并在那里公开(Method Object)。然后我可以轻松地测试现在存在于自己的类中的以前私有但现在公共的方法。


我不同意。理想情况下,您在开始编写函数之前编写一个快速测试。想想典型的输入和输出将是什么。编写测试(不应该花费你超过几秒钟的时间)和代码,直到它得到正确的测试。没有理由放弃私有方法的这种工作方式。
说私有方法不需要测试就像说汽车只要开得好就可以,引擎盖下的东西并不重要。但是如果知道里面的一些电缆开始松动,这不是很好吗——即使用户没有注意到任何东西?当然,您可以公开所有内容,但有什么意义呢?你总是需要一些私有方法。
“私有方法是一个实现细节,应该对类的用户隐藏。”但是测试真的与“常规”(运行时)用户在类界面的同一侧吗? ;)
将任何你想测试的东西放到另一个类中的危险在于,你最终可能会因过度设计你的产品和拥有一百万个永远不会被重用的可重用组件而产生开销。
将一段代码比作汽车是错误的;代码不会随着时间“变坏”,它是永恒的。如果您对公共接口的测试只是确定它“看起来不错”,那么您对公共代码的测试是不够的。在这种情况下,无论您多么努力,单独测试私有方法都不会使整个测试完成。集中精力全面测试你的公共代码,利用代码内部工作的知识来创建正确的场景。
D
Dave Sherohman

测试的目的是什么?

到目前为止,大多数答案都是说私有方法是实现细节,只要公共接口经过良好测试和工作,它们就不会(或至少不应该)重要。如果您测试的唯一目的是保证公共接口正常工作,那绝对正确。

就个人而言,我对代码测试的主要用途是确保将来的代码更改不会引起问题,并在出现问题时帮助我的调试工作。我发现测试私有方法和公共接口一样彻底(如果不是更多的话!)进一步实现了这一目的。

考虑:您有一个调用私有方法 B 的公共方法 A。A 和 B 都使用方法 C。C 已更改(可能由您更改,也可能由供应商更改),导致 A 开始无法通过测试。对 B 进行测试不是很有用吗,即使它是私有的,以便您知道问题是出在 A 对 C 的使用、B 对 C 的使用,还是两者兼而有之?

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


这里的问题是那些“未来的代码更改”总是意味着重构某些类的内部工作。这种情况经常发生,以至于编写测试会给重构带来障碍。
此外,如果您不断地更改单元测试,那么您在测试中失去了所有一致性,您甚至可能在单元测试本身中创建错误。
@17 如果测试和实现是同步修改的(看起来应该是这样),那么问题就会少得多。
@Pacerier 测试代码和拥有连续的自动化测试过程之间也有区别。您显然应该确保您的私有方法有效,但您不应该让测试将您耦合到私有方法,因为它不是软件用例的一部分。
N
Nate

我倾向于遵循 Dave Thomas 和 Andy Hunt 在他们的《实用单元测试》一书中的建议:

一般来说,你不想为了测试而破坏任何封装(或者就像妈妈过去常说的,“不要暴露你的隐私!”)。大多数时候,您应该能够通过执行其公共方法来测试一个类。如果有重要的功能隐藏在私有或受保护的访问之后,那可能是一个警告信号,表明那里有另一个班级正在努力摆脱。

但有时我无法阻止自己测试私有方法,因为它让我确信我正在构建一个完全健壮的程序。


我建议禁用针对私有方法的单元测试。它们是代码耦合,会给未来的重构工作带来负担,甚至有时会妨碍特性的添加或修改。在实现它们时为它们编写一个测试是很好的,作为一种自动化的方式来断言你的实现是有效的,但是将测试保持为回归是没有好处的。
c
connexo

我不喜欢测试私有功能有几个原因。它们如下(这些是 TLDR 人员的要点):

通常,当您想测试一个类的私有方法时,这是一种设计味道。您可以通过公共接口测试它们(这是您想要测试它们的方式,因为这是客户端调用/使用它们的方式)。通过查看私有方法的所有通过测试的绿灯,您可能会产生错误的安全感。通过公共接口在私有函数上测试边缘案例会更好/更安全。通过测试私有方法,您冒着严重的测试重复(看起来/感觉非常相似的测试)的风险。当需求发生变化时,这会产生重大影响,因为过多的测试会中断。它还可能使您处于由于您的测试套件而难以重构的位置……这是最大的讽刺,因为测试套件可以帮助您安全地重新设计和重构!

我将用一个具体的例子来解释这些。事实证明 2) 和 3) 有一些错综复杂的联系,所以它们的例子是相似的,尽管我认为它们是你不应该测试私有方法的不同原因。

有时测试私有方法是合适的,重要的是要注意上面列出的缺点。稍后我将更详细地讨论它。

我还讨论了为什么 TDD 不是最后测试私有方法的有效借口。

从糟糕的设计中重构你的出路

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

https://i.stack.imgur.com/h0RZH.png

例如,您可能希望通过在字符串上连续调用 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 中的样子:

https://i.stack.imgur.com/g1G9o.png

请注意,这种新设计增加了模块化,因此您可以在系统的其他部分重用这些类(在您不能重用之前,私有方法根据定义是不可重用的)。这是分解 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% 的时间里这样做),您也可以通过以下方式简单地测试私有函数公共接口。很多时候私有成员不值得测试,因为它们将通过公共接口进行测试。很多时候,我看到的测试看起来非常相似,但测试的是两个不同的函数/方法。最终发生的情况是,当需求发生变化时(而且它们总是如此),您现在有 2 个损坏的测试而不是 1 个。如果您真的测试了所有私有方法,您可能会有更多的 10 个损坏的测试而不是 1 个。简而言之,测试本来可以通过公共接口测试的私有函数(通过使用 FRIEND_TEST 或将其公开或使用反射)可能会导致测试重复。你真的不想要这个,因为没有什么比你的测试套件减慢你的速度更伤人的了。它应该减少开发时间并降低维护成本!如果您测试通过公共接口测试的私有方法,测试套件可能会做相反的事情,并积极增加维护成本并增加开发时间。当您将私有函数公开时,或者如果您使用 FRIEND_TEST 和/或反射之类的东西,从长远来看,您通常最终会后悔。

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

https://i.stack.imgur.com/Tbjqy.png

假设 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 所说的探索工具。这是一个可以让您触摸他人私处的工具。一个示例是来自 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 类来处理这项工作。自然地,您会期望一个测试会失败,但是当您测试私有函数时,这种痛苦会增加。

什么时候可以测试私有方法是合适的?

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

我与遗留系统进行了广泛的合作(这就是为什么我是 Michael Feathers 的忠实粉丝),我可以肯定地说,有时只测试私有功能是最安全的。这对于将“表征测试”纳入基线特别有帮助。你很着急,必须在此时此地做尽可能快的事情。从长远来看,您不想测试私有方法。但我会说,通常需要一些时间来重构以解决设计问题。有时您必须在一周内发货。没关系:如果您认为这是完成工作的最快和最可靠的方法,请使用摸索工具快速而肮脏地测试私有方法。但是请理解,从长远来看,您所做的事情并不理想,请考虑重新使用它(或者,如果它被遗忘但您稍后会看到它,请修复它)。

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

TDD 的借口

顺便说一句,我真的不喜欢人们使用 TDD 作为测试私有方法的借口。我练习 TDD,我不认为 TDD 强迫你这样做。您可以先编写测试(针对您的公共接口),然后编写代码来满足该接口。有时我为公共接口编写一个测试,我也会通过编写一两个较小的私有方法来满足它(但我不直接测试私有方法,但我知道它们可以工作,否则我的公共测试会失败)。如果我需要测试该私有方法的边缘案例,我将编写一大堆测试,这些测试将通过我的公共接口来处理它们。如果您不知道如何应对边缘情况,这是一个强烈的信号,您需要将其重构为小组件,每个组件都有自己的公共方法。这表明您的私人功能做得太多,超出了课程的范围。

另外,有时我发现我写的测试现在太难咀嚼了,所以我想“嗯,当我有更多的 API 可以使用时,我会回到那个测试”(我'将其注释掉并保留在我的脑海中)。这就是我遇到的许多开发人员开始为他们的私有功能编写测试的地方,他们使用 TDD 作为替罪羊。他们说“哦,好吧,我需要一些其他测试,但是为了编写那个测试,我需要这些私有方法。因此,由于我不能不编写测试就编写任何生产代码,我需要编写一个测试用于私有方法。”但是他们真正需要做的是重构更小的和可重用的组件,而不是向他们当前的类添加/测试一堆私有方法。

笔记:

不久前,我回答了一个关于 testing private methods using GoogleTest 的类似问题。我在这里大部分修改了这个答案,使其与语言无关。

PS 以下是 Michael Feathers 关于冰山课程和探索工具的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU


我将“您可能会在系统的其他部分重用这些类”列为优势时遇到的问题是,有时我将函数标记为私有的原因是因为我不希望它被其他部分使用系统。这是一个特定于语言的问题:理想情况下,这对于“模块”来说是私有的,但是如果语言不支持它(例如 PHP),我的类代表模块,而不是单元:私有方法是可重用的代码与他们自己的合同,但只能在该类中重用。
我明白你在说什么,但我喜欢 Python 社区处理这个问题的方式。如果您用前导 _ 命名有问题的“私人”成员,则表示“嘿,这是 '私人'。你可以使用它,但要充分披露,它不是为重复使用而设计的,你应该只使用它如果你真的知道你在做什么”。您可以在任何语言中采用相同的方法:将这些成员公开,但用前导 _ 标记它们。或者也许这些功能真的应该是私有的,并且只是通过公共接口进行测试(有关更多详细信息,请参阅答案)。视情况而定,没有一般规则
我真的很喜欢这个答案。
V
VonC

随着我在项目中越来越多地遵循我们最新的 QA 建议之一,我觉得有必要测试私有函数:

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

现在执行这项政策的副作用是,我的许多非常大的公共功能被划分为许多更集中、更好命名的私人功能。公共功能仍然存在(当然),但基本上被简化为调用所有那些私有“子功能”

这实际上很酷,因为调用堆栈现在更容易阅读(而不是大函数中的错误,我在子子函数中有一个错误,其中调用堆栈中先前函数的名称可以帮助我理解'我是如何到达那里')

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

只是我的2美分。


为了对@jop 做出反应,我觉得没有必要将那些私有函数(由于分割一个大的太圈复杂的公共函数而创建)导出到另一个类中。我喜欢让它们在同一个类中仍然与公共函数紧密耦合。但仍然经过单元测试。
我的经验是,那些私有方法只是被那些公共方法重用的实用方法。有时将原始类拆分为两个(或三个)更具凝聚力的类更方便,使这些私有方法在它们自己的类中公开,因此可测试。
不,就我而言,那些新的私有函数实际上是由公共函数表示的更大算法的一部分。该功能分为较小的部分,这些部分不是实用程序,而是更大过程的步骤。因此需要对它们进行单元测试(而不是一次对整个算法进行单元测试)
对于那些对圈复杂度感兴趣的人,我添加了一个关于该主题的问题:stackoverflow.com/questions/105852/…
糟糕,由于标题中的拼写错误,问题的网址已更改! stackoverflow.com/questions/105852/…
C
Chacko Mathew

是的,我确实测试了私有函数,因为尽管它们是由您的公共方法测试的,但是在 TDD(测试驱动设计)中测试应用程序的最小部分是很好的。但是当您在测试单元类中时,无法访问私有函数。这是我们为测试私有方法所做的工作。

为什么我们有私有方法?

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

当我们编码时,我们使用测试驱动设计(TDD)。这意味着有时我们会偶然发现一个私有功能并想要测试。私有函数在 phpUnit 中不可测试,因为我们无法在 Test 类中访问它们(它们是私有的)。

我们认为这里有 3 个解决方案:

1.您可以通过您的公共方法测试您的私人

优点

简单的单元测试(不需要“黑客”)

缺点

程序员需要了解公共方法,而他只想测试私有方法

您没有测试应用程序的最小可测试部分

2. 如果 private 如此重要,那么为它创建一个新的单独类可能是一种代码味道

优点

您可以将其重构为一个新类,因为如果它很重要,其他类可能也需要它

可测试单元现在是公共方法,因此可测试

缺点

如果不需要,您不想创建一个类,并且只由方法来自的类使用

由于额外开销而导致的潜在性能损失

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,我真的很喜欢通过模拟测试受保护方法的概念。谢谢!!!!
我只想指出,在 TDD 的原始描述中,在单元测试中,单元是类,而不是方法/函数。因此,当您提到“测试应用程序的最小部分”时,将最小的可测试部分称为方法是错误的。如果您使用该逻辑,您可能会说一行代码而不是整个代码块。
@Matt 一个工作单元可以指向一个类,也可以指向一个方法。
@eddy147 单元测试来自测试驱动开发,其中单元被定义为一个类。正如互联网所发生的那样,语义已经扩展到了很多东西(即问两个人单元测试和集成测试之间的区别是什么,你会得到 7 个答案)。 TDD 旨在作为一种使用 SOLID 原则编写软件的方法,包括单一职责,其中一个类具有单一职责并且不应该具有很高的循环复杂性。在 TDD 中,您可以一起编写类和测试,这两个单元都是单元。封装的私有方法没有相应的单元测试。
“当我们编写代码时,我们使用测试驱动设计 (TDD)。这意味着有时我们会偶然发现一个私有功能并想要进行测试。”我强烈不同意这种说法,请参阅下面的答案以获取更多详细信息。 TDD 并不意味着您被迫测试私有方法。您可以选择测试私有方法:这是您的选择,但不是 TDD 让您这样做。
1
17 of 26

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

一旦你为一个对象编写了一些可靠的单元测试,你就不想仅仅因为接口背后的实现发生了变化而返回并更改这些测试。在这种情况下,您破坏了单元测试的一致性。


c
chrissie1

如果您的私有方法没有通过调用您的公共方法进行测试,那么它在做什么?我说的是私人的,不受保护或朋友。


谢谢你。这是一个出人意料地被低估的评论,尤其是它仍然具有相关性,即使在它写成近 8 年后也是如此。
出于同样的原因,人们可能会争辩说只从用户界面测试软件(系统级测试),因为软件中的每个功能都会以某种方式从那里执行。
A
Adam Davis

如果私有方法定义明确(即,它具有可测试的功能并且不会随时间改变),那么可以。我在有意义的地方测试所有可测试的东西。

例如,加密库可能会隐藏它使用一次仅加密 8 个字节的私有方法执行块加密的事实。我会为此编写一个单元测试——它并不意味着改变,即使它是隐藏的,如果它确实坏了(例如,由于未来的性能增强),那么我想知道它是私有函数坏了,而不仅仅是其中一项公共职能遭到破坏。

它可以加速以后的调试。

-亚当


在这种情况下,将私有方法移动到另一个类,然后将其设为公共或公共静态是否有意义?
+1如果您不测试您的私有成员函数并且您对公共接口的测试失败,那么您将得到的结果相当于某些东西被破坏了,不知道那是什么东西。
m
maxbog

我不是这个领域的专家,但单元测试应该测试行为,而不是实现。私有方法是严格实现的一部分,因此恕我直言,不应测试。


然后在哪里测试实施?如果某些功能使用缓存,那么这是否是一个实现细节并且缓存没有经过测试?
J
Jader Dias

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


您将在重构 agiletips.blogspot.com/2008/11/… 时提取私有方法
不正确,您测试您的公共方法,一旦测试通过,您将在“清理”步骤期间将公共方法中的代码提取到私有方法中。测试私有方法是一个坏主意,因为它使改变实现方式变得更加困难(如果有一天你想改变你做某事的方式,你应该能够改变它并运行你所有的测试,如果你的新方法是事情是正确的,他们应该通过,我不想为此更改我所有的私人测试)。
@Tesseract,如果我能不止一次地支持你的评论的话。 “......你应该能够改变它并运行你所有的测试,如果你的新方法是正确的,他们应该通过”这是单元测试的主要好处之一。它们使您能够自信地重构。您可以完全改变类的内部私有工作,并且(无需重写所有单元测试)确信您没有破坏任何东西,因为您的所有(现有)单元测试(在您的公共接口上)仍然通过。
不同意,看我下面的回答
T
Tom Carr

我们通过推理测试私有方法,我的意思是我们寻找至少 95% 的总类测试覆盖率,但我们的测试只调用公共或内部方法。为了获得覆盖,我们需要根据可能发生的不同场景多次调用公共/内部人员。这使我们的测试更加专注于他们正在测试的代码的目的。

Trumpi 对您链接的帖子的回答是最好的。


s
scubabbl

我相信单元测试是为了测试公共方法。您的公共方法使用您的私有方法,因此间接地它们也得到了测试。


d
dkinzer

我一直在为这个问题烦恼一段时间,尤其是在尝试 TDD 时。

我遇到过两篇文章,我认为它们在 TDD 的情况下已经足够彻底地解决了这个问题。

测试私有方法、TDD 和测试驱动重构 测试驱动开发不是测试

总之:

当使用测试驱动开发(设计)技术时,私有方法应该只出现在已经工作和测试代码的重构过程中。

根据过程的本质,从经过彻底测试的功能中提取的任何简单实现功能都将进行自我测试(即间接测试覆盖率)。

对我来说,在编码的开始部分似乎很清楚,大多数方法将是更高级别的函数,因为它们封装/描述了设计。

因此,这些方法将是公开的,并且测试它们将很容易。

一旦一切正常,私有方法将在稍后出现,并且为了可读性和清洁性我们正在重构。


A
Adron

如上所述,“如果你不测试你的私有方法,你怎么知道它们不会破坏?”

这是一个重大问题。单元测试的一大要点是尽快知道在哪里、何时以及如何发生故障。从而减少了大量的开发和质量保证工作。如果所有被测试的都是公众,那么你就没有诚实的覆盖和描述类的内部结构。

我发现最好的方法之一就是简单地将测试引用添加到项目中,并将测试放在与私有方法平行的类中。放入适当的构建逻辑,以便测试不会构建到最终项目中。

然后,您将获得测试这些方法的所有好处,并且您可以在几秒钟内找到问题,而不是几分钟或几小时。

总而言之,是的,对您的私有方法进行单元测试。


我不同意。 “如果你不测试你的私有方法,你怎么知道它们不会破坏?” :我知道这一点,因为如果我的私有方法被破坏,那么测试依赖于这些私有方法的公共方法的测试将会失败。我不想每次改变我对如何实现公共方法的想法时都改变我的测试。我还认为单元测试的主要兴趣不是具体知道哪一行代码有问题,而是让您或多或少地确信在进行更改(对私有方法)时没有破坏任何东西。
f
fernandezdavid7

你不应该。如果您的私有方法具有必须测试的足够复杂性,您应该将它们放在另一个类中。保持高凝聚力,一个班级应该只有一个目的。类公共接口应该足够了。


B
Billy Jo

如果你不测试你的私有方法,你怎么知道它们不会破坏?


通过编写公共方法的测试。
这些私有方法应该由类的公共方法调用。所以只需测试调用私有方法的公共方法。
如果您的公共方法运行正常,那么显然他们访问的私有方法运行正常。
如果您的公共方法的测试失败,您会立即知道对象/组件/等中较低级别的某些内容不正确。
然而,很高兴知道它是一个内部函数,而不仅仅是破坏的外部函数(或者相反,内部函数很好,你可以专注于外部)。
d
dvorak

这显然取决于语言。在过去使用 c++ 时,我已将测试类声明为友元类。不幸的是,这确实需要您的生产代码了解测试类。


朋友关键字让我很难过。
如果测试类在另一个项目中实现,这不是问题。重要的是生产代码不引用测试类。
O
Olivier Pichon

我理解私有方法被视为实现细节然后不必进行测试的观点。如果我们只能在对象之外开发,我会坚持这条规则。但是我们,我们是不是某种受限的开发者,只在对象之外开发,只调用他们的公共方法?或者我们实际上也在开发那个对象?由于我们不必对外部对象进行编程,因此我们可能不得不将这些私有方法调用到我们正在开发的新公共方法中。知道私有方法可以抵抗所有困难不是很好吗?

我知道有些人可能会回答,如果我们正在为该对象开发另一种公共方法,那么应该测试这个方法,就是这样(私有方法可以在没有测试的情况下继续存在)。但这也适用于对象的任何公共方法:在开发 Web 应用程序时,对象的所有公共方法都是从控制器方法调用的,因此可以视为控制器的实现细节。

那么我们为什么要对对象进行单元测试呢?因为这真的很困难,并不是说不可能确定我们正在使用适当的输入测试控制器的方法,这将触发底层代码的所有分支。换句话说,我们在堆栈中的位置越高,测试所有行为就越困难。私有方法也是如此。

对我来说,私有方法和公共方法之间的界限是测试时的心理标准。对我来说更重要的标准是:

该方法是否从不同的地方多次调用?

该方法是否足够复杂以至于需要测试?


A
Andy

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


m
magallanes

我从不理解单元测试的概念,但现在我知道它的目标是什么。

单元测试不是完整的测试。因此,它不能替代 QA 和手动测试。 TDD 在这方面的概念是错误的,因为您无法测试所有内容,包括私有方法以及使用资源(尤其是我们无法控制的资源)的方法。 TDD 的所有质量都是基于它无法实现的。

单元测试更像是一个支点测试您标记了一些任意支点,支点的结果应该保持不变。


C
Colm Bhandal

是的,您应该尽可能测试私有方法。为什么?为了避免不必要的 state space explosion 测试用例最终只是隐式地在相同的输入上重复测试相同的私有函数。让我们用一个例子来解释为什么。

考虑以下稍微做作的示例。假设我们想要公开一个函数,该函数接受 3 个整数,当且仅当这 3 个整数都是素数时才返回 true。我们可以这样实现它:

public bool allPrime(int a, int b, int c)
{
  return andAll(isPrime(a), isPrime(b), isPrime(c))
}

private bool andAll(bool... boolArray)
{
  foreach (bool b in boolArray)
  {
    if(b == false) return false;
  }
  return true;
}

private bool isPrime(int x){
  //Implementation to go here. Sorry if you were expecting a prime sieve.
}

现在,如果我们采用只测试公共函数的严格方法,我们将只被允许测试 allPrime 而不能测试 isPrimeandAll

作为测试人员,我们可能对每个参数的五种可能性感兴趣:< 0= 0= 1prime > 1not prime > 1。但要彻底,我们还必须看看论点的每种组合是如何一起发挥作用的。所以这是 5*5*5 = 125 个测试用例,根据我们的直觉,我们需要彻底测试这个函数。

另一方面,如果允许我们测试私有函数,我们可以用更少的测试用例覆盖尽可能多的领域。我们只需要 5 个测试用例就可以将 isPrime 测试到与我们之前的直觉相同的水平。并且根据 Daniel Jackson 提出的 small scope hypothesis,我们只需要将 andAll 函数测试到较小的长度,例如 3 或 4。最多可以再进行 16 个测试。所以总共有21个测试。而不是 125 个。当然,我们可能希望在 allPrime 上运行 几个 测试,但我们不会觉得有义务详尽地涵盖我们所说的我们关心的所有 125 个输入场景组合.只有几条快乐的道路。

当然,这是一个人为的例子,但有必要进行清晰的演示。并且该模式扩展到实际软件。私有函数通常是最低级别的构建块,因此经常组合在一起以产生更高级别的逻辑。这意味着在更高级别,由于各种组合,我们对较低级别的东西有更多的重复。


首先,您不必像您展示的那样使用纯函数来测试这样的组合。对 isPrime 的调用是真正独立的,因此盲目地测试每个组合是毫无目的的。其次,将一个名为 isPrime 的纯函数标记为私有违反了许多设计规则,我什至不知道从哪里开始。 isPrime 应该非常清楚地是一个公共函数。话虽如此,不管这个极其糟糕的例子如何,我都明白你在说什么。然而,它建立在您想要进行组合测试的前提下,而在真实的软件系统中这很少是一个好主意。
马特是的,这个例子并不理想,我会给你。但原理应该很明显。
前提并不完全是您想要进行组合测试。如果您将自己限制为仅测试拼图的公共部分,那么您将不得不这样做。在某些情况下,您希望将纯函数设为私有以遵守适当的封装原则。而这个纯粹的私有函数可以被公共函数使用。以组合方式,可能与其他纯私有功能。在这种情况下,遵循不应测试私有的教条,您将被迫对公共功能进行组合测试,而不是对私有组件进行模块化测试。
t
tkruse

公共与私有对于从您的测试中调用什么 API 来说不是一个有用的区别,方法与类也不是。大多数可测试单元在一种情况下是可见的,但在其他情况下是隐藏的。

重要的是覆盖范围和成本。您需要在实现项目的覆盖目标(行、分支、路径、块、方法、类、等价类、用例......无论团队决定)的同时最大限度地降低成本。

所以使用工具来确保覆盖率,并设计你的测试以最小的成本(短期和长期)。

不要让测试变得比必要的更昂贵。如果只测试公共入口点最便宜,那就这样做。如果测试私有方法最便宜,那就这样做。

随着您的经验越来越丰富,您将更好地预测何时值得重构以避免测试维护的长期成本。


佚名

如果该方法足够重要/足够复杂,我通常会将其设为“受保护”并对其进行测试。某些方法将保留为私有并作为公共/受保护方法的单元测试的一部分进行隐式测试。


@VisibleForTesting 是一个注解。我不会放松封装进行测试,而是使用 dp4j.com
a
aemorales1

我看到很多人的想法都是一样的:在公共层面进行测试。但这不是我们的 QA 团队所做的吗?他们测试输入和预期输出。如果作为开发人员我们只测试公共方法,那么我们只是重做 QA 的工作,而不是通过“单元测试”增加任何价值。


目前的趋势是减少或没有QA团队。这些单元测试成为每次工程师将代码推送到主分支时运行的自动化测试。即使有 QA,他们也无法像自动化测试那样快速地测试整个应用程序。
u
unflores

“我应该测试私有方法吗?”的答案是“.......有时”。通常,您应该针对类的接口进行测试。

原因之一是您不需要对某个功能进行双重覆盖。

另一个原因是,如果您更改私有方法,您将不得不为它们更新每个测试,即使您的对象的接口根本没有改变。

这是一个例子:

class Thing
  def some_string
    one + two
  end

  private 

  def one
    'aaaa'
  end

  def two
    'bbbb'
  end

end


class RefactoredThing
def some_string
    one + one_a + two + two_b
  end

  private 

  def one
    'aa'
  end

  def one_a
    'aa'
  end

  def two
    'bb'
  end

  def two_b
    'bb'
  end
end

RefactoredThing 中,您现在有 5 个测试,其中 2 个您必须更新以进行重构,但您的对象的功能确实没有改变。因此,假设事情比这更复杂,并且您有一些方法可以定义输出的顺序,例如:

def some_string_positioner
  if some case
  elsif other case
  elsif other case
  elsif other case
  else one more case
  end
end

这不应该由外部用户运行,但是您的封装类可能很重,无法一遍又一遍地通过它运行这么多的逻辑。在这种情况下,您可能宁愿将其提取到一个单独的类中,给该类一个接口并对其进行测试。

最后,假设您的主要对象超重,并且方法很小,您确实需要确保输出正确。你在想,“我必须测试这个私有方法!”。您是否认为可以通过将一些繁重的工作作为初始化参数传递来使您的对象更轻?然后你可以传递一些更轻的东西并针对它进行测试。


C
Community

不,您不应该测试私有方法why?,而且流行的模拟框架(例如 Mockito)不支持测试私有方法。


S
Supun Wijerathne

一个要点是

如果我们测试是为了保证逻辑的正确性,并且私有方法承载了逻辑,我们就应该测试它。不是吗?那么我们为什么要跳过它呢?

基于方法的可见性编写测试是完全不相关的想法。

反过来

另一方面,在原始类之外调用私有方法是一个主要问题。在一些模拟工具中模拟私有方法也存在限制。 (例如:Mockito)

虽然有一些像 Power Mock 这样的工具支持这一点,但这是一个危险的操作。原因是它需要破解 JVM 来实现这一点。

可以完成的一项工作是(如果您想为私有方法编写测试用例)

将这些私有方法声明为受保护。但在几种情况下可能并不方便。


D
Dirk Herrmann

它不仅与公共或私有方法或函数有关,还与实现细节有关。私有函数只是实现细节的一个方面。

毕竟,单元测试是一种白盒测试方法。例如,无论谁使用覆盖分析来识别迄今为止在测试中被忽略的代码部分,都会进入实现细节。

A)是的,您应该测试实施细节:

考虑一个排序函数,如果有多达 10 个元素,则出于性能原因使用 BubbleSort 的私有实现,如果元素超过 10 个,则使用不同排序方法(例如堆排序)的私有实现。公共 API 是排序函数的 API。但是,您的测试套件更好地利用了实际上使用了两种排序算法的知识。

在此示例中,您当然可以在公共 API 上执行测试。但是,这将需要有许多测试用例来执行具有超过 10 个元素的排序函数,以便堆排序算法得到足够好的测试。仅此类测试用例的存在就表明测试套件与功能的实现细节相关联。

如果排序函数的实现细节发生变化,可能是两种排序算法之间的限制发生了变化,或者堆排序被合并排序或其他方式取代:现有的测试将继续工作。尽管如此,它们的价值还是值得怀疑的,它们可能需要重新设计以更好地测试更改后的排序功能。换句话说,尽管测试是在公共 API 上进行的,但仍会进行维护工作。

B) 如何测试实现细节

许多人认为不应测试私有函数或实现细节的原因之一是,实现细节更有可能发生变化。这种更高的更改可能性至少是将实现细节隐藏在接口后面的原因之一。

现在,假设接口背后的实现包含更大的私有部分,内部接口上的单独测试可能是一个选项。有人争辩说,这些部分不应该在私有时进行测试,它们应该变成公共的东西。一旦公开,对该代码进行单元测试就可以了。

这很有趣:虽然接口是内部的,但它可能会发生变化,因为它是一个实现细节。采用相同的接口,将其公开会进行一些神奇的转换,即把它变成一个不太可能改变的接口。显然,这种论证存在一些缺陷。

但是,这背后还是有一些道理:在测试实现细节时,尤其是使用内部接口时,应该努力使用可能保持稳定的接口。然而,某些接口是否可能是稳定的,不能简单地根据它是公共的还是私有的来确定。在我工作了一段时间的世界项目中,公共接口也经常发生变化,许多私有接口多年来一直保持不变。

不过,使用“前门优先”是一个很好的经验法则(参见 http://xunitpatterns.com/Principles%20of%20Test%20Automation.html)。但请记住,它被称为“前门优先”,而不是“仅前门”。

C) 总结

还要测试实现细节。更喜欢在稳定的接口(公共或私有)上进行测试。如果实现细节发生变化,公共 API 上的测试也需要修改。将私有内容变为公开内容并不会神奇地改变其稳定性。


Y
Yogesh D

您还可以将您的方法设置为包私有,即默认值,并且您应该能够对其进行单元测试,除非它需要是私有的。