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

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

141285 次浏览

我认为单元测试是用来测试公共方法的。您的公共方法使用您的私有方法,因此它们也间接地接受测试。

如果您不测试您的私有方法,您怎么知道它们不会崩溃呢?

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

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

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

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

我不是这方面的专家,但是单元测试应该测试行为,而不是实现。私有方法严格来说是实现的一部分,所以不应该测试IMHO。

这显然与语言有关。在过去的c++中,我将测试类声明为友类。不幸的是,这需要您的生产代码了解测试类。

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

特朗皮对你链接的帖子的回答是最好的。

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

一般来说,您不希望为了…而破坏任何封装 测试(或者就像妈妈常说的,“不要暴露你的私处!”)。大多数 此时,您应该能够通过执行类的 公共方法。如果隐藏了重要的功能 在私人或受保护访问的背后,这可能是一个警告信号 还有一个班级在里面挣扎着要出来

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

如果你的私有方法没有通过调用你的公共方法来测试,那么它在做什么?

如果私有方法定义良好(即,它有一个可测试的函数,并且不会随着时间的推移而改变),那么是的。我在有意义的地方测试所有可测试的东西。

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

它加快了以后的调试。

亚当

当我在我们的项目中越来越多地遵循我们最新的QA建议时,我感觉有必要测试私有函数:

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

现在,强制执行这个策略的副作用是,我的许多非常大的公共函数被划分为许多更集中的,更好的命名为私人函数 公共函数仍然存在(当然),但本质上被简化为所有那些私有“子函数”

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

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

这只是我的个人意见。

测试的目的是什么?

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

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

考虑:您有一个公共方法A,它调用私有方法B。A和B都使用方法C。C被更改(可能由您更改,也可能由供应商更改),导致A开始测试失败。对B进行测试不是很有用吗,即使它是私有的,这样你就知道问题是在A使用C, B使用C,还是两者都有?

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

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

正如上面引用的,“如果你不测试你的私有方法,你怎么知道它们不会崩溃?”

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

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

然后,您就有了测试这些方法的所有好处,您可以在几秒钟内而不是几分钟或几小时内发现问题。

总之,单元测试你的私有方法。

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

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

是的,我确实测试私有函数,因为尽管它们是由你的公共方法测试的,但在TDD(测试驱动设计)中测试应用程序的最小部分是很好的。但是在测试单元类中不能访问私有函数。下面是我们测试私有方法的方法。

为什么我们有私有方法?

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

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

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

1. 你可以通过你的公共方法来测试你的私处

优势

  • 简单的单元测试(不需要“hack”)

缺点

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

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

优势

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

缺点

  • 如果一个类是不需要的,并且只被 方法来自的类
  • 由于增加的开销而造成潜在的性能损失

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

优势

    您正在测试应用程序中最小的可测试部分。当 使用final protected,函数将不会被重写(只是 Like a private)
  • 无性能损失
  • 没有额外的开销

缺点

  • 您正在将一个私有访问更改为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来测试之前的私有函数。

绝对是的。这就是单元测试的要点,你测试单元。私有方法是一个单元。没有测试私有方法,TDD(测试驱动开发)是不可能的,

我已经为这个问题苦恼了一段时间,尤其是在尝试TDD的时候。

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

  1. 测试私有方法,TDD和测试驱动重构
  2. 测试驱动开发不是测试

总而言之:

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

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

对我来说,在编码的开始部分,大多数方法都是高级函数,因为它们封装/描述了设计。

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

私有方法将在一切正常工作后出现,并且我们正在为可读性清洁进行重构。

我理解这种观点,即私有方法被视为实现细节,然后不必进行测试。如果我们必须只在对象之外展开,我会坚持这个规则。但是我们,我们是受限制的开发者吗只在对象之外开发,只调用它们的公共方法?或者我们实际上也在开发那个对象?由于我们不一定要编程外部对象,我们可能不得不将那些私有方法调用到我们正在开发的新的公共方法中。如果知道私人方法能克服一切困难,那不是很好吗?

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

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

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

  • 该方法是否在不同的地方被多次调用?
  • 这种方法是否复杂到需要测试?

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

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

