如果执行TDD,是否应该避免使用私有方法?


99

我现在正在学习TDD。我的理解是,私有方法是不可测试的,不应担心,因为公共API将提供足够的信息来验证对象的完整性。

我了解OOP已有一段时间了。据我了解,私有方法使对象更易于封装,从而更能抵抗更改和错误。因此,默认情况下应使用它们,并且仅将对客户端重要的那些方法公开。

好吧,对于我来说,可以创建一个仅具有私有方法并通过侦听其他事件与其他对象进行交互的对象。这将被非常封装,但是完全不可测试。

另外,为了测试而添加方法也被认为是不好的做法。

这是否意味着TDD与封装不一致?适当的余额是多少?我现在倾向于公开大多数或所有方法...


9
软件行业的错误实践和现实是不同的动物。在商业世界中,理想情况往往是不真实的情况。做有意义的事情,并在整个应用程序中坚持下去。我宁愿有一个不好的习惯,也不愿在整个应用程序中分散当月的味道。
亚伦·麦克弗

10
“私有方法不可测”?哪种语言?在某些语言中,这很不方便。在其他语言中,这非常简单。另外,您是在说封装的设计原理必须始终通过许多私有方法来实现吗?这似乎有点极端。某些语言还没有私有方法,但似乎仍具有很好的封装设计。
S.Lott 2012年

“我的理解是,私有方法使对象更易于封装,从而更能抵抗更改和错误。因此,默认情况下应使用它们,并且只有那些对客户端重要的方法才应公开。” 在我看来,这似乎与TDD试图实现的目标相反。TDD是一种开发方法论,可引导您创建简单,可行和开放的设计。“从私人”和“仅公开...”的眼光完全被扭转了。忘了有一种包含TDD的私有方法。稍后,根据需要进行操作;作为重构的一部分。
herby 2012年


因此,@ gnat您认为应该将其关闭,作为我对这个问题的回答引起的问题的副本?* 8')
Mark Booth,

Answers:


50

优先于接口测试而不是实现测试。

据我了解,私有方法不可测试

这取决于您的开发环境,请参见下文。

[私有方法]不必担心,因为公共API将提供足够的信息来验证对象的完整性。

没错,TDD专注于测试接口。

私有方法是一种实现细节,可以在任何重构周期内更改。应该可以在不更改界面或黑匣子行为的情况下进行重构。实际上,这是TDD好处的一部分,您可以轻松地产生信心,即班级内部的更改不会影响该班级的用户。

好吧,对于我来说,可以创建一个仅具有私有方法并通过侦听其他事件与其他对象进行交互的对象。这将被非常封装,但是完全不可测试。

即使类没有公共方法,它的事件处理程序是它的公共接口,而且其针对的是 公共接口,你可以测试一下。

由于事件是接口,因此它是您需要生成以测试该对象的事件。

研究将模拟对象用作测试系统的粘合剂。应该可以创建一个简单的模拟对象,该对象生成一个事件并获取状态的变化(可能由另一个接收者模拟对象)。

另外,为了测试而添加方法也被认为是不好的做法。

绝对,您应该非常警惕暴露内部状态。

这是否意味着TDD与封装不一致?适当的余额是多少?

绝对不。

TDD除了可能简化类的实现之外,不应更改类的实现(通过从较早的位置应用YAGNI)。

使用TDD的最佳实践与不使用TDD的最佳实践是相同的,您只需找出原因,便会更快,因为在开发界面时就使用了该界面。

我现在倾向于公开大多数或所有方法...

这宁可将婴儿与洗澡水一起扔出去。

无需公开所有方法,即可以TDD方式进行开发。请参阅下面的注释,看看您的私有方法是否真的不可测试。

更详细地介绍测试私有方法

如果您绝对必须对类的某些私人行为进行单元测试,具体取决于语言/环境,则可以有以下三种选择:

  1. 将测试放在要测试的类中。
  2. 将测试放在另一个类/源文件中,并将要测试的私有方法公开为公共方法。
  3. 使用一个测试环境,该环境允许您将测试代码和生产代码分开,但仍允许测试代码访问生产代码的私有方法。

显然,第三个选项是迄今为止最好的。

1)将测试放在您要测试的课程中(不理想)

