应用JUnit实施单元测试.docx
- 文档编号:24172361
- 上传时间:2023-05-25
- 格式:DOCX
- 页数:16
- 大小:27.85KB
应用JUnit实施单元测试.docx
《应用JUnit实施单元测试.docx》由会员分享,可在线阅读,更多相关《应用JUnit实施单元测试.docx(16页珍藏版)》请在冰豆网上搜索。
应用JUnit实施单元测试
应用JUnit实施单元测试(原创作者:
eric )
(献给想保证java coding软件质量的朋友们)
--------------------------------------------------------------------------------
测试的概念
长期以来,我所接触的软件开发人员很少有人能在开发的过程中进行测试工作。
大部分的项目都是在最终验收的时候编写测试文档。
有些项目甚至没有测试文档。
现在情况有了改变。
我们一直提倡UML、RUP、软件工程、CMM,目的只有一个,提高软件编写的质量。
举一个极端的例子:
如果你是一个超级程序设计师,一个传奇般的人物。
(你可以一边喝咖啡,一边听着音乐,同时编写这操作系统中关于进程调度的模块,而且两天时间内就完成了!
)我真得承认,有这样的人。
(那个编写UNIX中的vi编辑器的家伙就是这种人。
)然而非常遗憾的是这些神仙们并没有留下如何修成正果的README。
所以我们这些凡人--在同一时间只能将注意力集中到若干点(据科学统计,我并不太相信,一般的人只能同时考虑最多7个左右的问题,高手可以达到12个左右),而不能既纵览全局又了解细节--只能期望于其他的方式来保证我们所编写的软件质量。
为了说明我们这些凡人是如何的笨。
有一个聪明人提出了软件熵(software entropy)的概念:
一个程序从设计很好的状态开始,随着新的功能不断地加入,程序逐渐地失去了原有的结构,最终变成了一团乱麻。
你可能会争辩,在这个例子中,设计很好的状态实际上并不好,如果好的话,就不会发生你所说的情况。
是的,看来你变聪明了,可惜你还应该注意到两个问题:
1)我们不能指望在恐龙纪元(大概是十年前)设计的结构到了现在也能适用吧。
2)拥有签字权的客户代表可不理会加入一个新功能是否会对软件的结构有什么影响,即便有影响也是程序设计人员需要考虑的问题。
如果你拒绝加入这个你认为致命的新功能,那么你很可能就失去了你的住房贷款和面包(对中国工程师来说也许是米饭或面条,要看你是南方人还是北方人)。
另外,需要说明的是我看过的一些讲解测试的书都没有我写的这么有人情味(不好意思...)。
我希望看到这片文章的兄弟姐妹能很容易地接受测试的概念,并付诸实施。
所以有些地方写的有些夸张,欢迎对测试有深入理解的兄弟姐妹能体察民情,并不吝赐教。
好了,我们现在言归正传。
要测试,就要明白测试的目的。
我认为测试的目的很简单也极具吸引力:
写出高质量的软件并解决软件熵这一问题。
想象一下,如果你写的软件和Richard Stallman(GNU、FSF的头儿)写的一样有水准的话,是不是很有成就感?
如果你一致保持这种高水准,我保证你的薪水也会有所变动。
测试也分类,白箱测试、黑箱测试、单元测试、集成测试、功能测试...。
我们先不管有多少分类,如何分类。
先看那些对我们有用的分类,关于其他的测试,有兴趣的人可参阅其他资料。
白箱测试是指在知道被测试的软件如何(How)完成功能和完成什么样(What)的功能的条件下所作的测试。
一般是由开发人员完成。
因为开发人员最了解自己编写的软件。
本文也是以白箱测试为主。
黑箱测试则是指在知道被测试的软件完成什么样(What)的功能的条件下所作的测试。
一般是由测试人员完成。
黑箱测试不是我们的重点。
本文主要集中在单元测试上,单元测试是一种白箱测试。
目的是验证一个或若干个类是否按所设计的那样正常工作。
集成测试则是验证所有的类是否能互相配合,协同完成特定的任务,目前我们暂不关心它。
下面我所提到的测试,除非特别说明,一般都是指单元测试。
需要强调的是:
测试是一个持续的过程。
也就是说测试贯穿与开发的整个过程中,单元测试尤其适合于迭代增量式(iterative and incremental)的开发过程。
Martin Fowler(有点儿像引用孔夫子的话)甚至认为:
“在你不知道如何测试代码之前,就不应该编写程序。
而一旦你完成了程序,测试代码也应该完成。
除非测试成功,你不能认为你编写出了可以工作的程序。
”我并不指望所有的开发人员都能有如此高的觉悟,这种层次也不是一蹴而就的。
但我们一旦了解测试的目的和好处,自然会坚持在开发过程中引入测试。
因为我们是测试新手,我们也不理会那些复杂的测试原理,先说一说最简单的:
测试就是比较预期的结果是否与实际执行的结果一致。
如果一致则通过,否则失败。
看下面的例子:
//将要被测试的类
public class Car {
public int getWheels() {
return 4;
}
}
//执行测试的类
public class testCar {
public static void main(String[] args) {
testCar myTest = new testCar();
myTest.testGetWheels();
}
public testGetWheels() {
int expectedWheels = 4;
Car myCar = Car();
if (expectedWheels==myCar.getWheels())
System.out.println("test [Car]:
getWheels works perfected!
");
else
System.out.println("test [Car]:
getWheels DOESN'T work!
");
}
}
如果你立即动手写了上面的代码,你会发现两个问题,第一,如果你要执行测试的类testCar,你必须必须手工敲入如下命令:
[Windows] d:
>;java testCar
[Unix] % java testCar
即便测试如例示的那样简单,你也有可能不愿在每次测试的时候都敲入上面的命令,而希望在某个集成环境中(IDE)点击一下鼠标就能执行测试。
后面的章节会介绍到这些问题。
第二,如果没有一定的规范,测试类的编写将会成为另一个需要定义的标准。
没有人希望查看别人是如何设计测试类的。
如果每个人都有不同的设计测试类的方法,光维护被测试的类就够烦了,谁还顾得上维护测试类?
另外有一点我不想提,但是这个问题太明显了,测试类的代码多于被测试的类!
这是否意味这双倍的工作?
不!
1)不论被测试类-Car 的 getWheels 方法如何复杂,测试类-testCar 的testGetWheels 方法只会保持一样的代码量。
2)提高软件的质量并解决软件熵这一问题并不是没有代价的。
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 junit.framework.*;
public class testCar extends TestCase {
protected int expectedWheels;
protected Car myCar;
public testCar(String name) {
super(name);
}
protected void setUp() {
expectedWheels = 4;
myCar = new Car();
}
public static Test suite() {
/*
* the type safe way
*
TestSuite suite= new TestSuite();
suite.addTest(
new testCar("Car.getWheels") {
protected void runTest() { testGetWheels(); }
}
);
return suite;
*/
/*
* the dynamic way
*/
return new TestSuite(testCar.class);
}
public void testGetWheels() {
assertEquals(expectedWheels, myCar.getWheels());
}
}
改版后的testCar已经面目全非。
先让我们了解这些改动都是什么含义,再看如何执行这个测试。
1>;import语句,引入JUnit的类。
(没问题吧)
2>;继承 TestCase 。
可以暂时将一个TestCase看作是对某个类进行测试的方法的集合。
详细介绍请参看JUnit资料
3>;setUp()设定了进行初始化的任务。
我们以后会看到setUp会有特别的用处。
4>;testGetWheeels()对预期的值和myCar.getWheels()返回的值进行比较,并打印比较的结果。
assertEquals是junit.framework.Assert中所定义的方法,junit.framework.TestCase继承了junit.framework.Assert。
5>;suite()是一个很特殊的静态方法。
JUnit的TestRunner会调用suite方法来确定有多少个测试可以执行。
上面的例子显示了两种方法:
静态的方法是构造一个内部类,并利用构造函数给该测试命名(test name, 如 Car.getWheels ),其覆盖的runTest()方法,指明了该测试需要执行那些方法--testGetWheels()。
动态的方法是利用内省(reflection )来实现runTest(),找出需要执行那些测试。
此时测试的名字即是测试方法(test method,如testGetWheels)的名字。
JUnit会自动找出并调用该类的测试方法。
6>;将TestSuite看作是包裹测试的一个容器。
如果将测试比作叶子节点的话,TestSuite就是分支节点。
实际上TestCase,TestSuite以及TestSuite组成了一个composite Pattern。
JUnit的文档中有一篇专门讲解如何使用Pattern构造Junit框架。
有兴趣的朋友可以查看JUnit资料。
如何运行该测试呢?
手工的方法是键入如下命令:
[Windows] d:
>;java junit.textui.TestRunner testCar
[Unix] % java junit.textui.TestRunner testCar
别担心你要敲的字符量,以后在IDE中,只要点几下鼠标就成了。
运行结果应该如下所示,表明执行了一个测试,并通过了测试:
.
Time:
0
OK (1 tests)
如果我们将Car.getWheels()中返回的的值修改为3,模拟出错的情形,则会得到如下结果:
.F
Time:
0
There was 1 failure:
1) testGetWheels(testCar)junit.framework.AssertionFailedError:
expected:
<4>; but was:
<3>;
at testCar.testGetWheels(testCar.java:
37)
FAILURES!
!
!
Tests run:
1, Failures:
1, Errors:
0
注意:
Time上的小点表示测试个数,如果测试通过则显示OK。
否则在小点的后边标上F,表示该测试失败。
注意,在模拟出错的测试中,我们会得到详细的测试报告“expected:
<4>; but was:
<3>;”,这足以告诉我们问题发生在何处。
下面就是你调试,测试,调试,测试...的过程,直至得到期望的结果。
Design by Contract(这句话我没法翻译)
Design by Contract本是Bertrand Meyer(Eiffel语言的创始人)开发的一种设计技术。
我发现在JUnit中使用Design by Contract会带来意想不到的效果。
Design by Contract的核心是断言(assersion)。
断言是一个布尔语句,该语句不能为假,如果为假,则表明出现了一个bug。
Design by Contract使用三种断言:
前置条件(pre-conditions)、后置条件(post-conditions)和不变式(invariants)这里不打算详细讨论Design by Contract的细节,而是希望其在测试中能发挥其作用。
前置条件在执行测试之前可以用于判断是否允许进入测试,即进入测试的条件。
如 expectedWheels >; 0, myCar !
= null。
后置条件用于在测试执行后判断测试的结果是否正确。
如 expectedWheels==myCar.getWheels()。
而不变式在判断交易(Transaction)的一致性(consistency)方面尤为有用。
我希望JUnit可以将Design by Contract作为未来版本的一个增强。
Refactoring(这句话我依然没法翻译)
Refactoring本来与测试没有直接的联系,而是与软件熵有关,但既然我们说测试能解决软件熵问题,我们也就必须说出解决之道。
(仅仅进行测试只能发现软件熵,Refactoring则可解决软件熵带来的问题。
)软件熵引出了一个问题:
是否需要重新设计整个软件的结构?
理论上应该如此,但现实不允许我们这么做。
这或者是由于时间的原因,或者是由于费用的原因。
重新设计整个软件的结构会给我们带来短期的痛苦。
而不停地给软件打补丁甚至是补丁的补丁则会给我们带来长期的痛苦。
(不管怎样,我们总处于水深火热之中)
Refactoring是一个术语,用于描述一种技术,利用这种技术我们可以免于重构整个软件所带来的短期痛苦。
当你refactor时,你并不改变程序的功能,而是改变程序内部的结构,使其更易理解和使用。
如:
该变一个方法的名字,将一个成员变量从一个类移到另一个类,将两个类似方法抽象到父类中。
所作的每一个步都很小,然而1-2个小时的Refactoring工作可以使你的程序结构更适合目前的情况。
Refactoring有一些规则:
1>; 不要在加入新功能的同时refactor已有的代码。
在这两者间要有一个清晰的界限。
如每天早上1-2个小时的Refactoring,其余时间添加新的功能。
2>; 在你开始Refactoring前,和Refactoring后都要保证测试能顺利通过。
否则Refactoring没有任何意义。
3>; 进行小的Refactoring,大的就不是Refactoring了。
如果你打算重构整个软件,就没有必要Refactoring了。
只有在添加新功能和调试bug时才又必要Refactoring。
不要等到交付软件的最后关头才Refactoring。
那样和打补丁的区别不大。
Refactoring 用在回归测试中也能显示其威力。
要明白,我不反对打补丁,但要记住打补丁是应该最后使用的必杀绝招。
(打补丁也需要很高的技术,详情参看微软网站)
IDE对JUnit的支持
目前支持JUnit的Java IDE 包括 IDE 方式 个人评价(1-5,满分5)
Forte for Java 3.0 Enterprise Edition plug-in 3
JBuilder 6 Enterprise Edition integrated with IDE 4
Visual Age for Java support N/A
在IDE中如何使用JUnit,是非常具体的事情。
不同的IDE有不同的使用方法。
一旦理解了JUnit的本质,使用起来就十分容易了。
所以我们不依赖于具体的IDE,而是集中精力讲述如何利用JUnit编写单元测试代码。
心急的人可参看资料。
JUnit简介
既然我们已经对JUnit有了一个大致的了解,我希望能给大家提供一个稍微正式一些的编写JUnit测试文档的手册,明白其中的一些关键术语和概念。
但我要声明的是这并不是一本完全的手册,只能认为是一本入门手册。
同其他OpenSource的软件有同样的问题,JUnit的文档并没有商业软件文档的那种有规则,简洁和完全。
由开发人员编写的文档总是说不太清楚问题,全整的文档需要参考"官方"指南,API手册,邮件讨论组的邮件,甚至包括源代码中及相关的注释。
事实上问题并没有那么复杂,除非你有非常特别的要求,否则,只需参考本文你就可以得到所需的大部分信息。
安装
首先你要获取JUnit的软件包,从JUnit下载最新的软件包(截至写作本文时,JUnit的最新版本是3.7)。
将其在适当的目录下解包。
这样在安装目录(也就是你所选择的解包的目录)下你找到一个名为junit.jar的文件。
将这个jar文件加入你的CLASSPATH系统变量。
(IDE的设置会有所不同,参看你所喜爱的IDE的配置指南)JUnit就安装完了。
太easy了!
你一旦安装完JUnit,就有可能想试试我们的Car和testCar类,没问题,我已经运行过了,你得到的结果应该和我列出的结果类似。
(以防新版JUnit使我的文章过时)
接下来,你可能会先写测试代码,再写工作代码,或者相反,先写工作代码,再写测试代码。
我更赞成使用前一种方法:
先写测试代码,再写工作代码。
因为这样可以使我们编写工作代码时清晰地了解工作类的行为。
要注意编写一定能通过的测试代码(如文中的例子)并没有任何意义,只有测试代码能帮助我们发现bug,测试代码才有其价值。
此外测试代码还应该对工作代码进行全面的测试。
如给方法调用的参数传入空值、错误值和正确的值,看看方法的行为是否如你所期望的那样。
你现在已经知道了编写测试类的基本步骤:
1>;扩展TestCase类;
2>;覆盖runTest()方法(可选);
3>;写一些testXXXXX()方法;
Fixture
解下来的问题是,如果你要对一个或若干个的类执行多个测试,该怎么办?
JUnit对此有特殊的解决办法。
如果需要在一个或若干个的类执行多个测试,这些类就成为了测试的context。
在JUnit中被称为Fixture(如testCar类中的 myCar 和 expectedWheels )。
当你编写测试代码时,你会发现你花费了很多时间配置/初始化相关测试的Fixture。
将配置Fixture的代码放入测试类的构造方法中并不可取,因为我们要求执行多个测试,我并不希望某个测试的结果意外地(如果这是你要求的,那就另当别论了)影响其他测试的结果。
通常若干个测试会使用相同的Fixture,而每个测试又各有自己需要改变的地方。
为此,JUnit提供了两个方法,定义在TestCase类中。
protected void setUp() throws java.lang.Exception
protected void tearDown() throws java.lang.Exception
覆盖setUp()方法,初始化所有测试的Fixture(你甚至可以在setUp中建立网络连接),将每个测试略有不同的地方在testXXX()方法中进行配置。
覆盖tearDown()(我总想起一首叫雨滴的吉他曲),释放你在setUp()中分配的永久性资源,如数据库连接。
当JUnit执行测试时,它在执行每个testXXXXX()方法前都调用setUp(),而在执行每个testXXXXX()方法后都调用tearDown()方法,由此保证了测试不会相互影响。
TestCase
需要提醒一下,在junit.framework.Assert类中定义了相当多的assert方法,主要有assert(), assert(), assertEquals(), assertNull(), assertSame(), assertTrue(), fail()等方法。
如果你需要比较自己定义的类,如Car。
assert方法需要你覆盖Object类的equals()方法,以比较两个对象的不同。
实践表明:
如果你覆盖了Object类的equals()方法,最好也覆盖Object类的hashCode()方法。
再进一步,连带Object类的toString()方法也一并覆盖。
这样可以使测试结果更具可读性。
当你设置好了Fixture后,下一步是编写所需的testXXX()方法。
一定要保证testXXX()方法的public属性,否则无法通过内省(reflection)对该测试进行调用。
每个扩展的TestCase类(也就是你编写的测试类)会有多个testXXX()方法。
一个testXXX()方法就是一个测试。
要想运行这个测试,你必须定义如何运行该测试。
如果你有多个testXXX()方法,你就要定义多次。
JUnit支持两种运行单个测试的方法:
静态的和动态的方法。
静态的方法就是覆盖TestCase类的runTest()方法,一般是采用内部类的方式创建一个测试实例:
TestCase test01 = new testCar("test getWheels") {
public void runTest() {
testGetWheels();
}
}
采用静态的方法要注意要给每个测试一个名字(这个名字可以任意起,但你肯定希望这个名字有某种意义),这样你就可以区分那个测试失败了。
动态的方法是用内省来实现runTest()以创建一个测试实例。
这要求测试的名字就是需要调用的测试方法的名字:
TestCase test01 = new testCar("testGetWheels");
JUnit会动态查找并调用指定的测试方法。
动态的方法很简洁,但如果你键入了错误的名字就会得到一个令人奇怪的NoSuchMethodException异常。
动态的方法和静态的方法都很好,你可以按照自己的喜好来选择。
(先
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 应用 JUnit 实施 单元测试
![提示](https://static.bdocx.com/images/bang_tan.gif)