TDD红绿色重构以及是否/如何测试变为私有的方法


91

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

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

因此,首先,我“戴上红色帽子”,也就是说,我需要测试失败。我想,我需要一种可以找到一行中所有非数字字段的方法。因此,我编写了一个简单的测试,当然它无法立即编译,因此我开始编写函数本身,并且在来回循环(红色/绿色)之后,我有了一个有效的函数和一个完整的测试。

接下来,我继续使用函数“ gatherNonNumericColumns”,一次读取文件,并在每一行调用我的“ findNonNumericFields”功能以收集最终必须删除的所有列。几个红绿色循环,我完成了,又一次具有工作功能和完整的测试。

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

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

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

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



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

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

7
出于这个原因,我不喜欢TDD:如果您处于一个新领域,那么您将做很多额外的工作,同时尝试了解体系结构应该如何以及某些事情如何工作。另一方面:如果您在一个已经有经验的领域,那么除了使您烦恼之外,首先编写测试将是有好处的,因为intellisense无法理解您为什么编写不可编译的代码。我是考虑设计,编写然后进行单元测试的忠实粉丝。
Jeroen Vannevel 2015年

1
“大多数人似乎都同意不应直接测试私有方法”-不,如果有必要的话,请直接测试方法。隐藏它,private好像这样做有意义。
osa 2015年

Answers:


44

单位

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

我想,我需要一种可以找到一行中所有非数字字段的方法。

随后应该问自己:“将它作为单独的可测试单元,gatherNonNumericColumns还是作为同一单元的一部分?”

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

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


保持短周期

做好以上应该不会导致你在倒数第二段描述的问题:

因为如果我从为公共方法编写测试开始,那么在私有方法中获得所有详细信息之前,将需要几分钟(甚至几小时,甚至几天)才能使该方法测试公共方法通过。

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


路线图

为了给这个特定示例提供一个大致的路线图,我不知道您使用的确切测试用例,而是说您是以gatherNonNumericColumns公共方法编写的。这样一来,测试用例很可能与您编写的用例相同findNonNumericFields,每个用例仅使用一张只有一行的表。当该单行方案完全实现后,您想要编写一个测试以强制您提取该方法,那么您将编写一个两行的案例,这将需要您添加迭代。


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

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

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

66

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

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

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

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

我认为许多人尝试将私有方法转换为可测试的单元,因为对他们来说运行测试似乎很容易,但这使测试粒度过高。马丁·福勒说

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

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


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

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

3
@gbjbaanb:尝试建议一个允许OP首先使用纯TDD来实现其“聚集行中非数字字段”的测试,而无需拥有他打算编写的类的公共接口。
布朗

8
我必须在这里同意@DocBrown。发问者的问题不是他想要更多的测试粒度而不是不测试私有方法所能达到的。这是因为他试图遵循严格的TDD方法,并且没有进行这样的计划,这导致他碰壁,突然发现自己针对专用方法进行了大量测试。这个答案无济于事。对于某些问题,这是一个很好的答案,而不仅仅是这个问题。
本·亚伦森2015年

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

51

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

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

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


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

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

无论如何,我觉得数据处理方面(例如findNonNumericFields)应该确实是私有的。而且,如果我将其分为另一个类,则无论如何我都必须将其公开,因此我不太明白其中的意义。
亨里克·伯格

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

@gbjbaanb我会争辩说它们既是同一个人。
RubberDuck

29

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

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

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


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

11

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

您的测试用例应基于类/功能/程序对外部环境的影响。在您的示例中,用户是否会使用来调用您的阅读器类find all the non-numerical fields in a line?

如果答案是“否”,那么首先写就不好了。您想在类/接口级别上编写有关功能的测试,而不是“类必须要实现什么才能使它正常工作”级别,而这正是您的测试。

TDD的流为:

  • 红色(类/对象/函数/等对外部世界做了什么)
  • 绿色(编写使该外部函数正常工作的最少代码)
  • 重构(使此工作更好的代码是什么)

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

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

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

9

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

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

  • 为功能F编写测试
  • 实施F
  • 为功能G编写测试
  • 通过调用F实现G
  • 为函数H编写测试
  • 使用对G的调用实现H

等等。

这里的问题是实际上您没有针对函数H的单元测试。应该测试H的测试实际上是同时测试H,G和F。

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

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


3
我希望我可以多次投票。我只是总结一下,因为您没有正确设计解决方案。
贾斯汀·欧姆

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

8

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

您绝对可以使用私有方法来实现该API,但是无需通过TDD创建测试-由于公共API可以正常工作,因此将对功能进行测试。

现在,假设您的私有方法足够复杂,因此值得进行独立测试-但作为原始类的公共API的一部分,它们没有任何意义。好吧,这可能意味着它们实际上应该是其他某个类上的公共方法-您的原始类在其自己的实现中利用了该方法。

通过仅测试公共API,您将来可以更轻松地修改实现细节。无用的测试只会在以后需要重新编写以支持您刚刚发现的一些优雅的重构时使您烦恼。


4

我认为正确的答案是从公共方法开始得出的结论。您将从编写一个调用该方法的测试开始。它将失败,因此您将创建一个不执行任何操作的名称的方法。然后,您可能需要对检查返回值的测试进行正确处理。

(我对您的函数的用途尚不完全清楚。它是否返回一个字符串,其中包含非数字值的文件内容?)

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

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


3

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

有句老话。

当您计划失败时,您计划失败。

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

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

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

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


2

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

其他人则说您的私有方法将在其他API测试中被调用,否则将无法访问并因此被删除。实际上,如果我们考虑执行路径,事情会更细粒度。

例如,如果我们有一个执行除法的公共方法,我们可能要测试导致零除的路径。如果将方法设为私有,则可以选择:要么考虑除零路径,要么考虑其他方法如何调用该路径以消除该路径。

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


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

2

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

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

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


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

0

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

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

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


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

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

0

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


0

我经历了这一点,感到了你的痛苦。

我的解决方案是:

停止像建造整体一样对待测试。

请记住,当您编写了一组测试(假设5)来实现某些功能时,不必保留所有这些测试,尤其是当它成为其他功能的一部分时。

例如,我经常有:

  • 低水平测试1
  • 满足它的代码
  • 低水平测试2
  • 满足它的代码
  • 低水平测试3
  • 满足它的代码
  • 低水平测试4
  • 满足它的代码
  • 低水平测试5
  • 满足它的代码

所以我有

  • 低水平测试1
  • 低水平测试2
  • 低水平测试3
  • 低水平测试4
  • 低水平测试5

但是,如果我现在添加调用它的更高级别的函数(具有很多测试),则现在可以将那些低级别的测试减少为:

  • 低水平测试1
  • 低水平测试5

细节决定成败,能否做到将取决于具体情况。


-2

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

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


1
伽利略(Galileo)和教皇(The Pope)使用的是侮辱,而不是对此问题的任何回答。
hildred
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.