其中一个要点是

如果我们测试以确保逻辑的正确性,并且私有方法携带逻辑,那么我们应该测试它。不是吗?我们为什么要跳过这个呢?

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

相反

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

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

一种可以解决的方法是(如果你想为私有方法写测试用例)

将这些私人方法声明为受保护的。但在某些情况下可能不方便。

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

  • 其中一个原因是您不需要对一个特性进行双重覆盖。
  • 另一个原因是,如果您更改了私有方法,则必须为它们更新每个测试,即使对象的接口根本没有更改。

这里有一个例子:

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

这不应该由外部用户来运行,但是您的封装类可能太笨重了,无法一遍又一遍地运行这么多逻辑。在这种情况下,您可能更愿意将其提取到一个单独的类中,为该类提供一个接口并对其进行测试。

最后,假设你的主对象非常重,方法非常小你需要确保输出是正确的。你会想,“我必须测试这个私有方法!”也许你可以通过传入一些繁重的工作作为初始化参数使你的对象更轻?然后你可以放一些更轻的东西进去测试。

不,你不应该测试私有方法为什么?,而且流行的mock框架(如Mockito)不支持测试私有方法。

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

  1. 通常当你想测试一个类的私有方法时,
  2. 你可以通过公众来测试他们 接口(这是您想要测试它们的方式,因为这就是 客户端将调用/使用它们)。你会得到一种虚假的安全感 看到你的私人测试都通过了 方法。通过公共接口在私有函数上测试边缘用例会更好/更安全
  3. 您可能面临严重的测试重复(看起来/感觉上的测试 非常相似)通过测试私有方法。这是主要的 当需求发生变化时,结果会比测试多出许多 必要必破。它也会让你陷入困境 因为你的测试套件很难重构…这是最终的 讽刺的是,因为测试套件是用来帮助您安全地重新设计的 和重构!李< / >

我将用一个具体的例子来解释这些问题。事实证明,2)和3)之间存在某种复杂的联系,因此它们的示例类似,尽管我认为它们是不应该测试私有方法的不同原因。

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

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

重构你摆脱糟糕设计的方法

我看到的最常见的(反)模式之一是迈克尔的羽毛调用一个“Iceberg"类(如果你不知道Michael Feathers是谁,去买/读他的书“Working effective with Legacy code”;如果你是一个专业的软件工程师/开发人员,他是一个值得了解的人)。还有其他(反)模式导致这个问题突然出现,但这是迄今为止我遇到的最常见的模式。“Iceberg"类有一个公共方法,其余的都是私有方法(这就是为什么很容易测试私有方法)。它叫做“冰山”。类,因为通常只有一个单独的公共方法,但其余功能以私有方法的形式隐藏在水下。它可能看起来像这样:

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()是一个私人函数!所以我们不能像这样测试它,RuleEvaluator0(假设我们使用的是一些真正强制公共/私有的语言,不像一些脚本语言,如Python)。但是如果将RuleEvaluator类更改为遵循单一责任原则(Single Responsibility Principle)呢?例如,我们似乎把解析器、标记器和求值器塞进了一个类中。把这些责任分开不是更好吗?最重要的是,如果你创建了一个Tokenizer类,那么它的公共方法将是HasMoreTokens()GetNextTokens()RuleEvaluator类可以有一个Tokenizer对象作为成员。现在,我们可以保持与上面相同的测试,只是我们测试的是Tokenizer类而不是RuleEvaluator类。

下面是它在UML中的样子:

Rule Evaluator refactoring

注意,这种新设计增加了模块化,因此您可能会在系统的其他部分重用这些类(在此之前,私有方法根据定义是不可重用的)。这是分解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所说的摸索工具。这个工具可以让你触摸别人的隐私部位。一个例子是googletest中的FRIEND_TEST,如果语言支持则为reflection。

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. 你很着急,必须在此时此地做最快的事情。从长远来看,您不希望测试私有方法。但是我想说的是,重构通常需要一些时间来解决设计问题。有时候你得在一周内发货。没关系:如果您认为这是完成工作的最快、最可靠的方法,那么可以快速地、不那么严格地使用摸索工具来测试私有方法。但你要明白,从长远来看,你所做的不是最优的,请考虑回头去做(或者,如果你已经忘记了,但后来看到了,那就改正它)。

