测试的概念
回归测试框架JUnit
Design by Contract
Refactoring
IDE对JUnit的支持
JUnit简介
安装
Fixture
TestCase
TestSuite
TestRunner
JUnit最佳实践
JUnit与JEE
测试的概念
长期以来我所接触的软件开发人员很少有人能在开发的过程中进行测试工作大部分的项目都是在最终验收的时候编写测试文档有些项目甚至没有测试文档现在情况有了改变我们一直提倡UMLRUP软件工程CMM目的只有一个提高软件编写的质量举一个极端的例子如果你是一个超级程序设计师一个传奇般的人物(你可以一边喝咖啡一边听着音乐同时编写这操作系统中关于进程调度的模块而且两天时间内就完成了!)我真得承认有这样的人(那个编写UNIX中的vi编辑器的家伙就是这种人)然而非常遗憾的是这些神仙们并没有留下如何修成正果的README所以我们这些凡人--在同一时间只能将注意力集中到若干点(据科学统计我并不太相信一般的人只能同时考虑最多个左右的问题高手可以达到个左右)而不能既纵览全局又了解细节--只能期望于其他的方式来保证我们所编写的软件质量
为了说明我们这些凡人是如何的笨有一个聪明人提出了软件熵(software entropy)的概念一个程序从设计很好的状态开始随着新的功能不断地加入程序逐渐地失去了原有的结构最终变成了一团乱麻你可能会争辩在这个例子中设计很好的状态实际上并不好如果好的话就不会发生你所说的情况是的看来你变聪明了可惜你还应该注意到两个问题)我们不能指望在恐龙纪元(大概是十年前)设计的结构到了现在也能适用吧)拥有签字权的客户代表可不理会加入一个新功能是否会对软件的结构有什么影响即便有影响也是程序设计人员需要考虑的问题如果你拒绝加入这个你认为致命的新功能那么你很可能就失去了你的住房贷款和面包(对中国工程师来说也许是米饭或面条要看你是南方人还是北方人)
另外需要说明的是我看过的一些讲解测试的书都没有我写的这么有人情味(不好意思)我希望看到这片文章的兄弟姐妹能很容易地接受测试的概念并付诸实施所以有些地方写的有些夸张欢迎对测试有深入理解的兄弟姐妹能体察民情并不吝赐教
好了我们现在言归正传要测试就要明白测试的目的我认为测试的目的很简单也极具吸引力写出高质量的软件并解决软件熵这一问题想象一下如果你写的软件和Richard Stallman(GNUFSF的头儿)写的一样有水准的话是不是很有成就感?如果你一致保持这种高水准我保证你的薪水也会有所变动
测试也分类白箱测试黑箱测试单元测试集成测试功能测试我们先不管有多少分类如何分类先看那些对我们有用的分类关于其他的测试有兴趣的人可参阅其他资料白箱测试是指在知道被测试的软件如何(How)完成功能和完成什么样(What)的功能的条件下所作的测试一般是由开发人员完成因为开发人员最了解自己编写的软件本文也是以白箱测试为主黑箱测试则是指在知道被测试的软件完成什么样(What)的功能的条件下所作的测试一般是由测试人员完成黑箱测试不是我们的重点本文主要集中在单元测试上单元测试是一种白箱测试目的是验证一个或若干个类是否按所设计的那样正常工作集成测试则是验证所有的类是否能互相配合协同完成特定的任务目前我们暂不关心它下面我所提到的测试除非特别说明一般都是指单元测试
需要强调的是测试是一个持续的过程也就是说测试贯穿与开发的整个过程中单元测试尤其适合于迭代增量式(iterative and incremental)的开发过程Martin Fowler(有点儿像引用孔夫子的话)甚至认为在你不知道如何测试代码之前就不应该编写程序而一旦你完成了程序测试代码也应该完成除非测试成功你不能认为你编写出了可以工作的程序我并不指望所有的开发人员都能有如此高的觉悟这种层次也不是一蹴而就的但我们一旦了解测试的目的和好处自然会坚持在开发过程中引入测试 因为我们是测试新手我们也不理会那些复杂的测试原理先说一说最简单的:测试就是比较预期的结果是否与实际执行的结果一致如果一致则通过否则失败看下面的例子
//将要被测试的类
public class Car{
public int getWheels() {
return ;
}
}
//执行测试的类
public class testCar {
public static void main(String[] args) {
testCar myTest = new testCar();
myTesttestGetWheels();
}
public testGetWheels () {
int expectedWheels = ;
Car myCar = Car();
if (expectedWheels==myCargetWheels())
Systemoutprintln(test [Car]: getWheels works perfected!);
else
Systemoutprintln(test [Car]: getWheels DOESNT work!);
}
}
如果你立即动手写了上面的代码你会发现两个问题第一如果你要执行测试的类testCar你必须必须手工敲入如下命令
[Windows] d:>java testCar
[Unix] % java testCar
即便测试如例示的那样简单你也有可能不愿在每次测试的时候都敲入上面的命令而希望在某个集成环境中(IDE)点击一下鼠标就能执行测试后面的章节会介绍到这些问题第二如果没有一定的规范测试类的编写将会成为另一个需要定义的标准没有人希望查看别人是如何设计测试类的如果每个人都有不同的设计测试类的方法光维护被测试的类就够烦了谁还顾得上维护测试类?另外有一点我不想提但是这个问题太明显了测试类的代码多于被测试的类!这是否意味这双倍的工作?不!)不论被测试类-Car
的 getWheels 方法如何复杂测试类-testCar 的testGetWheels
方法只会保持一样的代码量)提高软件的质量并解决软件熵这一问题并不是没有代价的testCar就是代价
我们目前所能做的就是尽量降低所付出的代价我们编写的测试代码要能被维护人员容易的读取我们编写测试代码要有一定的规范最好IDE工具可以支持这些规范好了你所需要的就是JUnit一个Open Source的项目用其主页上的话来说就是 JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)用于Java开发人员编写单元测试之用所谓框架就是Erich Gamma 和 Kent Beck 定下了一些条条框框你编写的测试代码必须遵循这个条条框框继承某个类实现某个接口其实也就是我们前面所说的规范好在JUnit目前得到了大多数软件工程师的认可遵循JUnit我们会得到很多的支持回归测试就是你不断地对所编写的代码进行测试编写一些测试一些调试一些然后循环这一过程你会不断地重复先前的测试哪怕你正编写其他的类由于软件熵的存在你可能在编写第五个类的时候发现第五个类的某个操作会导致第二个类的测试失败通过回归测试我们抓住了这条大Bug
回归测试框架JUnit
通过前面的介绍我们对JUnit有了一个大概的轮廓知道了它是干什么的现在让我们动手改写上面的测试类testCar使其符合Junit的规范--能在JUnit中运行
//执行测试的类(JUnit版)
import junitframework*;
public class testCar extends TestCase {
protected int expectedWheels;
protected Car myCar;
public testCar(String name) {
super(name);
}
protected void setUp() {
expectedWheels = ;
myCar = new Car();
}
public static Test suite() {
/*
* the type safe way
*
TestSuite suite= new TestSuite();
suiteaddTest(
new testCar(CargetWheels) {
protected void runTest() { testGetWheels(); }
}
);
return suite;
*/
/*
* the dynamic way
*/
return new TestSuite(testCarclass);
}
public void testGetWheels() {
assertEquals(expectedWheels myCargetWheels());
}
}
改版后的testCar已经面目全非先让我们了解这些改动都是什么含义再看如何执行这个测试
>import语句引入JUnit的类(没问题吧)
>继承 TestCase 可以暂时将一个TestCase看作是对某个类进行测试的方法的集合详细介绍请参看JUnit资料
>setUp()设定了进行初始化的任务我们以后会看到setUp会有特别的用处
>testGetWheeels()对预期的值和myCargetWheels()返回的值进行比较并打印比较的结果assertEquals是junitframeworkAssert中所定义的方法junitframeworkTestCase继承了junitframeworkAssert
>suite()是一个很特殊的静态方法JUnit的TestRunner会调用suite方法来确定有多少个测试可以执行上面的例子显示了两种方法静态的方法是构造一个内部类并利用构造函数给该测试命名(test
name 如 CargetWheels )其覆盖的runTest()方法指明了该测试需要执行那些方法--testGetWheels()动态的方法是利用内省(reflection
)来实现runTest()找出需要执行那些测试此时测试的名字即是测试方法(test method如testGetWheels)的名字JU