最简单的选择是将测试用例与要测试的生产代码存储在相同的类/源文件中。但是,如果没有很多预处理器指令或注释,最终将导致测试代码不必要地膨胀生产代码,并且取决于您如何构造代码,最终可能会意外地向代码用户公开内部实现。

2)将要测试的私有方法公开为公共方法(真的不是一个好主意)

如建议的那样,这是非常差的做法,会破坏封装并将内部实现向代码用户公开

3)使用更好的测试环境(最佳选择,如果有的话)

在Eclipse世界中,3.可以通过使用fragments实现。在C#世界中,我们可以使用局部类。其他语言/环境通常具有类似的功能,您只需要找到它即可。

盲目假设1.或2.是唯一的选择,可能会导致生产软件software满测试代码或讨厌的类接口,从而在公共场合清洗脏的亚麻布。* 8')

  • 总而言之-最好不要对私有实现进行测试。

5
我不确定我会同意您建议的三个选项中的任何一个。我的选择是仅像您之前所说的那样测试公共接口,但要确保这样做可以执行私有方法。这样做的部分优势是找到无效代码,如果您强迫测试代码破坏该语言的常规用法,则不太可能发生这种情况。
Magus

您的方法应该做一件事,并且测试不应以任何方式考虑实现。私有方法是实现细节。如果仅测试公共方法意味着您的测试是集成测试,则说明您存在设计问题。
Magus

将方法设置为默认/受保护并在具有相同程序包的测试项目中创建测试该怎么办?
RichardCypher

@RichardCypher这实际上与2)相同,因为您正在更改理想的方法规范,以适应测试环境中的不足,因此,实践仍然很差。
Mark Booth'3

75

当然,您可以使用私有方法,也可以测试它们。

某种方法可以使私有方法运行,在这种情况下,您可以用这种方法进行测试,或者没有办法使私有方法运行,在这种情况下:为什么要尝试对其进行测试,只是删除该死的东西!

在您的示例中:

好吧,对于我来说,可以创建一个仅具有私有方法并通过侦听其他事件与其他对象进行交互的对象。这将被非常封装,但是完全不可测试。

为什么那是不可测的?如果调用该方法以响应事件,则只需让测试为对象提供适当的事件即可。

这不是关于没有私有方法,而是关于不破坏封装。您可以使用私有方法,但是应该通过公共API对其进行测试。如果公共API基于事件,则使用事件。

对于私有助手方法的更常见情况,可以通过调用它们的公共方法对其进行测试。特别是,由于只允许您编写代码以使失败的测试通过,并且您的测试正在测试公共API,因此您编写的所有新代码通常都将公开。私有方法仅在从现有公共方法中拔出时才作为Extract Method Refactoring的结果出现。但是在那种情况下,测试公用方法的原始测试仍然涵盖了专用方法,因为公用方法调用了专用方法。

因此,通常只有在从已经测试过的公共方法中提取私有方法并且因此也已经对其进行测试时,才会出现私有方法。


3
通过公共方法进行的测试在99%的时间内效果很好。挑战在于,当您的单个公共方法后面有数百或数千行复杂代码且所有中间状态都特定于实现时,这种情况的发生率为1%。一旦变得足够复杂,尝试使用公共方法处理所有极端情况就变得很痛苦。或者,通过破坏封装并将更多方法公开为私有方法来测试边缘情况,或者通过使用kludge让测试调用私有方法来直接测试边缘情况,这除了使丑陋之外,还导致了脆弱的测试情况。
Dan Neely'2

24
大型,复杂的私有方法是代码的味道。如此复杂的实现无法以有用的方式分解为组成部分(带有公共接口),是可测试性问题,它揭示了潜在的设计和体系结构问题。在私有代码庞大的情况下,有1%的情况通常会受益于返工以分解和公开。
S.Lott 2012年

13
@Dan Neely这样的代码无论如何都无法测试-编写单元测试的一部分指出了这一点。消除状态,分解类,应用所有典型的重构,然后编写单元测试。另外,在TDD中,您是如何做到这一点的?这是TDD的优点之一,可测试代码的编写变得自动。
Bill K

