据我了解,大多数人似乎都同意不应直接测试私有方法,而应通过任何公共方法对其进行测试。我可以理解他们的观点,但是当我尝试遵循“ TDD的三定律”并使用“红色-绿色-重构”循环时,我对此有一些疑问。我认为最好用一个示例来解释:

现在,我需要一个程序,该程序可以读取文件(包含制表符分隔的数据)并过滤出所有包含非数值数据的列。我想可能已经有一些简单的工具可以做到这一点,但是我决定从头开始实现它,主要是因为我认为这对我来说是一个不错的,干净的项目,可以进行TDD的实践。

因此,首先,我“戴上红色帽子”,也就是说,我需要测试失败。我想,我需要一种可以找到一行中所有非数字字段的方法。因此,我编写了一个简单的测试,当然它无法立即编译,因此我开始编写函数本身,并且在来回循环(红色/绿色)之后,我有了一个有效的函数和一个完整的测试。 >
接下来,我继续一个函数“ gatherNonNumericColumns”,一次读取文件,并在每一行上调用我的“ findNonNumericFields”功能以收集最终必须删除的所有列。几个红绿色循环,我已经完成了,又有了一个正常工作的功能和一个完整的测试。

现在,我认为我应该重构。因为我的方法“ findNonNumericFields”仅是因为我认为实现“ gatherNonNumericColumns”时需要它而设计的,所以在我看来,让“ findNonNumericFields”成为私有是合理的。但是,这将中断我的第一个测试,因为他们将无法再访问他们正在测试的方法。

因此,我最终得到了一个私有方法,以及一组测试它的测试。既然有很多人建议不要测试私有方法,那感觉就像我把自己画在了角落。但是我到底在哪里失败了?

我可以从更高层次开始,编写一个测试来测试最终将成为我的公共方法的测试(即findAndFilterOutAllNonNumericalColumns),但是感觉与TDD的观点相反(至少根据Bob叔叔的说法):您应该在编写测试和生产代码之间不断切换,并且在任何时间点,所有测试都在最后一分钟左右进行。因为如果我从为公共方法编写测试开始,那么在获得私有方法中的所有详细信息之前,将需要几分钟(甚至几小时,甚至几天)才能使该方法测试公共方法通过。

那怎么办? TDD(具有快速的红绿色重构周期)是否与私有方法不兼容?还是我的设计有问题?

评论

如何测试文件阅读器?

这两个功能的差异足以使它们成为不同的单元(在这种情况下,私有方法可能应该在自己的类上),或者它们是同一单元,在这种情况下,我看不到为什么要为单元内部的行为。关于倒数第二段,我看不到冲突。为什么需要编写整个复杂的私有方法来通过一个测试用例?为什么不通过公共方法逐步将其淘汰,或者先以内联方式开始然后将其提取出来?

人们为什么将编程书籍和博客中的成语和陈词滥调当作如何编程的实际指南超出了我的范围。

正是由于这个原因,我不喜欢TDD:如果您在一个新的领域,那么您将要做很多额外的工作,同时试图了解架构的外观和某些事物的工作方式。另一方面:如果您在一个已经有经验的领域,那么除了使您烦恼之外,首先编写测试是有好处的,因为intellisense不理解您为什么编写不可编译的代码。我是思考设计,编写代码然后进行单元测试的忠实粉丝。

“大多数人似乎都认为不应直接测试私有方法”-不,如果有必要的话,请直接测试方法。如果可行,将其隐藏为私人。

#1 楼

单位

我想我可以精确地指出问题的出处:


我想,我需要一种方法来查找其中的所有非数值字段一条线。


紧接着问自己:“将它作为gatherNonNumericColumns的一个单独的可测试单元还是同一单元的一部分?”

如果答案是“是的,分开的”,那么您的操作过程就很简单:该方法需要在适当的类上公开,因此可以作为一个单元进行测试。您的想法就像“我需要测试一种方法,我也需要测试另一种方法”

从您说的内容中,您认为答案是“不,一部分相同”。此时,您的计划应该不再是完全编写和测试findNonNumericFields,然后编写gatherNonNumericColumns。相反,应该简单地编写gatherNonNumericColumns。目前,当您选择下一个红色测试用例并进行重构时,findNonNumericFields应该只是目标位置的一部分。这次,您的思路是“我需要测试一种方法,而在这样做的时候,我应该记住,完成的实现可能会包含另一种方法”。


保持短周期

执行上述操作不会导致您在倒数第二段中描述的问题:


