我读过这篇关于如何测试私有方法的帖子.我通常不测试它们,因为我一直认为只测试从对象外部调用的公共方法会更快.你测试私人方法吗?我应该经常测试吗?
我没有单元测试私有方法.私有方法是应该为类的用户隐藏的实现细节.测试私有方法会破坏封装.
如果我发现私有方法很庞大或复杂或非常重要,需要自己的测试,我只需将它放在另一个类中并在那里公开(Method Object).然后我可以轻松地测试现在存在于自己的类中的先前私有但现在公开的方法.
测试的目的是什么?
到目前为止,大多数答案都说私有方法是实现细节,只要公共接口经过充分测试和工作,它们就不会(或至少不应该)起作用.如果您测试的唯一目的是保证公共接口正常工作,这绝对是正确的.
就个人而言,我主要用于代码测试是为了确保未来的代码更改不会导致问题并帮助我调试工作.我发现像公共接口一样彻底地测试私有方法(如果不是这样的话!)进一步推动了这个目的.
考虑一下:你有公共方法A,它调用私有方法B.A和B都使用方法C.C被更改(可能由您,可能是供应商),导致A开始失败其测试.对B进行测试也不是很有用,即使它是私有的,所以你知道问题是在A中使用C,B使用C,还是两者兼而有之?
在公共接口的测试覆盖不完整的情况下,测试私有方法也会增加价值.虽然这是我们通常希望避免的情况,但效率单元测试取决于测试发现错误以及这些测试的相关开发和维护成本.在某些情况下,100%测试覆盖率的好处可能被认为不足以保证这些测试的成本,从而在公共接口的测试覆盖范围中产生差距.在这种情况下,对私有方法进行有针对性的测试可能是代码库的一个非常有效的补充.
我倾向于遵循Dave Thomas和Andy Hunt在他们的实用单位测试一书中的建议:
一般来说,你不想为了测试而打破任何封装(或者像妈妈常说的那样,"不要暴露你的私有!").大多数情况下,您应该能够通过行使其公共方法来测试课程.如果隐藏在私有或受保护访问后面的重要功能,那可能是一个警告信号,表示还有另一个类在那里努力逃脱.
但有时我无法阻止自己测试私有方法,因为它让我感到放心,我正在构建一个完全健壮的程序.
我觉得有必要测试私人功能,因为我在我们的项目中遵循了越来越多的最新QA建议:
每个函数的圈复杂度不超过10 .
现在,执行这项政策的副作用是,我的许多大型公共职能部门都被分成了许多更集中,更好命名的私人职能部门.
公共职能仍然存在(当然),但基本上被简化为所谓的私人"子职能"
这实际上很酷,因为callstack现在更容易阅读(而不是大函数中的bug,我在子子函数中有一个错误,其中包含callstack中先前函数的名称,以帮助我理解"我怎么到那里"
但是,现在似乎更容易直接对这些私有函数进行单元测试,并将大型公共函数的测试留给需要解决方案的某种"集成"测试.
只需2美分.
是的我测试私有函数,因为虽然它们是通过您的公共方法测试的,但在TDD(测试驱动设计)中测试应用程序的最小部分是很好的.但是当您在测试单元课程中时,无法访问私有函数.这是我们测试私有方法的方法.
为什么我们有私人方法?
私有函数主要存在于我们的类中,因为我们想在公共方法中创建可读代码.我们不希望这个类的用户直接调用这些方法,而是通过我们的公共方法.此外,我们不希望在扩展类时(在受保护的情况下)更改其行为,因此它是私有的.
当我们编码时,我们使用测试驱动设计(TDD).这意味着有时我们会偶然发现一些私有且想要测试的功能.私有函数在phpUnit中是不可测试的,因为我们无法在Test类中访问它们(它们是私有的).
我们认为这里有3个解决方案:
你可以通过公共方法测试你的私人
好处
直接的单元测试(不需要'hacks')
缺点
程序员需要了解公共方法,而他只想测试私有方法
您没有测试应用程序中最小的可测试部分
2.如果私有是如此重要,那么为它创建一个新的单独类可能是一个代码
好处
你可以将它重构为一个新类,因为如果它很重要,其他类也可能需要它
可测试单元现在是一种公共方法,因此是可测试的
缺点
如果不需要,您不想创建类,并且仅由方法所来自的类使用
由于增加的开销导致潜在的性能损失
3.将访问修饰符更改为(最终)受保护
好处
您正在测试应用程序中最小的可测试部分.使用final final时,该函数不会被覆盖(就像私有一样)
没有性能损失
没有额外的开销
缺点
您正在更改对受保护的私人访问权限,这意味着它可以由其子项访问
您仍然需要在测试类中使用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来测试我们以前的私有函数.
出于几个原因,我不喜欢测试私有功能.它们如下(这些是TLDR人员的要点):
通常当你想要测试一个类的私有方法时,它就是一种设计气味.
您可以通过公共接口测试它们(这是您想要测试它们的方式,因为这是客户端调用/使用它们的方式).通过查看私有方法的所有通过测试的绿灯,您可以获得错误的安全感.通过公共接口测试私有函数的边缘情况要好得多/更安全.
通过测试私有方法,您可能面临严重的测试重复(看起来/感觉非常相似的测试).当需求发生变化时,这会产生重大影响,因为许多测试将超过必要的测试.由于您的测试套件,它还可以让您处于难以重构的位置......这是极具讽刺意味的,因为测试套件可以帮助您安全地重新设计和重构!
我将用一个具体的例子解释每一个.事实证明,2)和3)有点错综复杂地连接,所以他们的例子是相似的,虽然我认为它们是你不应该测试私有方法的独立原因.
有一次我认为测试私有方法是合适的,但我稍后会更详细地介绍它.
我还说明为什么TDD不是最后测试私有方法的有效借口.
我看到的最常见的(反)模式之一是Michael Feathers所谓的"冰山"课程(如果你不知道Michael Feathers是谁,去买/读他的书"有效地使用遗产代码".他是如果您是专业的软件工程师/开发人员,那么值得了解的人.还有其他(反)模式导致这个问题突然出现,但这是迄今为止我偶然发现的最常见的模式."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()
是一个私人的功能!所以我们不能像这样测试它,因为它甚至不会编译(假设我们使用的某些语言实际上强制执行公共/私有,不像Python这样的脚本语言).但是如何改变RuleEvaluator
课程以遵循单一责任原则(单一责任原则)?例如,我们似乎有一个解析器,标记器和评估器卡在一个类中.将这些责任分开是不是更好?最重要的是,如果你创建一个Tokenizer
类,那么它的公共方法就是HasMoreTokens()
和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),您将测试您的私有方法.
我不是这个领域的专家,但是单元测试应该测试行为,而不是实现.私有方法严格地是实现的一部分,因此不应该测试恕我直言.
我们通过推理测试私有方法,我的意思是我们寻找至少95%的总类测试覆盖率,但只有我们的测试调用公共或内部方法.为了获得覆盖,我们需要根据可能发生的不同场景对公共/内部进行多次调用.这使得我们的测试更加关注他们正在测试的代码的目的.
特朗普对你所关联的帖子的回答是最好的.
我认为单元测试用于测试公共方法.您的公共方法使用您的私有方法,因此它们也间接地进行测试.
我一直在讨论这个问题,特别是在试用TDD的时候.
我发现在TDD的情况下,我认为可以彻底解决这个问题.
测试私有方法,TDD和测试驱动的重构
测试驱动开发不是测试
综上所述:
当使用测试驱动的开发(设计)技术时,私有方法应该仅在已经工作和测试的代码的重新分解过程中出现.
根据流程的本质,从经过全面测试的功能中提取的任何简单实现功能都将是自我测试(即间接测试覆盖).
对我而言,似乎很清楚,在编码的开始部分,大多数方法将是更高级别的功能,因为它们封装/描述了设计.
因此,这些方法将是公开的,并且测试它们将是足够容易的.
私有方法将在一切运行良好之后出现,我们为了可读性和清洁而重新考虑因素.
如上所述,"如果你不测试你的私人方法,你怎么知道他们不会破坏?"
这是一个重大问题.单元测试的一个重点是知道事情在何时,何时以及如何破坏.从而减少了大量的开发和QA工作量.如果测试的所有内容都是公开的,那么您就没有诚实的报道和对班级内部的描述.
我发现执行此操作的最佳方法之一是将测试引用添加到项目中,并将测试放在与私有方法并行的类中.放入适当的构建逻辑,以便测试不会构建到最终项目中.
然后,您将拥有测试这些方法的所有好处,您可以在几秒钟内找到问题,而不是几分钟或几小时.
总而言之,是的,单元测试你的私有方法.
你不应该.如果您的私有方法具有必须测试的足够复杂性,则应将它们放在另一个类上.保持高凝聚力,一个班级应该只有一个目的.类公共接口应该足够了.