至少应该经常直接internalinternal类中的方法或公共方法进行测试。幸运的是,.net支持InternalsVisibleToAttribute,但没有它,测试这些方法将是PITA。
CodesInChaos 2012年

25

在代码中创建新类时,您可以这样做来满足一些要求。要求说什么的代码必须做,不能怎么样这使我们很容易理解为什么大多数测试都是在公共方法级别进行的。

通过测试,我们可以验证代码是否可以实现预期的功能,在预期的时间引发适当的异常等。我们并不真正在乎开发人员如何实现代码。尽管我们不在乎实现,即代码如何执行其工作,但避免测试私有方法是有意义的。

至于没有公共方法并且仅通过事件与外界交互的测试类,您也可以通过测试发送事件并监听响应来进行测试。例如,如果某个类每次接收到一个事件都必须保存一个日志文件,则单元测试将发送该事件并验证是否写入了日志文件。

最后但并非最不重要的一点是,在某些情况下,测试私有方法是完全有效的。这就是为什么在.NET中,您不仅可以测试公共类,还可以测试私有类,即使解决方案不像公共方法那样简单。


4
+1 TDD的一项重要功能是,它迫使您测试是否满足要求,而不是测试方法是否按照他们认为的去做。因此,“我可以测试私有方法”这个问题有点与TDD的精神背道而驰-问题可能是“我可以测试其实现包括私有方法的需求”。这个问题的答案显然是肯定的。
达伍德·伊本·卡里姆

6

据我了解,私有方法不可测试

我不同意该说法,或者我会说您不直接测试私有方法。公共方法可以调用不同的私有方法。也许作者想拥有“小型”方法,并将一些代码提取到一个巧妙命名的私有方法中。

无论如何编写公共方法,您的测试代码都应涵盖所有路径。如果在测试后发现测试中从未涉及一种私有方法中的一个分支语句(if / switch),那么您就有问题了。要么您错过了一个案例,而实现是正确的,要么实现是错误的,并且该分支实际上不应该存在。

这就是为什么我经常使用Cobertura和NCover的原因,以确保我的公共方法测试也涵盖私有方法。可以使用私有方法随意编写良好的OO对象,并且在这种情况下不要让TDD / Testing陷入困境。


5

只要您使用依赖注入来提供您的CUT与之交互的实例,您的示例就仍然可以完美测试。然后,您可以使用模拟,生成感兴趣的事件,然后观察CUT是否对其依赖项采取正确的操作。

另一方面,如果您的语言具有良好的事件支持,则可能会采取略有不同的方法。我不喜欢对象自己订阅事件,而是让创建对象的工厂将事件连接到对象的公共方法。它更易于测试,并使外部可见CUT需要测试的事件类型。


这是一个好主意-“ ...拥有创建对象的工厂,可以将事件与对象的公共方法联系起来。它更易于测试,并且可以在外部显示CUT需要测试的事件类型。 ”
幼犬

5

您无需放弃使用私有方法。使用它们是完全合理的,但是从测试的角度来看,在不破坏封装或向类中添加特定于测试的代码的情况下,直接测试变得更加困难。诀窍是尽量减少您知道会使蠕动的东西,因为您感觉自己已经弄脏了代码。

这些是我要记住的事情,它们试图达到可行的平衡。

  1. 尽量减少使用的私有方法和属性的数量。无论如何,您需要在课堂上做的大多数事情都需要公开公开,因此请考虑一下您是否真的需要将该聪明的方法私有化。
  2. 最小化私有方法中的代码量-无论如何,您确实应该这样做-并通过其他方法的行为进行间接测试。您永远都不会期望获得100%的测试覆盖率,也许您将需要通过调试器手动检查一些值。使用私有方法引发异常可以很容易地间接测试。私有属性可能需要手动或通过另一种方法进行测试。
  3. 如果间接或手动检查不适合您,请添加一个受保护的事件并通过接口访问以公开一些私有内容。这有效地“弯曲了”封装规则,但避免了实际交付执行测试的代码的需要。缺点是,这可能会导致一些额外的内部代码,以确保在需要时将触发该事件。
  4. 如果您认为公用方法不够“安全”,请查看是否有方法可以在方法中实施某种验证过程以限制其使用方式。可能是,当您通过考虑一种更好的方法来实现您的方法的方式进行思考时,或者您会看到另一个类开始成形。
  5. 如果您有很多私有方法为您的公共方法做“工作”,则可能有一个新类正在等待提取。您可以将其作为单独的类直接进行测试,但可以作为使用它的类中的一个复合体来实现。