因为如果我首先编写一个测试公共方法,在私有方法工作之前,要花几分钟(在非常复杂的情况下,甚至是数小时,甚至几天)才能使所有细节生效,以便测试公共方法的测试通过。


该技术绝对不需要您编写红色测试,只有当您从头开始实现整个findNonNumericFields时,它才会变成绿色。 findNonNumericFields很有可能在您正在测试的公共方法中以内联代码的形式开始,这些代码将在几个周期的过程中建立,并最终在重构期间提取。


路线图

要给出此特定示例的大致路线图,我不知道您使用的确切测试用例,但是说您正在编写gatherNonNumericColumns作为公用方法。那么最有可能的是,这些测试用例与您为findNonNumericFields编写的用例相同,每个用例都使用一个只有一行的表。当该单行方案完全实现后,并且您想要编写一个测试以强制您提取该方法时,您将编写一个两行案例,这将需要您添加迭代。

评论


我认为这就是答案。在OOP环境中采用TDD时,我经常发现自己很难克服自己的自下而上的直觉。是的,功能应该很小,但这是在重构之后。以前,它们可以是巨大的整体。 +1

–JoãoMendes
15年4月16日在9:26

@JoãoMendes好吧,我不确定您是否应该在重构之前进入巨大的整体状态,尤其是在非常短的RGR周期中。但是,是的,在一个可测试的单元中,自下而上地工作可能会导致OP所描述的问题。

–本·亚伦森
15年4月16日在14:22

好的,我想我知道现在哪里出了问题。非常感谢大家(将此答案标记为答案,但其他大多数答案也同样有帮助)

–亨里克·伯格
15年4月17日在13:19

#2 楼

许多人认为单元测试是基于方法的。不是。它应该基于有意义的最小单位。对于大多数事情来说,这意味着您应该作为一个整体来测试类。没有单独的方法就可以了。

现在显然您将在类上调用方法,但是您应该将测试视为适用于所拥有的黑盒对象,因此您应该能够看到类中的任何逻辑操作提供;这些是您需要测试的东西。如果您的类太大以至于逻辑运算太复杂,那么您就应该首先解决一个设计问题。

具有一千种方法的类可能看起来是可测试的,但是如果您仅测试每个单独的方法,您并没有真正测试类。在调用方法之前,某些类可能需要处于某种状态,例如,在发送数据之前需要建立连接的网络类。不能独立于整个类来考虑发送数据方法。

,因此您应该看到私有方法与测试无关。如果您不能通过调用类的公共接口来使用私有方法,则这些私有方法将无用,而且将永远不会被使用。

我认为许多人都试图将私有方法转换为可测试的单元,因为对他们而言,运行起来似乎很容易,但这会使测试的粒度过于严格。马丁·福勒(Martin Fowler)说,


尽管我从单元作为一类的概念开始,但我经常
采用一堆紧密相关的类并将它们视为一个单一的
unit


这对于面向对象的系统(将对象设计为单元)非常有意义。如果要测试单个方法,也许应该创建一个像C这样的过程系统,或者创建一个完全由静态函数组成的类。

评论


在我看来,这个答案完全忽略了OP问题中的TDD方法。这只是“不要测试私有方法”的口头禅,但没有说明TDD(实际上是基于方法的)如何与基于非方法的单元测试方法一起使用。

–布朗博士
15年4月15日在12:58

@DocBrown不,它完全回答了他,说“不要过度花钱”,使您的生活艰难。 TDD不是基于方法的,而是基于单位的,其中任何单位都有意义。如果您有C库,则可以,每个单元都是一个函数。如果您有一个班级,那么这个单位就是一个对象。正如Fowler所说,有时一个单元是几个紧密相关的类。我认为许多人认为单元测试是一种方法,因为某些愚蠢的工具会基于方法生成存根。

– gbjbaanb
15年4月15日在13:01

@DocBrown与方法有什么关系?红色/绿色/重构表示编写少量测试,然后编写少量代码,然后重构以不断改进系统(AFAIK)。一次任何一种单元测试方法都无法发挥作用,您可以使用一个类轻松地做到这一点。唯一的联系是您可能只对一个方法进行一次更改,因为您只进行了很小的更改,但是您仍然应该进行更多的测试。这是一个解释RGR的链接,完全没有提及方法

– gbjbaanb
15年4月15日在13:25

我必须在这里同意@DocBrown。发问者的问题不是他想要的测试粒度超过了不测试私有方法所能达到的粒度。这是因为他试图遵循严格的TDD方法,并且-没有这样的计划-导致他碰壁,突然发现他对应该使用私有方法进行了大量测试。这个答案无济于事。这是对某些问题的好答案,而不仅仅是这个问题。

