@H_502_1@自动生成的测试与@H_502_1@TDD
@H_502_1@@H_502_1@Posted by Uncle Bob
@H_502_1@测试驱动的开发方式@H_502_1@(TDD)@H_502_1@现在已经很流行了,很多公司都在采用。不过,一些人也当心这会花费太多的时间去写单元测试,所以并一直在寻找自动生成测试的工具,以降低编写单元测试的负担。
@H_502_1@这负担可不是无关紧要的。在很恰当地使用@H_502_1@TDD@H_502_1@后,创建一个具有@H_502_1@45,000@H_502_1@行@H_502_1@Java@H_502_1@代码的应用,其中有@H_502_1@15,000@H_502_1@行代码是单元测试。我们可以很快计算出@H_502_1@TDD@H_502_1@增加了编码负担的整整@H_502_1@1/3@H_502_1@!
@H_502_1@当然了,这是一个消极的分析。使用@H_502_1@TDD@H_502_1@带来的好处是很重要的,其价值远远超出了编写‘额外代码的负担。但是,@H_502_1@33@H_502_1@%的测试代码仍然觉得过于“额外”,这也诱使人们去寻找回避@H_502_1@TDD@H_502_1@的方法,并且这个方法将不会失去@H_502_1@TDD@H_502_1@带来的任何好处。
@H_502_1@测试生成器
@H_502_1@很多朋友都希望工具在检查代码的同时能够自动生成测试代码。这样的工具是相当聪明的。
@H_502_1@它们可以随机生成方法调用以及记录下返回结果。它们能够自动构建模拟对象@H_502_1@(mocks)@H_502_1@以及存根对象@H_502_1@(stubs)@H_502_1@,从而解决模块依赖的问题。它们很精明的算法去选择随机测试数据。它们甚至以插件@H_502_1@(plugins)@H_502_1@的方式提供给程序员调整那些测试数据选择算法,以便更适合应用程序的测试。
@H_502_1@这样工具的运行结果是观察点的一个集合。当带有确定参数的调用发生时,这个工具观察类实例变量的改变。它会注意调用的返回值,实例变量值的改变,还有对存根对象@H_502_1@(stubs)@H_502_1@与模拟对象的调用。所有的这些都作为观察展现给他的使用者。
@H_502_1@使用者必须查看所有的观察点,判断其中哪些是正确的,哪些是不相干的,哪些是@H_502_1@Bugs@H_502_1@。@H_502_1@Bugs@H_502_1@被修复后,测试依然要一次又一次地被运行测试。对于使用@H_502_1@GUI@H_502_1@的测试者,这与录音机的回放很类似。一旦收到了所有正确的观察结果,你也能返回测试的开始并确认所有的观察点仍然是可被观察到的。
@H_502_1@这样的工具一般将以@H_502_1@JUnit@H_502_1@测试的方式生成观察点,所以你能像运行测试包一样地运行这些观察点。这过程很像@H_502_1@TDD@H_502_1@,不是吗?呃,别着急……
@H_502_1@如果不犯错的话,工具确实相当有用。如果你有一堆未测试过的遗留代码,然后生成一个@H_502_1@JUnit@H_502_1@的测试包去检验那些代码的部分行为,这样是多么的惬意啊!
@H_502_1@外围的问题
@H_502_1@另一个方面,无论测试生成器如何的聪明,它生产的测试总是比人类写的要幼稚一些。举个简单的例子,我曾经尝试过使用两个很好的测试生成器去生成一个保龄球游戏程序的测试。这个保龄球游戏的接口如下示:
@H_502_1@public class BowlingGame { @H_502_1@public void roll(int pins) {...} @H_502_1@public int score() {...} @H_502_1@}
@H_502_1@你可以掷出球使用@H_502_1@roll@H_502_1@方法,也能取得得分使用@H_502_1@score@H_502_1@方法。
@H_502_1@测试生成器不能随机地生成可用的游戏局。问题很明显,一次游戏是一个在@H_502_1@12@H_502_1@与@H_502_1@21@H_502_1@次掷球之间的序列,每次得分是@H_502_1@0@H_502_1@到@H_502_1@10@H_502_1@之间的整数。最重要的就是在一小局@H_502_1@(frame)@H_502_1@内,得分不能超过@H_502_1@10@H_502_1@。这个限制对于目前宇宙中的随机数生成器来说过于严格了。
@H_502_1@我写了一个插件去指导生成器创建可用的游戏;但是,这样一个指导算法比保龄球游戏自身更具有逻辑性,所以它是不够聪明的。
@H_502_1@测试生成器遇到了不属于任何类型的内部算法的问题,如调用时序,或者是状态语义。它们可以生成测试在这样待测类的周边,但是不能在没有帮助的情况下进入到其内部。
@H_502_1@TDD?
@H_502_1@实际的问题是在使用测试驱动的开发@H_502_1@(TDD)@H_502_1@时,这样生成的测试是否能帮助你。@H_502_1@TDD@H_502_1@扮演了使用测试者的角色从而驱动系统的开发。
@H_502_1@首先,你编写单元测试代码,然后你编写通过测试的应用程序 代码。显然从既有代码生成测试违反了这个简单规则。 因此,在一些开发哲学观念里,使用测试生成器不是@H_502_1@TDD@H_502_1@实践。但是只要编写了测试,谁会在意这些测试是怎么写出来的,不是吗?呃,等等……
@H_502_1@TDD@H_502_1@的一个优点很类似双条目的帐目登记。会计师做一个条目两次;一次在借出方,另一次在在借入方。这两个条目遵循独立条件的数学方法。最后,一个不可思议的减法产生了@H_502_1@0@H_502_1@如果两边的条目是正确清算的话。
@H_502_1@在@H_502_1@TDD@H_502_1@里,程序员陈述他们的意图两次;一次是测试代码,另一次是产品代码。这两次陈述的意图是相互验证。测试一方,验证产品代码的意图,而产品代码验证测试代码的意图。这样的方式能够工作是因为只有人类才能做两次条目!虽然必须陈述意图两次,但是是以补充的形式。这大大降低了各种错误;也提供了增强设计至关重要的洞察力。
@H_502_1@使用测试生成器打破了这个概念,因为生成器使用产品代码作为输入编写测试。生成的测试不是人类的重述行为,它只是一个自动的翻译记过。这样做时,人类只陈述了一次意图,因此很难获得重述时的洞察力,生成的测试也不能检验产品代码的意图 。人们必须验证观察点的倒是真的,而与@H_502_1@TDD@H_502_1@相比较的话,这只是一个负面的行为。提供了很少发现缺陷、设计、意图方面的洞察力。
@H_502_1@我的结论就是自动测试生成与@H_502_1@TDD@H_502_1@是不等价的,也不是提高@H_502_1@TDD@H_502_1@效率的有效途径。当你尝试生成那@H_502_1@33@H_502_1@%的测试代码时,你失去了消灭缺陷、重述意图还有增强设计的洞察力。你也牺牲了测试覆盖率的深度,因为那些外围的问题。
@H_502_1@不过,这不是说测试生成器没有用处。就像我前面说的,我认为他们的确能够帮助我们看到遗留代码的部分特征。但是,请记住,这些工具不是@H_502_1@TDD@H_502_1@工具。他们生成的测试不等价于@H_502_1@TDD@H_502_1@时所编写的测试,@H_502_1@TDD@H_502_1@给我们带来的好处是测试生产所不能的。
原文链接:https://www.f2er.com/javaschema/288107.html