软件测试中单元测试和测试驱动开发(TDD)杂谈

发表于:2010-11-12来源:作者:点击数: 标签:tddTDDTdd软件测试单元
软件测试中单元测试和 测试驱动开发 ( TDD )杂谈 在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的 面向对象 的语言中, 要进行测试的基本单元是类。对Ada语言来说,开发人员可以选择是在独立的过程和函数,还是

软件测试中单元测试和测试驱动开发TDD)杂谈

在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中, 要进行测试的基本单元是类。对Ada语言来说,开发人员可以选择是在独立的过程和函数,还是在Ada包的级别上进行单元测试。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里基本单元被典型地划分为一个菜单或显示界面。   经常与单元测试联系起来的另外一些开发活动包括代码走读(Code review),静态分析(Static analysis)和动态分析(Dynamic analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。动态分析就是通过观察软件运行时的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息。

最近公司要求重新回顾单元测试的实际效果,作为一个开发经理,我个人对单元测试也有很多疑惑。就个人而言,我自己也写过很多单元测试,也鼓励程序员写单元测试,但实际效果似乎不尽如人意。因此,写了这篇短文,想和大家一起探讨。

1. 背景介绍

我所在的公司是一家外资软件公司,主要工作是开发一个复杂的在线系统(java based web applicaiton). 该系统的主要特点是:定制化程度比较高,业务逻辑相当复杂。系统的技术栈是Struts, EJB (JBoss)and Hibernate。我管理的小组一共有10个左右开发人员,6个左右测试人员,平均工作经验在3年以上。

公司在两年前开始推行单元测试。在开始推行单元测试之前,系统已经正式上线,也就是意味着有海量的没有单元测试的代码。推行之后,应该说投入了相当多的时间,总共覆盖的行数有20k, 其中行覆盖率(line coverage)有55%左右,分支覆盖率(branch coverage)有40%左右。我相信经过这么多尝试,应该说,我带的这个组不是一个单元测试的新手,有资格讨论单元测试的得失。

2. 实践中的问题和疑惑(开发人员怎么说?)

我一向主张:一项技术值不值得或者好不好用归根结底是要问实际的使用者和开发者的。作为一个经理,我不倾向于推行一项程序员极力反对的技术,不管这项技术是不是业界的标准或者是评论者的宠儿。一项技术必须要解决实际问题,也就是mark your life easier。所以下面是开发人员的回答。

2.1为什么需要单元测试和TDD (Test Driven Development)?

2.2.1 单元测试可以发现代码缺陷(Defect)么?投入/产出比(Defect count/Effort)是多少?

只能发现待测单元的缺陷,不能发现单元交互(集成)之间的缺陷。在实践过程中,很少有defect通过单元测试发现。

基本不能用于发现表现层(JSP, Java scripts, css, UI etc)的代码缺陷投入/产出比太高。换句话说,相比于单元测试,人工测试(munual testing)可以很大程度得更快更好的发现系统缺陷。

2.2.2单元测试可以用来防止Regression Defect么?

如果我们特地为某个regression defect加了相应的单元测试,那么单元测试在某种程度上可以防止regression defect的再一次出现。但是同样的,单元测试只能防止待测单元中的Regression Defect,而且需要通过猜测来加入相应的测试案例

2.2.3 单元测试对设计有帮助么?

单元测试本身不一定能帮助设计。据说TTD可以帮助设计,实践过程中没有很深的体会。

2.2.4你投入了多少时间写单元测试?需要多少时间维护单元测试?

单元测试:代码= 2:1,也就是说一行代码需要两行单元测试。也有些人说1:1。

维护成本基本上决定于单元接口的变化频率:对于一些比较稳定的代码单元,维护成本还可以接受。但对于一些需求变化剧烈的单元,基本上需要重写。在实际实践中,可能的比例为稳定的单元测试:重写的单元测试 = 80%:20%。但是这里有一个悖论:其实我们更希望单元测试可以用于验证(verify)核心单元的正确性,然而这些单元的测试单元确基本上需要重写。这是为什么呢?其中一个可能的原因是:对于一个在线系统(web based application)来说,系统的主要逻辑和用户接口(user interface)绑定过于紧密,所以,用户接口的变化导致从表现层到数据库层的垂直变化。即使业务需求只是加了一个新的属性,但是这个数据将被加入核心的对象当中,所有涉及这个对象的单元测试需要改变。

2.2.5单元测试的主要挑战是什么?

挑战之一:如何在多个测试用例之间共享测试数据。

公司产品支持一个很复杂的在线向导,由七步组成,每一步可以单独保存然后退出,下次继续编辑。如果你想测试最后一步的API,你需要准备很多其他页面的数据。因此,需要花很多时间准备测试数据。另外,公司产品还支持相似功能的其他向导。作为一个程序员,我们一直想在多个类似功能的向导API之间共享测试数据。然而,如果待测对象本身有些微变化,所有共享该数据的测试代码全部需要重写。这是一个巨大的维护费用。

挑战之二:剧烈的需求变化导致维护成本剧增,收益减少。

正如2.2.4中描述的,一个典型的在线系统(web based application),通常可以分为三层:表现层,主要是用户界面,包括HTML/JSP/CSS/Java Scripts/Ajex等等;业务层,主要是业务逻辑;数据层,存取数据。根据面向对象设计(OOD)的原则,业务层主要由一组领域对象(Business Object/Domain Object)构成。这些领域对象只提供一组数目相对有限的,接口比较清晰的,时间比较稳定的API。对这组API进行单元测试是有必要的,也是有意义的。

然而,系统还有相当一部分的代码用于调用不同领域对象之间的API,转变成表现层需要的对象。表现层其他的逻辑还包含大量的代码用于连接不同的页面,以及构建不同的向导。

正如和绝大多数的系统一样,产品需求的变化是极其剧烈的,可以预测的,不可避免的。在这种情况下,需求变化将导致领域对象API以上的代码(包括绝大多数表现层代码和一部分业务层代码)将发生剧烈变化,与之相应的单元测试代码都需要相应的改变。也就是说,这些代码的单元测试代码的维护成本很好。

挑战之三:海量的遗留代码(Legacy Codes)

正如前面描述的,我们是在产品已经上线之后才开始推行单元测试的。因此,大量的遗留代码并不适用于单元测试。换句话说,单元测试必须要在API实现之前予以仔细得考虑。如果API本身没有得到很好的设计,单元测试基本上是不可能的。

2.2.6 拿什么来衡量单元测试?

一般来说,业界使用行覆盖率(line coverage)和分支覆盖率(branch coverage)来衡量单元测试的测量。但在实际过程中,我们发现这些衡量标准和我们对单元测试的期望有很大差距:比如说,高覆盖率不见得较少的代码缺陷。高覆盖率也不能防止regression缺陷。高覆盖率也似乎和设计没有直接关联。从另外一个角度说,达到高覆盖率所花费的时间也是相当惊人的。

从另外一个角度来说,我们希望找到一个方法可以简单直接地衡量单元测试的测量:比如说代码缺陷或regression defect数量。

3. 聆听和讨论(业界怎么说)

带着这些问题和困惑,我在网上查询了大量相关资料,牛人的文章和业界的讨论。很容易看出,业界对于单元测试的目标,作用,方法和手段都有很多争议。

3.1 什么是(不是)单元测试?

3.1.1 单元测试和发现缺陷无关(http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/)

【摘要】单元测试不是一个发现缺陷或者检测regression defect的有效方法。其一,单元测试,根据定义,是用来测试特定代码单元。然而,一个系统往往是一个大量单元的复杂集成,单元测试很难发现集成的缺陷。其二,相比单元测试,手工测试或者自动化集成测试更容易用于检测缺陷。

原文转自:http://www.ltesting.net