–本·亚伦森
15年4月15日在15:03



@Matthew:他的错误是他首先编写了该函数。理想情况下,他应该将公共方法编写为意大利面条代码,然后在重构周期中将其重构为私有函数,而不是在重构周期中将其标记为私有函数。

–slebetman
15年4月16日在3:45

#3 楼

您的数据收集方法足够复杂,可以进行测试,并且可以与主要目标分离,成为自己的方法,而不是某些循环的一部分,这一事实指向解决方案:使这些方法不是私有的,而是其他类的成员提供收集/过滤/制表功能。

然后,您在一处编写针对助手类的愚蠢数据操作方面的测试(例如“区分字符与数字”),并测试您的主要目标(例如“获取销售数据”) )放在另一个地方,您不必在测试中为常规的业务逻辑重复进行基本的过滤测试。

一般而言,如果您做一件事的类包含用于执行另一件事的扩展代码,这是代码的主要目的所必需的,但与它的主要目的不同,该代码应位于另一个类中并通过公共方法进行调用。它不应隐藏在仅偶然包含该代码的类的私有角落中。这样可以同时提高可测试性和可理解性。

评论


是的,我同意你的看法。但是,我的第一个陈述有一个问题,即“足够复杂”部分和“足够独立”部分。关于“足够复杂”:我试图做一个快速的红绿色循环,这意味着在切换到测试之前(或相反),我每次最多只能编码一分钟左右。这意味着我的测试确实会非常精细。我认为这是TDD的优势之一,但也许我已经夸大了它,所以它成为了劣势。

–亨里克·伯格
15年4月15日在9:08

关于“足够独立”:我(再次从unclebob那里)获悉,函数应该很小,而函数应该小于那个。所以基本上我尝试制作3-4行功能。因此,无论多么小和简单,所有功能或多或少都被分成了自己的方法。

–亨里克·伯格
15年4月15日在9:09

无论如何,我觉得数据处理方面(例如findNonNumericFields)应该确实是私有的。而且,如果我将其分成另一个类,则无论如何我都必须将其公开,因此我不太明白这一点。

–亨里克·伯格
15年4月15日在9:13

@HenrikBerg首先要考虑为什么要有对象-它们不是对功能进行分组的便捷方法,而是独立的单元,使复杂的系统更易于使用。因此,您应该考虑将类作为事物进行测试。

– gbjbaanb
15年4月15日在9:26

@gbjbaanb我会争辩说,他们既是同一个人。

–RubberDuck
15年4月17日在11:37

#4 楼

就个人而言,当您编写测试时,我觉得您已经深入到实现的思维定势中。您假设您将需要某些方法。但是,您真的需要他们去做课堂应该做的事情吗?如果有人来内部对他们进行重构,该类会失败吗?如果您使用的是类(在我看来,这应该是测试人员的心态),那么,如果有一种明确的方法来检查数字,您实际上就不会在意。

您应该测试一个类的公共接口。出于某种原因,私有实现是私有的。它不是公共接口的一部分,因为它不是必需的,并且可以更改。这是一个实现细节。

如果您针对公共接口编写测试,您将永远不会真正遇到遇到的问题。您可以为覆盖私有方法(很好)的公共接口创建测试用例,或者不能。在这种情况下,可能是时候考虑一​​下私有方法了,如果无论如何都无法使用它们,则可能将它们全部废弃。

评论


“实施细节”类似于“我是否使用XOR或临时变量在变量之间交换整数”。受保护/私有方法具有合同,就像其他任何合同一样。他们在一定的约束下接受输入,使用输入并产生一些输出。最终,任何具有合同的内容都应进行测试-不一定要对使用您的库的用户进行测试,而对于那些在维护后对其进行修改并对其进行修改的用户而言,则不一定要进行测试。仅仅因为它不是“公共”并不意味着它不是API的一部分。

– Knetic
2015年4月17日23:25



#5 楼

您不会根据您期望班级在内部所做的事情来进行TDD。

您的测试用例应基于班级/功能/程序对外部世界的作用。在您的示例中,用户是否曾经用find all the non-numerical fields in a line?调用您的阅读器类?如果答案为“否”,那么首先编写该文本将是一个不好的测试。您想在类/接口级别上编写关于功能的测试,而不是“类必须要实现什么才能使其正常工作”级别,这就是您的测试。

TDD的流程是:

红色(对外部世界执行的类/对象/函数/等是什么)
绿色(编写使该外部代码最少的代码)世界函数工作)
重构(使它更有效的代码是什么)

