编写可测试代码与避免投机性


11

我今天早上在看一些博客文章,偶然发现了这一篇

如果唯一实现Customer接口的类是CustomerImpl,那么您实际上就没有多态性和可替代性,因为实际上在运行时没有什么可以替代。这是假的普遍性。

这对我来说很有意义,因为实现接口会增加复杂性,而且,如果只有一种实现,则可能会争辩说它会增加不必要的复杂性。编写比需要的抽象要抽象得多的代码通常被认为是被称为“投机性一般性”的代码气味(在文章中也提到了)。

但是,如果我遵循TDD,那么如果没有这种推测性的普遍性(无论是接口实现形式还是我们的其他多态选项),就无法(轻松)创建测试双打,从而使类可继承且其方法是虚拟的。

那么,我们如何协调这种权衡呢?以投机性方式推广测试/ TDD是否值得?如果您使用的是测试双打,是否将这些视为第二种实现方式,从而使普遍性不再具有推测性?您是否应该考虑一个更重的模拟框架,该框架可以模拟具体的协作者(例如,C#世界中的Moles与Moq)?或者,您应该使用具体的类进行测试,并编写什么可以被视为“集成”测试,直到您的设计自然需要多态性之前?

我很想阅读其他人对此事的看法-预先感谢您的意见。


我个人认为实体不应该被嘲笑。我只模拟服务,并且在任何情况下都需要一个接口,因为代码域代码通常没有引用实现服务的代码。
CodesInChaos 2012年

7
我们使用动态类型语言的用户嘲笑您对静态类型语言给您的束缚。这是使使用动态类型语言进行单元测试更加容易的一件事,我不必开发接口即可将对象替换为测试目的。
Winston Ewert'2

接口不仅用于实现通用性。它们用于许多目的,将代码解耦是重要的之一。反过来,这使对代码的测试变得非常容易。
Marjan Venema

@WinstonEwert这是动态输入的一个有趣的好处,正如您所指出的那样,我以前从未考虑过这种动态输入的人通常不会使用动态输入的语言。
Erik Dietrich'2

@CodeInChaos我没有考虑此问题的区别,尽管这是一个合理的区别。在这种情况下,我们可能会想到只有一个(当前)实现的服务/框架类。假设我有一个可以通过DAO访问的数据库。在拥有辅助持久性模型之前,是否应该不与DAO交互?(这似乎是博客作者所暗示的意思)
Erik Dietrich'2

Answers:


14

我去阅读了博客文章,我同意很多作者所说的话。但是,如果您出于接口测试目的而使用接口编写代码,那么我想说接口的模拟实现您的第二种实现。我认为这确实不会给您的代码增加太多复杂性,尤其是如果不这样做会导致您的类紧密耦合且难以测试的话。


3
绝对正确。测试代码应用程序的一部分,因为您需要它才能正确地进行设计,实施,维护等。您不将其交付给客户这一事实是无关紧要的-如果您的测试套件中有第二种实现,那么存在普遍性,您应该适应它。
Kilian Foth

1
这是我认为最有说服力的答案(@KilianFoth补充说,代码是否附带第二种实现仍然存在)。我将暂缓接受答案,以查看是否还有其他人进入。
Erik Dietrich'2

我还要补充一点,当您依赖于测试中的接口时,普遍性不再是推测性的
Pete

“不这样做”(使用接口)不会自动导致您的类紧密耦合。只是没有。例如,在.NET Framework中,有一个Stream类,但没有紧密耦合。
路加·普普利特

3

一般来说,测试代码并不容易。如果是这样的话,我们很久以前就已经做过,而且仅在过去10到15年中才做得这么多。最大的困难之一一直是确定如何测试以内聚方式编写,结构合理,可测试而不破坏封装的代码。BDD负责人建议我们几乎完全将重点放在行为上,并且在某些方面似乎表明您并不需要真正担心内部细节如此之大,但这通常会使事情很难测试。许多私有方法以非常隐蔽的方式进行“填充”,因为这会增加测试的整体复杂性,以便在更公共的级别处理所有可能的结果。

模拟可以在一定程度上有所帮助,但同样是外部关注。依赖注入也可以很好地工作,再次与模拟或测试双打配合使用,但这也可能需要您通过接口或直接公开元素,否则可能更希望保持隐藏状态-如果您希望这样做,尤其如此。对系统中的某些类具有很好的偏执性安全级别。

对我而言,对于是否将您的课程设计为更易于测试的问题,尚无定论。如果您发现自己需要在维护旧代码的同时提供新的测试,则会造成问题。我接受您应该能够绝对测试系统中的所有内容,但我不喜欢公开(甚至间接公开)类的私有内部结构的想法,只是为了让我可以为它们编写测试。

对我而言,解决方案始终是采取一种相当务实的方法,并结合多种技术以适应每种特定情况。我使用许多继承的测试双打来暴露测试的内部属性和行为。我模拟了可以附加到类上的所有内容,并且在不影响类安全性的情况下,我将提供一种方法来覆盖或注入行为以进行测试。我什至会考虑提供更多事件驱动的界面,如果它有助于提高测试代码的能力

在找到任何无法测试”的代码的地方,我看是否可以重构以使事情更可测试。在您有很多隐藏在后台的私有代码执行的地方,通常您会发现新的类正在等待被突破。这些类可以在内部使用,但通常可以以较少的私有行为进行独立测试,并且随后通常可以减少访问和复杂性的层数。但是,我确实要避免的一件事是使用内置的测试代码编写生产代码。创建“ 测试突耳 ” 可能会很诱人,从而导致诸如的恐怖if testing then ...,这表明测试问题并未得到完全解构和未完全解决。

您可能会发现阅读Gerard Meszaros的xUnit Test Patterns本书很有帮助,该书比我在此处介绍的所有内容都详尽得多。我可能并没有按照他的建议去做,但是确实有助于弄清一些棘手的测试情况。归根结底,您希望能够在继续应用首选设计的同时满足测试要求,这有助于更好地了解所有选项,以便更好地确定可能需要折衷的地方。


1

您使用的语言是否可以“模拟”测试对象?如果是这样,这些烦人的界面就会消失。

另一方面,可能有理由拥有一个SimpleInterface和一个实现它的唯一ComplexThing。可能您不希望SimpleInterface用户访问某些ComplexThing。并非总是因为过度的OO-ish编码器。

我现在走开,让每个人都跳一个事实,即这样做的代码对他们“很不好”。


是的,我使用的语言带有支持模拟具体对象的模拟框架。这些工具需要不同程度的跳跃。
Erik Dietrich '02

0

我将分两部分回答:

  1. 如果您只对测试感兴趣,则不需要界面。为此,我使用了模拟框架(在Java中:Mockito或easymock)。我认为您设计的代码不应出于测试目的而被修改。编写可测试的代码等同于编写模块化代码,因此我倾向于编写模块化(可测试)代码,并且仅测试代码公共接口。

  2. 我一直在一个大型Java项目中工作,并且我深信使用只读(仅getter)接口是行之有效方法(请注意,我是不变性的忠实拥护者)。实现类可能有setter,但这是一个实现细节,不应暴露给外层。从另一个角度来看,我确实更喜欢组合而不是继承(模块化,还记得吗?),因此接口在这里也有帮助。我愿意付出投机的笼统性,而不是朝自己的脚下投弹。


0

自从我开始对多态性以外的接口进行编程以来,我已经看到了许多优点。

  • 这迫使我事先更加认真地考虑类的接口(它是公共方法)以及它如何与其他类的接口交互。
  • 它帮助我编写更紧凑且遵循单一职责原则的较小类。
  • 测试我的代码更容易
  • 静态类/全局状态较少,因为该类必须是实例级的
  • 在整个程序准备就绪之前,更容易将零件集成和组装在一起
  • 依赖注入,将对象构造与业务逻辑分离

许多人会同意,更多,更小班级比更少,更大班级更好。您不必一次集中精力,每个班级都有明确的目的。其他人可能会说,您拥有更多的类会增加您的复杂性。

使用工具来提高生产率是很好的选择,但是我认为仅依靠Mock框架,而不是将可测试性和模块化直接构建到代码中,从长远来看将导致较低质量的代码。

总而言之,我相信它帮助我编写了更高质量的代码,而带来的好处远远超过了任何后果。

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.