横向思考。使您的类变小,方法变小,并使用大量的组合。听起来需要做更多的工作,但最后您将获得更多可单独测试的项目,测试将变得更加简单,您将有更多选择使用简单的模拟代替真实的,大型的和复杂的对象,希望-因数和松散耦合的代码,更重要的是,您将为自己提供更多选择。尽量减少内容的使用量最终会节省您的时间,因为减少了每个类需要单独检查的内容,而且自然地减少了当类变大且包含很多内容时有时会发生的代码错误内部相互依赖的代码行为。


4

好吧,对于我来说,可以创建一个仅具有私有方法并通过侦听其他事件与其他对象进行交互的对象。这将被非常封装,但是完全不可测试。

该对象如何应对这些事件?据推测,它必须在其他对象上调用方法。您可以通过检查是否调用了这些方法来进行测试。让它调用一个模拟对象,然后您可以轻松地断言它可以满足您的期望。

问题是我们只想测试对象与其他对象的交互。我们不在乎对象内部发生了什么。所以不,您不应该再有其他公共方法。


4

我也为同样的问题而苦恼。确实,解决该问题的方法是:您如何期望程序的其余部分与该类交互?相应地测试您的课程。 这将迫使您根据程序的其余部分如何与之交互来设计类,并且实际上将鼓励对类进行封装和良好的设计。


3

代替私有使用默认修饰符。然后,您可以单独测试这些方法,而不仅仅是与公共方法结合使用。这要求您的测试与主代码具有相同的包结构。


...假设这是Java。
达伍德·伊本·卡里姆

internal.net中。
CodesInChaos

2

一些私有方法通常不是问题。您只需通过公共API测试它们,就像将代码内联到您的公共方法中一样。过多的私人方法可能表明凝聚力不佳。您的班级应该承担一种内聚的责任,并且通常人们将方法私有化,以使内聚力的外观真正不存在。

例如,您可能有一个事件处理程序,该事件处理程序响应这些事件进行了许多数据库调用。由于实例化事件处理程序进行数据库调用显然是不好的做法,因此诱惑是使所有与数据库相关的调用成为私有方法,而实际上应该将它们提取到单独的类中。


2

这是否意味着TDD与封装不一致?适当的余额是多少?我现在倾向于公开大多数或所有方法。

TDD与封装并不矛盾。根据您选择的语言,以最简单的getter方法或属性为例。假设我有一个Customer对象,并且希望它具有一个ID字段。我要编写的第一个测试是说“ customer_id_initializes_to_zero”的内容。我定义吸气剂引发未实现的异常并观察测试失败。然后,我要做的最简单的事情就是让getter返回零。

从那里开始,我继续进行其他测试,大概是那些涉及客户ID的实际功能领域的测试。在某个时候,我可能必须创建一个私有字段,客户类使用该私有字段来跟踪getter应该返回的内容。我到底该如何追踪?这是一个简单的支持int吗?我要跟踪一个字符串然后将其转换为int吗?我是否跟踪20个整数并取平均值?外界不在乎-您的TDD测试也不在乎。那是一个封装的细节。

我认为这在启动TDD时并不总是立即显而易见的-您未测试内部使用的方法-您正在测试对类的关注程度较小的问题。因此,您并不是要测试该方法DoSomethingToFoo()实例化Bar,在其上调用方法,在其属性中添加两个,等等。 (或不)。那就是测试的一般模式:“当我对被测类进行X训练时,我随后可以观察到Y”。到达Y的方式与测试无关,这就是封装的原因,这就是为什么TDD与封装不矛盾的原因。


2

避免使用?否。
避免开头 是。

我注意到您没有询问使用TDD抽象类是否可以;如果您了解TDD期间抽象类是如何出现的,则相同的原理也适用于私有方法。

您不能像直接测试私有方法那样直接测试抽象类中的方法,但是这就是为什么您不从抽象类和私有方法开始的原因。您从具体的类和公共API开始,然后在进行过程中重构常用功能。

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.