不要这样做,因为将来我需要X作为私有方法,让我实现
先进行测试。”如果发现自己在执行此操作,则说明您在“红色”阶段执行不正确。这似乎是您遇到的问题。

如果您发现自己经常为成为私有方法的方法编写测试,那么您在做以下事情之一:


对接口/公共级别用例的理解不够正确,无法为它们编写测试
剧烈地更改设计并重构了多个测试(这可能是一件好事,具体取决于该功能是否在较新的测试中进行了测试)


#6 楼

您通常会在测试中遇到一个常见的误解。

大多数刚接触测试的人都开始这样思考:


为功能F编写测试
实现F
使用对F的调用编写函数G
实现G的测试
通过对G的调用编写针对函数H的测试
br />
,依此类推。

这里的问题是,您实际上没有对功能H进行单元测试。应该测试H的测试实际上是在测试H,G和F同时使用。

要解决此问题,您必须认识到,可测试单元绝不能相互依赖,而要依赖于它们的接口。在您的情况下,单位是简单功能,则接口只是其呼叫签名。因此,必须以某种方式实现G,使其可以与具有与F相同签名的任何函数一起使用。

如何精确地实现G取决于您的编程语言。在许多语言中,您可以将函数(或指向它们的指针)作为参数传递给其他函数。这将使您能够独立测试每个功能。

评论


我希望我可以投票更多次。我只是总结一下,因为您没有正确设计解决方案。

–贾斯汀·欧姆斯(Justin Ohms)
15年4月16日在20:52

在像C这样的语言中,这是有道理的,但是对于OO语言,该单元通常应该是一个类(具有公共和私有方法),那么您应该测试该类,而不是孤立地测试每个私有方法。隔离您的课程,是的。隔离每个类中的方法,不。

– gbjbaanb
2015年6月2日,12:58

#7 楼

在测试驱动开发期间编写的测试应确保一个类正确实现其公共API,同时确保该公共API易于测试和使用。

意味着使用私有方法来实现该API,但是不需要通过TDD创建测试-因为公共API可以正常工作,所以将对功能进行测试。

现在假设您的私有方法足够复杂,以至于它们值得进行独立的测试-但作为原始类的公共API的一部分,它们没有任何意义。好吧,这可能意味着它们实际上应该是其他类上的公共方法-您的原始类在其自己的实现中利用的一种公共方法。以后更容易修改实施细节。无用的测试只会在以后需要重新编写以支持您刚刚发现的一些优雅的重构时让您感到烦恼。

#8 楼

我认为正确的答案是从公共方法开始得出的结论。您将从编写一个调用该方法的测试开始。它将失败,因此您创建的名称不起作用的方法。然后,您也许可以对一个检查返回值的测试进行检查。是否已删除?)

如果您的方法返回一个字符串,则检查该返回值。因此,您只需继续构建它。

我认为在私有方法中发生的任何事情都应该在处理过程中的某个时刻放在公共方法中,然后才作为重构步骤的一部分移入私有方法中。据我所知,重构不需要测试失败。添加功能时,您仅需要通过失败的测试。您只需在重构后运行测试以确保它们全部通过即可。

#9 楼


好像我已经把自己画在角落里了。但是我到底在哪里失败?


有句老话。


如果您计划失败,您计划失败。


人们似乎认为,当您使用TDD时,您只需坐下来编写测试,然后设计就会神奇地发生。这不是真的您需要制定一个高级计划。我发现,当我首先设计接口(公共API)时,我会从TDD中获得最好的结果。我个人创建了一个实际的interface,它首先定义了该类。

gasp我在编写任何测试之前写了一些“代码”!好吧,不。我没有我写了一份合同,一份设计书。我怀疑通过在方格纸上记下UML图可以得到类似的结果。关键是,您必须有一个计划。 TDD并不是进行恶意代码破解的许可。

我真的觉得“测试优先”是一个误称。首先设计,然后测试。

当然,请遵循其他人提供的关于从代码中提取更多类的建议。如果您强烈需要测试某个类的内部,请将这些内部提取到易于测试的单元中并注入。

#10 楼

请记住,测试也可以重构!如果将方法设为私有,则将减少公共API,因此完全可以接受一些针对该“丢失的功能”的测试(也就是降低了复杂性)。

其他人说您的private方法将作为其他API测试的一部分被调用,或者将无法访问,因此可以删除。实际上,如果我们考虑执行路径,事情会更细粒度。例如,如果我们有一个执行除法的公共方法,我们可能要测试导致除法的路径。零。如果将方法设为私有,则可以选择:要么考虑除零路径,要么考虑其他方法如何调用该路径,以消除该路径。