也许在其他情况下,这是可以接受的。如果你认为这是可以的,并且你有一个很好的理由,那么就去做。没人阻止你。只是要注意潜在的成本。

TDD的借口

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

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

注意:

我刚才回答了一个关于使用GoogleTest测试私有方法的类似问题。在这里,我修改了这个答案,使之更加语言不可知。

附注:这里是Michael Feathers关于冰山类和摸索工具的相关讲座:https://www.youtube.com/watch?v=4cVZvoFGJTU

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

单元测试不是一个完整的测试。所以,它并不是QA和手动测试的替代品。在这方面,TDD的概念是错误的,因为你不能测试所有的东西,包括私有方法,也包括使用资源的方法(特别是我们无法控制的资源)。TDD的所有质量都是无法实现的。

单元测试更像是主测试你标记一些任意的枢轴,枢轴的结果应该保持不变。

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

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

A)是的,你应该测试实现细节:

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

在本例中,当然可以在公共API上执行测试。然而,这需要有大量的测试用例来执行具有10个以上元素的排序函数,以便对堆排序算法进行充分的测试。这种测试用例的单独存在表明测试套件连接到功能的实现细节。

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

B)如何测试实现细节

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

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

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

但是,这背后仍然有一些事实:在测试实现细节时,特别是使用内部接口时,应该尽量使用可能保持稳定的接口。然而,某些接口是否可能是稳定的,不能简单地根据它是公共的还是私有的来确定。在我工作过一段时间的项目中,公共接口也经常发生变化,而许多私有接口多年未动。

尽管如此,使用“前门优先”是一个很好的经验法则(参见http://xunitpatterns.com/Principles%20of%20Test%20Automation.html)。但请记住,这是“前门优先”,而不是“只有前门”。

C)总结

还要测试实现细节。更喜欢在稳定接口(公共或私有)上进行测试。如果实现细节发生变化,也需要修改对公共API的测试。把私人的东西变成公共的东西并不能神奇地改变它的稳定性。

是的,您应该在任何可能的地方测试私有方法。为什么?避免不必要的状态空间爆炸测试用例,最终只是在相同的输入上隐式地重复测试相同的私有函数。让我们用一个例子来解释为什么。

考虑一下下面略显做作的例子。假设我们想公开一个函数,该函数接受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提出的小范围假说,我们只需要测试andAll函数到一个很小的长度,例如3或4。最多还有16个测试总共21个测试。而不是125。当然,我们可能想要在allPrime上运行测试,但我们不会觉得有必要详尽地覆盖我们所说的关心的所有125种输入场景组合。只有几条快乐的小路。

当然,这是一个虚构的例子,但为了清晰地演示,这是必要的。这种模式可以扩展到真实的软件中。私有函数通常是最低级别的构建块,因此经常组合在一起以产生更高级别的逻辑。也就是说,在较高的层次上,由于不同的组合,我们对较低层次的东西有更多的重复。

你也可以让你的方法包私有,即默认,你应该能够单元测试它,除非它被要求是私有的。

对于从测试中调用什么api,公共和私有并不是一个有用的区分,方法和类也不是。大多数可测试单元在一个上下文中是可见的,但在其他上下文中是隐藏的。

重要的是覆盖范围和成本。您需要最小化成本,同时实现项目的覆盖目标(行、分支、路径、块、方法、类、等价类、用例……)不管团队怎么决定)。

因此,使用工具来确保覆盖率,并设计您的测试以导致最小的成本(简短和长期)。

不要让测试变得过于昂贵。 如果只测试公共入口是最便宜的,那就这样做。 如果测试私有方法的成本最低,就这样做

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