unit testing – unit testing带来合同变化的好处?

最近我和一个同事讨论unit testing。 我们正在讨论当你的合同改变时,维持unit testing的效率降低了。

也许任何人都可以让我知道如何解决这个问题。 让我详细说明一下:

所以,让我们说有一个类可以做一些漂亮的计算。 合同说它应该计算一个数字,或者它由于某种原因失败时返回-1。

我有合同testing谁testing。 而在我所有的其他testing中,我把这个漂亮的计算器简直就是存在的。

所以现在我改变了合同,每当它不能计算的时候就会抛出一个CannotCalculateExceptionexception。

我的合同testing将失败,我会相应地修复它们。 但是,我所有的嘲弄/残留物体仍然会使用旧的合同规则。 这些testing会成功,而他们不应该!

问题在于,unit testing的这种信念,在这样的变化中可以有多less信心……unit testing成功了,但是在testing应用程序时会出现错误。 使用这个计算器的testing将需要被固定,这会花费时间,甚至可能会被扼杀/嘲笑很多次。

你怎么看这个案子? 我从未想过这件事。 在我看来,unit testing的这些变化是可以接受的。 如果我不使用unit testing,我也会在testing阶段(testing人员)看到这样的错误。 但是我没有足够的信心指出什么会花费更多的时间(或更less)。

有什么想法吗?

你提出的第一个问题是所谓的“脆弱testing”问题。 您对应用程序进行了更改,并且由于该更改而导致数百个testing中断。 发生这种情况时,您有devise问题。 您的testing被devise为脆弱的。 它们没有与生产代码充分分离。 解决scheme(就像所有这样的软件问题一样)find一个抽象,将testing从生产代码中解耦出来,这样生产代码的波动性就从testing中隐藏起来了。

导致这种脆弱性的一些简单的事情是:

  • testing显示的string。 这样的string是不稳定的,因为它们的语法或拼写可能随分析师的意愿而改变。
  • testing离散值(例如3),应在抽象背后进行编码(例如FULL_TIME)。
  • 从许多testing中调用相同的API。 您应该将API调用包装在testing函数中,以便在API更改时您可以在一个位置进行更改。

testingdevise是TDD初学者经常忽略的一个重要问题。 这经常导致脆弱的testing,然后导致新手拒绝TDD为“非生产性”。

你提出的第二个问题是误报。 你已经使用了太多的模拟testing,没有一个testing实际testing集成系统。 虽然testing独立单元是一件好事,但testing系统的部分和整体集成也很重要。 TDD不仅仅是unit testing。

testing应安排如下:

  • unit testing提供接近100%的代码覆盖率。 他们testing独立单位。 它们是由程序员使用系统的编程语言编写的。
  • 组件testing覆盖了大约50%的系统。 它们由业务分析师和QA编写。 它们是用FitNesse,Selenium,Cucumber等语言编写的。它们testing整个组件,而不是单个的单元。 他们主要testing快乐path案例和一些高度可见的不快乐path案例。
  • 集成testing覆盖了大约20%的系统。 他们testing小部件组件而不是整个系统。 也写在FitNesse / Selenium /黄瓜等由build筑师写的。
  • 系统testing涵盖系统的约10%。 他们testing整个系统整合在一起。 再次,他们写在FitNesse / Selenium /黄瓜等由build筑师写的。
  • 探索性手动testing。 (见詹姆斯·巴赫)这些testing是手动的,但不是脚本。 他们运用人的独创性和创造力。

最好不得不修复由于有意的代码更改而失败的unit testing,而不是通过testing来捕获这些更改最终引入的错误。

当您的代码库具有良好的unit testing覆盖率时,您可能会遇到许多unit testing失败,这些失败不是由于代码中的错误,而是在合同或代码重构上的有意更改。

但是,unit testing覆盖率也将使您有信心重构代码并执行任何合同更改。 有些testing会失败,需要修复,但是其他testing最终会因为这些更改引入的错误而失败。

unit testing肯定不能捕捉到所有的错误,即使在100%代码/function覆盖率的理想情况下。 我认为这是不可预料的。

如果testing合同发生变化,我(开发人员)应该用我的大脑相应地更新所有代码(包括testing代码!)。 如果我没有更新某些嘲讽,因此仍然会产生旧的行为,那是我的错,而不是unit testing。

这与我修正一个错误并产生一个unit testing的情况类似,但是我没有考虑所有类似的情况,其中一些后来certificate是错误的。

所以是的,unit testing和生产代码本身一样需要维护。 没有维护,他们腐烂腐烂。

我有类似的unit testing经验 – 当你经常改变一个类的合同时,你也需要改变其他testing的负载(这在很多情况下实际上会通过,这使得它更加困难)。 这就是为什么我总是使用更高级别的testing:

  1. 验收testing – testing几个或更多的课程。 这些testing通常与需要实施的用户商店alignment – 因此您testing用户故事“有效”。 这些不需要连接到数据库或其他外部系统,但可以。
  2. 集成testing – 主要检查外部系统连接性等
  3. 全面的端到端testing – testing整个系统