这样,我们可能会放弃一些测试(例如,被零除),并根据剩余的公共API重构其他测试。当然,在理想的世界中,现有的测试可以解决所有剩余的问题,但是现实总是一种折衷;)

评论


虽然其他答案是正确的,因为不应以红色周期编写私有方法,但是人会犯错误。而且,当您走完错误的道路已经足够远时,这就是合适的解决方案。

–slebetman
15年4月16日在3:46

#11 楼

有时可以将私有方法变成另一个类的公共方法。

例如,您可能具有非线程安全的私有方法,并使类处于临时状态。这些方法可以移到单独的类中,该类由您的第一堂课私有地持有。因此,如果您的类是一个Queue,则可以有一个具有公共方法的InternalQueue类,并且Queue类可以私下保存InternalQueue实例。这使您可以测试内部队列,还可以弄清楚InternalQueue上的各个操作是什么。

(当您想象没有List类并且尝试实现时,这是最明显的。 List用作使用它们的类中的私有方法。)

评论


“有时可以将私有方法变成另一个类的公共方法。”我不能太强调。有时,私有方法只是另一个类在为自己的身份大喊大叫。

–user22815
2015年4月17日,下午3:34

#12 楼

我想知道为什么您的语言只有两个级别的隐私,即完全公开和完全私有。

您可以将非公开方法安排为可通过程序包访问的还是类似的方式?然后将您的测试放在同一个程序包中,并享受对不属于公共接口的内部工作的测试。您的构建系统将在构建发行二进制文件时排除测试。

当然,有时您需要拥有真正的私有方法,除定义类外,其他任何方法都无法访问。我希望所有这些方法都非常小。通常,将方法保持较小(例如20行以下)会有很大帮助:测试,维护和仅了解代码会变得更容易。

评论


仅仅为了运行测试而更改方法访问修饰符的情况就是尾巴摆动狗。我认为测试单元的内部部件只会使以后的重构变得更加困难。相反,测试公共接口非常有用,因为它可以作为单位的“合同”。

–脚本
15年4月15日在15:19

您无需更改方法的访问级别。我要说的是,您具有一个中间访问级别,该级别允许无需编写公共合同即可轻松编写某些代码(包括测试)。当然,您必须测试公共接口,但是有时单独测试某些内部工作方式有时会有所帮助。

– 9000
15年4月15日在15:35

#13 楼

我偶尔会碰碰到私有方法来进行保护,以允许进行更细粒度的测试(比公开的公共API更严格)。这应该是(希望是非常罕见的)例外,而不是规则,但是在您可能遇到的某些特定情况下,它可能会有所帮助。另外,在构建公共API时,您根本不需要考虑这一点,在那些罕见的情况下,人们可以在内部使用软件上使用更多的“欺骗”。

#14 楼

我已经经历了这个过程,感到很痛苦。
我的解决方法是:
停止像对待整体测试那样对待测试。
请记住,当您编写了一组测试时,假设5,无需使用所有功能,尤其是当它成为其他功能的一部分时,不必保留所有这些测试。
例如,我经常有:

低级测试1
符合要求的代码
低水平测试2
符合要求的代码
低水平测试3
符合要求的代码
低水平测试4
满足要求的代码
低级别测试5
满足要求的代码

所以我有了

低级别测试1
低级测试2
低级测试3
低级测试4
低级测试5

但是现在如果我添加调用它的高级功能,其中包含很多测试,我现在可以将这些低级别测试减少为:

低级别测试1
低级别测试5

细节在于魔鬼,而这样做的能力将取决于视情况而定。

#15 楼

太阳是围绕地球公转还是围绕太阳公转?根据爱因斯坦的说法,答案是肯定的,或者两种模式都仅在观点上有所不同,同样,封装和测试驱动的开发仅在冲突中,因为我们认为它们是冲突的。我们像伽利略和教皇一样坐在这里,互相侮辱:傻瓜,难道你不知道私有方法也需要测试吗?异端,不要破坏封装!同样,当我们认识到真理比我们想象的宏大时,可以尝试封装专用接口的测试,以使公用接口的测试不会破坏封装。

尝试以下操作:添加两个方法,一个没有输入但只返回一个私有测试的方法,一个以测试号作为参数并返回通过/失败的方法。

评论


伽利略(Galileo)和教皇(The Pope)使用了这些侮辱,而不是对此问题的任何回答。

–躲藏
2015年4月18日14:54