我已阅读这篇文章如何测试私有方法。我通常不测试它们,因为我一直认为仅测试将从对象外部调用的公共方法会更快。您是否测试私有方法?我应该一直测试它们吗?
我已阅读这篇文章如何测试私有方法。我通常不测试它们,因为我一直认为仅测试将从对象外部调用的公共方法会更快。您是否测试私有方法?我应该一直测试它们吗?
Answers:
我不对私有方法进行单元测试。私有方法是实现的细节,应该对类的用户隐藏。测试私有方法会破坏封装。
如果我发现私有方法庞大,复杂或重要到需要进行自己的测试,则将其放在另一个类中并在其中公开(方法对象)。然后,我可以轻松地测试以前生活在其自己的类中的以前私有但现在公开的方法。
测试的目的是什么?
到目前为止,大多数答案都说私有方法是实现细节,只要公共接口经过了良好的测试并且可以正常工作,它们就不会(或至少不应该)起作用。如果您唯一的测试目的是确保公共接口正常工作,那么这是绝对正确的。
就个人而言,我进行代码测试的主要用途是确保将来的代码更改不会引起问题,并在可能的情况下帮助我进行调试。我发现对私有方法的测试与对公共接口的测试一样彻底(如果不是,那么!)进一步达到了这个目的。
考虑:您有一个公用方法A,它调用私有方法B。A和B都使用方法C。C被更改了(也许由您,也许由供应商更改),导致A开始测试失败。即使B是私有的,也对B进行测试不是有用的,这样您就知道问题出在A是使用C,B是使用C还是两者都存在?
在私有接口的测试范围不完整的情况下,测试私有方法还可以增加价值。虽然这是我们通常要避免的情况,但效率单元测试取决于发现错误的测试以及这些测试的相关开发和维护成本。在某些情况下,可能会判定100%测试覆盖率的好处不足以保证这些测试的成本,从而在公共界面的测试覆盖率上造成了缺口。在这种情况下,对专用方法进行有针对性的测试可能是对代码库的非常有效的补充。
testDoSomething()
或中testDoSomethingPrivate()
。这使测试的价值降低。。这是测试私有stackoverflow.com/questions/34571/…的更多原因 :
我倾向于遵循Dave Thomas和Andy Hunt在他们的《实用单元测试》一书中的建议:
通常,您不想为了测试而破坏任何封装(或者就像妈妈曾经说过的,“不要暴露您的私人信息!”)。大多数时候,您应该能够通过使用公共方法来测试一个类。如果私有或受保护的访问背后隐藏着重要的功能,则可能是一个警告信号,表明其中还有另一个类别正在努力摆脱困境。
但是有时候我无法阻止自己测试私有方法,因为它使我感到放心,因为我正在构建一个完全健壮的程序。
在执行项目中越来越多的最新质量检查建议之一时,我感到不得不测试私有功能:
每个函数的圈复杂度不超过10 。
现在,执行此策略的副作用是,我的许多大型公共职能被划分为许多更集中,名称更好的私有职能。
公共功能仍然存在(当然),但本质上被简化为称为所有这些私有“子功能”
这实际上很酷,因为调用栈现在更易于阅读(而不是大型函数中的错误,我在子子函数中有一个错误,它具有调用栈中先前函数的名称,以帮助我理解'我怎么到达那里')
但是,现在似乎可以直接对那些私有功能进行单元测试,而将大型公共功能的测试留给某种需要解决方案的“集成”测试似乎更容易。
只是我的2美分。
是的,我确实在测试私有功能,因为尽管私有功能已通过您的公共方法进行了测试,但是在TDD(测试驱动设计)中很好地测试了应用程序的最小部分。但是,当您处于测试单元类中时,私有功能将无法访问。这是我们测试私有方法的工作。
为什么我们有私有方法?
私有函数主要存在于我们的类中,因为我们想在我们的公共方法中创建可读的代码。我们不希望此类的用户直接调用这些方法,而是通过我们的公共方法。另外,我们不希望在扩展类时更改其行为(在受保护的情况下),因此它是私有的。
在编写代码时,我们使用测试驱动设计(TDD)。这意味着有时我们偶然发现了一项私有的功能,需要测试。私有函数无法在phpUnit中进行测试,因为我们无法在Test类中访问它们(它们是私有的)。
我们认为这是3个解决方案:
1.您可以通过公开方法测试私人身份
优点
缺点
2.如果私有非常重要,那么为它创建一个新的单独的类也许是一种代码技巧
优点
缺点
3.将访问修饰符更改为(最终)受保护
优点
缺点
例
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来测试我们以前的私有函数。
由于某些原因,我不喜欢测试私有功能。它们如下(这些是TLDR人员的要点):
我将用一个具体的例子来解释每一个。事实证明2)和3)有点错综复杂,所以它们的示例相似,尽管我认为它们是您不应该测试私有方法的单独原因。
有时候测试私有方法是合适的,了解上面列出的缺点很重要。我将在后面详细介绍。
我还将讨论为什么TDD最终并不是测试私有方法的有效借口。
我看到的最常见的(反)模式之一是Michael Feathers所说的“ Iceberg”课(如果您不知道Michael Feathers是谁,那就去买/读他的书“使用Legacy Code有效地工作”。一个值得了解您是否是专业软件工程师/开发人员的人)。还有其他(反)模式会使此问题浮出水面,但这是我偶然发现的最常见的模式。“ Iceberg”类有一个公共方法,其余的是私有方法(这就是为什么要尝试测试私有方法的原因)。之所以称为“ Iceberg”类,是因为通常会出现一个单独的公共方法,但是其余功能以私有方法的形式隐藏在水下。
例如,您可能希望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()
是私有功能!因此,我们无法像这样测试它,因为它甚至不会编译(假设我们使用的是某种实际上强制执行public / private的语言,这与某些脚本语言(如Python)不同)。但是,如何改变RuleEvaluator
班级以遵循单一责任原则(Single Responsibility Principle)呢?例如,我们似乎有一个解析器,标记器和评估器被卡在一个类中。仅将这些职责分开会更好吗?最重要的是,如果您创建一个Tokenizer
类,则它的公共方法将是HasMoreTokens()
and GetNextTokens()
。本RuleEvaluator
类可以有Tokenizer
对象为成员。现在,我们可以保持与上面相同的测试,除了我们要测试的是Tokenizer
类而不是RuleEvaluator
类。
这是UML中的样子:
请注意,这种新设计提高了模块化,因此您有可能在系统的其他部分重用这些类(在定义之前,私有方法不可重用)。这是将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
该类的以下可能实现:
假设这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)所说的摸索工具。此工具可让您触摸其他人的私处。一个示例FRIEND_TEST
来自googletest,如果语言支持,则进行反射。
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不会强迫您这样做。您可以先编写测试(针对公共接口),然后编写满足该接口的代码。有时我会为公共接口编写测试,而我也会通过编写一个或两个较小的私有方法来满足它(但我不会直接测试私有方法,但我知道它们可以工作,否则我的公共测试会失败)。如果我需要测试该私有方法的边缘情况,我将编写一堆测试,这些测试将通过我的公共接口进行测试。如果您不知道如何处理极端情况,则这是一个强烈的信号,您需要将其重构为每个都有自己的公共方法的小组件。这表明您私有函数做得太多,超出了类的范围。
另外,有时我发现我编写的测试现在无法满足需要,因此我认为“嗯,等我有更多的API可以使用时,我会稍后再返回该测试。” '将其注释掉并保留在我的脑海中)。在这里,我遇到的许多开发人员将开始使用TDD作为替罪羊为其私有功能编写测试。他们说:“哦,我还需要其他测试,但是要编写该测试,我需要这些私有方法。因此,由于如果不编写测试就无法编写任何生产代码,因此我需要编写测试用于私有方法。” 但是他们真正需要做的是将其重构为更小且可重用的组件,而不是在当前类中添加/测试一堆私有方法。
注意:
不久前,我回答了一个有关使用GoogleTest测试私有方法的类似问题。我在这里大部分修改了该答案,使其与语言无关。
PS这是迈克尔·费瑟斯(Michael Feathers)关于冰山课程和摸索工具的相关演讲:https : //www.youtube.com/watch?v=4cVZvoFGJTU
_
,则表示“嘿,这是'私人'。您可以使用它,但要完全披露,它不是为重用而设计的,只有在您确实需要时才使用它知道你在做什么“。您可以使用任何一种语言使用相同的方法:将这些成员公开,但是要在他们前面加上领导_
。或者,也许这些功能确实应该是私有的,并且只是通过公共接口进行了测试(有关更多详细信息,请参见答案)。视情况而定,没有一般规则
如果您的私有方法未通过调用公共方法进行测试,那么它在做什么?我说的是不受保护的私人或朋友。
如果私有方法定义明确(即,它具有可测试的功能,并且不打算随时间变化),则可以。我会在有意义的地方测试所有可测试的东西。
例如,加密库可能掩盖了它使用私有方法执行块加密的事实,该私有方法一次仅加密8个字节。我会为此编写一个单元测试-即使它被隐藏了,也并不意味着要改变,并且如果它确实崩溃了(例如,由于将来性能的提高),那么我想知道是私有函数崩溃了,而不仅仅是一项公共职能中断了。
稍后可以加快调试速度。
-亚当
如果您正在开发测试驱动(TDD),则将测试您的私有方法。
我一直在解决这个问题有一段时间了,尤其是尝试在TDD方面。
我遇到过两篇文章,我认为在TDD的情况下,可以彻底解决此问题。
综上所述:
当使用测试驱动的开发(设计)技术时,私有方法仅应在已经工作且经过测试的代码的重构过程中出现。
从过程的本质上讲,从经过全面测试的功能中提取的任何简单实现功能都将对其进行自我测试(即间接测试覆盖率)。
对我来说,似乎很清楚,在编码的开始部分,大多数方法将是更高级别的函数,因为它们封装/描述了设计。
因此,这些方法将是公开的,并且对其进行测试将非常容易。
一旦一切正常,私有方法将在稍后出现,为了便于阅读和整洁,我们正在重构。
如上所述,“如果不测试私有方法,您怎么知道它们不会中断?”
这是一个主要问题。单元测试的重点之一就是要知道在哪里,何时以及如何尽快破坏某些内容。因此减少了大量的开发和质量检查工作。如果所有经过测试的都是公开的,那么您就不会对课程内部有诚实的报道和描述。
我发现最好的方法之一就是将测试引用添加到项目中,然后将测试放入与私有方法平行的类中。放入适当的构建逻辑,以使测试不会构建到最终项目中。
这样,您便具有测试这些方法的所有好处,并且可以在数秒而不是数分钟或数小时内发现问题。
综上所述,是的,对您的私有方法进行单元测试。
你不应该。如果您的私有方法具有必须测试的足够复杂性,则应将它们放在另一个类上。保持高度凝聚力,一堂课只有一个目的。类的公共接口应该足够了。
我理解将私有方法视为实现细节,而不必进行测试的观点。如果我们只需要在对象之外进行开发,我会坚持这条规则。但是我们,我们是否是某种受限的开发人员,他们仅在对象外部进行开发,仅调用其公共方法?还是我们实际上也在开发该对象?由于我们不必对外部对象进行编程,因此我们可能必须将这些私有方法称为我们正在开发的新公共方法。知道私有方法可以抵抗一切困难,这不是很好吗?
我知道有人会回答,如果我们要在该对象中开发另一种公共方法,那么应该对此对象进行测试,就是这样(私有方法可以在未经测试的情况下进行)。但这对于对象的任何公共方法也是如此:开发Web应用程序时,对象的所有公共方法都是从controllers方法中调用的,因此可以视为控制器的实现细节。
那么为什么要对对象进行单元测试?因为这确实很困难,所以要确保我们正在使用适当的输入来测试控制器的方法,这将触发基础代码的所有分支,这并不是说不可能。换句话说,我们在堆栈中的位置越高,测试所有行为的难度就越大。私有方法也是如此。
对我来说,私人和公共方法之间的边界是测试的心理学标准。对我来说更重要的标准是:
如果该方法足够重要/足够复杂,我通常会使其受到“保护”并进行测试。一些方法将被保留为私有方法,并作为公共/受保护方法的单元测试的一部分进行隐式测试。
我看到许多人的想法一致:在公共级别进行测试。但是这不是我们的质量检查小组的工作吗?他们测试输入和预期输出。如果作为开发人员我们仅测试公共方法,那么我们只是在重做QA的工作,而不是通过“单元测试”增加任何价值。
答案为“我应该测试私有方法吗?” 是“ ..有时”。通常,您应该针对类的接口进行测试。
这是一个例子:
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
这不应该由外部用户运行,但是您的封装类可能很重,无法一遍又一遍地运行这么多的逻辑。在这种情况下,您可能希望将其提取到一个单独的类中,为该类提供一个接口并对其进行测试。
最后,假设您的主要对象非常沉重,并且该方法非常小,您确实需要确保输出正确。您在想,“我必须测试这种私有方法!”。您是否可以通过传递一些繁重的工作作为初始化参数来减轻物体的重量?然后,您可以传递一些较轻的内容并进行测试。
一个要点是
如果我们进行测试以确保逻辑的正确性,并且私有方法正在承载逻辑,则应该对其进行测试。是不是 那么为什么我们要跳过呢?
基于方法的可见性编写测试完全无关紧要。
反过来
另一方面,在原始类之外调用私有方法是一个主要问题。在某些模拟工具中模拟私有方法也有局限性。(例如:Mockito)
虽然有一些工具,例如Power Mock支持该,但这是危险的操作。原因是它需要破解JVM才能实现。
可以解决的一项工作是(如果您要为私有方法编写测试用例)
将那些私有方法声明为受保护的。但这在某些情况下可能并不方便。
它不仅涉及公共或私有方法或函数,还涉及实现细节。私有功能只是实现细节的一方面。
毕竟,单元测试是一种白盒测试方法。例如,无论谁使用覆盖率分析来识别到目前为止在测试中被忽略的部分代码,都会进入实现细节。
A)是的,您应该测试实施细节:
考虑一下出于性能原因的排序函数,如果存在多达10个元素,则使用BubbleSort的私有实现;如果存在10个以上的元素,则使用其他排序方法(例如堆排序)的私有实现。公共API是排序功能的API。但是,您的测试套件更好地利用了以下知识:实际上使用了两种排序算法。
当然,在此示例中,您可以在公共API上执行测试。但是,这将需要多个测试用例来执行具有超过10个元素的排序功能,以使堆排序算法得到充分的测试。仅存在这种测试用例就表明测试套件已连接到该功能的实现细节。
如果排序功能的实现细节发生变化,可能是通过改变两种排序算法之间的限制,或者是用mergesort或其他方式替换了heapsort:现有测试将继续起作用。但是,它们的价值仍然值得怀疑,并且可能需要对其进行重新设计以更好地测试更改后的排序功能。换句话说,尽管测试是在公共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
而不能进行isPrime
or andAll
。
作为测试者,我们可能会感兴趣的每个参数五种可能性:< 0
,= 0
,= 1
,prime > 1
,not prime > 1
。但要彻底,我们还必须查看参数的每种组合如何一起发挥作用。因此5*5*5
,根据直觉,这= 125个测试用例,我们需要彻底测试此功能。
另一方面,如果允许我们测试私有功能,则可以用更少的测试用例覆盖尽可能多的领域。我们只需要5个测试用例就可以测试isPrime
到与之前的直觉相同的水平。根据丹尼尔·杰克逊(Daniel Jackson)提出的小范围假设,我们只需要对andAll
函数进行最小长度的测试即可,例如3或4。最多可以进行16个测试。总共进行了21次测试。而不是125。当然,我们可能希望对进行一些测试allPrime
,但是我们没有必要全面介绍我们关心的所有125种输入方案组合。只是一些快乐的道路。
当然,这是一个人为的例子,但是对于一个清晰的示范是必要的。而且模式扩展到了真正的软件。私有功能通常是最低级别的构建块,因此经常被组合在一起以产生更高级别的逻辑。在较高级别上,由于各种组合,我们对较低级别的内容有更多的重复。
isPrime
是真正独立的,因此盲目测试每个组合都是毫无目的的。其次,标记一个纯函数叫做isPrime
private违反了很多设计规则,我什至不知道从哪里开始。isPrime
显然应该是公共职能。话虽这么说,但不管这个极差的例子,我都会明白你的意思。但是,它是在您要进行组合测试的前提下构建的,而在真正的软件系统中,这通常不是一个好主意。