-
单元测试一问一答 - [Test]
2008-10-11
Q:单元测试的主要作用有哪些?

A:单元测试的作用主要有:
(1) 确保代码实现了预想的逻辑。这是所有测试都有的,也是最重要的功能。
(2) 确保重构不破坏现有的功能。所有测试都或多或少地支持这一功能,有些“重构不友好”的测试在重构过程中可能会被删除或修改。
(3) 驱动出设计,主要是帮助识别出类或接口函数。这是个一次性的好处,只有部分测试会起到这个作用。那些直接保证内部函数实现逻辑的测试起不到这个作用。
(4) 当难以编写满意的测试时,提示开发人员现有设计的问题并鼓励他们重构现有的代码。这也是个一次性的好处,只有部分测试会起到这个作用。
(5) 作为需求文档的一部分记录了实现的一项项具体需求。大部分测试都有这个功能,那些直接保证内部函数实现逻辑的测试不具备这个功能。其中2、3和4都体现了单元测试对设计的帮助作用。
Q:如何认识一个单元测试的成本与收益?
A:毫无疑问,单元测试是需要成本的,它包括测试的编写、运行和维护修改的成本等。仅从测试代码与产品代码数量的对比上看就能清楚我们为单元测试付出了多少代价。
单元测试在如下几个时机为我们带来收益:
* 由于破坏了现有的功能导致测试运行失败的时候。(单元测试的第1、2个作用)
* 写测试的时候,我们更容易从使用者的角度识别出新的类和接口函数。(单元测试的第2个作用)
* 写测试遇到的痛苦帮我们下决心重构现有代码的时候。(单元测试的第3个作用)
* 看测试的名字了解到类具体的契约。(单元测试的第4个作用)单元测试在如下几个时机让我们付出更多代价:
* 测试运行失败,但它所要确保的功能并没有被破坏的时候。这可能是由于修改了实现代码或进行了重构,此时需要fix测试本身而不是实现代码。
* 测试失败了,但其可读性很差,很难找到问题的根源的时候。可见,并不是每个单元测试都有价值,不同单元测试的价值也可能相差很远。对一个从来没有失败过的测试,它存在的必要性就值得被质疑。一个失败了很多次,但没有一次是因为Break功能而失败的测试,更是让人头疼!
写一个低成本、高收益的单元测试并不那么容易。
Q:怎么判断一个单元测试的好坏?
A:一个理想的单元测试,有如下特征:
(1) 保障了一个含义清晰的逻辑。测试完整地覆盖了一个可识别的逻辑,或其一部分。该逻辑的变化会导致测试失败。测试不会对于逻辑无关的部分有任何假设,只有和逻辑相关部分的变化会导致测试失败。
(2) 可读性好。测试容易编写,容易读懂。
(3) 重构友好。实现可以随意修改,只要最终行为不变,测试不会失败也不需要修改。
(4) 逻辑不重复。每个测试保障一个独立的逻辑,相互之间没影响。同一个逻辑不被多个测试覆盖,不会因为一个逻辑的变化影响一群测试。
(5) 反馈距离近。测试失败后很容易找到是那里的问题。作为可运行的程序,还要求:
(6) 健壮性好,可多次重复运行并有一致的结果。
(7) 独立运行,运行时互不干扰。
(8) 运行速度尽量快。后面3个运行时的要求互相没有影响,可同时满足,但前面4个要求很难同时满足。比如一个集成测试,可能满足了1、2、3条,但与其它单元测试有重复,而且反馈距离远。而一个Mock测试,可能满足了1、4、5条,但可读性较差,重构代码时测试也要跟着修改。
Q:在Selenium测试、集成测试、单元测试、黑盒或白盒测试、Mock和非Mock测试之间我该如何选择?
A:我认为为测试做一个细致的归类实在没什么太大的现实意义。重要的是你有一个测试,它保证了一个功能,不管别人叫它Mock、Stub或是别的什么都没有那么重要了。
针对一个逻辑,我们可以选择不同风格的方法去编写测试,可以是几个黑盒的测试,或是几个白盒的测试,重要的是我们要清楚各种选择的优劣,基于这一理解的任何选择都是合理的。
我对各种风格测试在可读性、重构友好性、逻辑不重复和反馈距离近等几个方面做以比较:
* 表现较差
** 表现中等
*** 表现优秀黑盒风格测试 白盒风格测试 Mock测试 集成测试 Selenium测试 可读性 *** ** * *** ** 重构友好性 ** * * *** *** 逻辑不重复 ** *** *** * * 反馈距离近 ** *** *** * * 我个人比较看重可读性和重构友好性,因此倾向于选择黑盒风格的测试。
Q:坚持测试先行一定会导致较好的设计吗?
A:上面提到过,测试在3个方面对设计有帮助,测试先行涉及到了其中2个方面:
* 帮助识别出类或接口函数。测试先行给我们一个机会在没有任何代码实现的时候从使用者的角度去发现新的类并考虑为它设计什么样的接口,同时为接口先行写一个可运行的规约。
* 当难以编写满意的测试时,提示开发人员现有设计的问题并鼓励他们重构现有的代码。测试先行更容易产生较好的设计,但不必然产生好的设计。实事上,测试先行在保证设计上的能力远没有期望的那么大。一个实现了良好设计的人就算没有测试先行想必他的设计结果也不会差到哪里去,这个人本身的设计功底和对问题域的理解才是最重要的,其次,保持简单设计,尽早地应用自己的设计并根据反馈改进设计,以及测试先行等等都会对设计结果有所帮助。
Q:一定要测试先行才好吗?
A: 总体来讲,是要测试先行的,先行的测试会给眼前的工作指定一个明确的价值目标,容易形成良好的开发节奏,另外它还有助于形成良好的设计。但是在某些场合,可能就没必要追求测试先行了,因为此时先写测试不但很别扭,也发挥不出来前面的那些好处,比如:
* 测试依赖一个没有把握的设计构想。此时可以花上很短的一段时间,忽落任何测试,迅速编码验证这一构想。如果证明是可行的,就Revert之前的代码,按测试优先的流程去开发。
* 有些Mock测试,需要多次调用被Mock的接口函数,这时如果先写测试,就可能很别扭,要绞尽脑汁空想实现细节,难免有所疏漏,往往要在后面运行测试失败后再来修改。此时直接去写实现代码是最自然的。当然,这样的测试无论是先写还是后写都与实现绑定在了一起,是一种重复,可能意味着需要重构代码了。
* 有时,直接在实现处写些代码比划比划,要比先写测试更容易识别出新的类或接口函数,这时就可以先写些实现代码,驱动出新类或接口函数,再对这个类或接口函数进行测试,最后再去实现这个新类或接口函数。总之,测试先行不是教条,我们要的是这样做的好处,而不是这个做法本身。
Q:测试越多越好吗?
A:我认为,对逻辑的覆盖率(不是代码行的覆盖率)以及测试的质量(前面提到的成本与收益)才是最重要的,在保证这两点的基础上,测试越少越好。
收藏到:Del.icio.us