请注意,即使你有100%的unit testing覆盖率,你甚至不能保证你的应用程序启动! 这就是为什么你需要更高水平的testing。 有很多不同的testing层,因为你testing的东西越低,通常就越便宜(在开发,维护testing基础设施以及执行时间方面)。

作为一个方面说明 – 由于你提到的使用unit testing的问题,教你保持你的组件尽可能分离,他们的合同尽可能小 – 这绝对是一个很好的做法!

有人在Google Group中提出了“面向对象的软件 – 以testing为导向”的相同问题。 线程是unit testing模拟/存根假设腐烂 。

这里是JB Rainsberger的回答 (他是Manning的“ JUnit食谱 ”的作者)。

unit testing代码(以及用于testing的所有其他代码)的规则之一是将其与生产代码相同的方式处理 – 不多也不less – 只是相同。

我对此的理解是(除了保持相关性,重构,工作等生产规范之外),也应该从投资/成本的angular度来看待它。

或许你的testing策略应该包括一些东西来解决你在第一篇文章中描述的问题 – 当devise师改变时,指定什么testing代码(包括存根/模拟)应该被检查(执行,检查,修改,固定等)生产代码中的一个函数/方法。 因此,任何产品代码变更的成本必须包括这样做的成本,否则testing代码将成为“三等公民”,devise师对unit testing套件的信心以及相关性将降低。显然,ROI正处于发现和修复错误的时间。

我所依靠的一个原则就是消除重复。 我通常没有很多不同的假货或嘲笑执行这个合同(我使用更多的假货比嘲笑部分出于这个原因)。 当我更换合同时,检查合同,生产代码或testing的每个实施是自然的。 当我发现我正在做这种改变的时候,它会让我感到困惑,我的抽象应该已经被更好地考虑过了,但是如果testing代码对于合同改变的规模来说过于繁重,那么我不得不问自己是否这些也是由于一些重构。

我这样看,当你的合同变了,你应该把它当作新的合同来对待。 因此,您应该为这个“新”合同创build一套全新的UNITtesting。 事实上,你有一套现有的testing案例是重点。

我第二叔叔鲍勃的意见是问题在于devise。 我会另外回去一步, 检查你的合同的devise

简而言之

在适当的情况下,使用niftyCalcuatorThingy(x,y) x!=y && x!=0来指定niftyCalcuatorThingy(x,y) ,而不是说“return x for x == 0”或者“抛出CannotCalculateException for x == y” 。 因此,对于这些情况,您的存根可能会任意行为,您的unit testing必须反映这一点,并且您拥有最大的模块性,即可以随意更改所有未详细说明的testing系统的行为 – 无需更改合同或testing。

在适当的地方没有规范

根据以下标准,您可以区分您的陈述“-1由于某种原因失败时”:是情景

  1. 执行可以检查的exception行为?
  2. 在方法的域/职责范围内?
  3. 调用者(或调用堆栈中较早的某个人)可以从其他方式恢复/处理的exception?

当且仅当1)至3)保持时,指定合约中的场景(例如,在空栈上调用pop()时引发EmptyStackException )。

没有1),在例外的情况下,实现不能保证一个特定的行为。 例如,当不满足自反性,对称性,传递性和一致性的条件时,Object.equals()没有指定任何行为。

没有2),SingleResponsibility原则不符合,模块化被打破,用户/读者的代码混淆。 例如, Graph transform(Graph original)不应该指定可能抛出MissingResourceException因为深层次的,通过序列化的一些克隆完成。

没有3),调用者不能使用指定的行为(某些返回值/exception)。 例如,如果JVM抛出一个UnknownError。

优点和缺点

如果你确定了1),2)或3)不成立的情况,你会遇到一些困难:

  • (devise)合同的主要目的是模块化。 如果你真的把责任分开了,这是最好的实现:当前提条件(调用者的责任)没有得到满足时,没有指定实现的行为会导致最大的模块化 – 正如你的例子所示。
  • 你在将来没有任何改变的自由,甚至没有更less的情况下抛出exception的更一般的function
  • 例外行为会变得相当复杂,因此涵盖它们的合同变得复杂,容易出错,难以理解。 例如:是否涵盖了每一种情况? 如果有多个特殊的前提条件,哪个行为是正确的?

不足之处在于(testing)鲁棒性,即实现对exception情况作出适当反应的能力,是比较困难的。

作为妥协,我喜欢在可能的情况下使用以下合约模式:

<(半)正式的PRE和POST条件,包括1)至3)保持>的特殊行为

如果PRE未被满足,则当前实现抛出RTE A,B或C.