第一章:Junit初探
所有的代码都是测试通过的。
在开发中,我们首先要做的是对程序进行可接受性测试。我们编码,编译,运行,测试。测试当鼠标点击按钮时程序是否会作出期望的回应。为此,我们每天进行编码,编译,运行,测试。
我们测试时,尤其在测试开始时常常发现很多问题。因此我们再一次编码,编译,运行,测试。
我们常常开发一个模型来进行非正式的测试:添加记录,查看记录,编辑记录,删除记录。像这样手动运行一个小的测试套件就足够我们测试了,因此我们一再地重复。
一些开发者喜欢这种重复的测试方式。深思和硬编码会打断愉快的心情,当我们通过少量的鼠标点击测试后发现了bug,就会有一种成功的感觉:我找到它了!
其它的开发者不喜欢这种重复的工作方式。与其手工进行测试,他们更愿意创建一些小的程序来使测试自动地运行。运行测试代码是一回事,运行自动化测试又是另一回事。
如果你是一个测试开发人员,这本书非常适合你。我们将向你演示如何简单,有效,有趣地创建自动化测试。
如果你已经对测试感兴趣,这本书也适合你。第一部分将覆盖一些基础的内容,往后有所增强,实际开发问题将会在第二,三,四部分。
1.1证明它的工作原理
一些开发者认为自动化测试是开发过程中一个必要的组成部分:你不能证明一个组件能正确地运行,除非它能通过所有的测试。有些开发者认为单元测试是如此的重要,因此单元测试也应当有自己的测试框架,在1997年, 埃里克伽马(Erich Gamma)和肯特贝克(Kent Beck)创建了一个简单但是有效的单元测试框架,叫做JUnit。根据这样的设计,他们创建了Smalltalk的早期框架,叫 SUnit。
定义(Definition):框架是一个半成品的应用程序,框架提供一个可用的,通用的结构来进行应用程序之间的共享。开发者们将框架结合到他们自己的程序中,并扩展框架来满足他们特定的需求。框架不像工具箱一样提供一致的结构,而是一些简单实用类的集合。
如果你知道这些名字,就有足够的理由学习JUnit。埃里克伽马是《设计模式》的作者,肯特贝克是《极限编程》的作者。
JUnit是一个开源软件,遵从IBM发布的通用公共许可证1.0版并托管在SourceForge下,这个通用公共许可证是商业公开的:人们可以发布商业的Junit而没有过多的繁文缛节和限制。
Junit迅速地成为Java开发中单元测试的标准框架。基于这样的测试模型,形成了众所周知的测试框架:XUnit,它派生成为了许多开发语言的标准框架,可以用于ASP,C++, C#, Eiffel, Delphi, Perl, PHP, Python, REBOL, Smalltalk, and Visual Basic等等。
JUnit团队并没有发明软件测试或者单元测试,原先单元测试这个术语描述了这样一个测试,单元测试是检查一个独立单元工作的行为。
随着时间的推移,单元测试也扩大了应用范围,比如,IEEE是这样定义单元测试:测试独立的软件单元或者硬件单元或者一组相关的单元。
在这本书中,我们使用单元测试这个术语的侠义概念,就是检查一个独立单元工作的行为而不涉及其它概念。我们专注于这种小的类型,随着增量测试,开发者可以把相应的单元应用到他们的代码中。有时我们称他们为程序员测试以区别质量保证测试或者客户测试。
这是我们对典型的单元测试一般性的描述:验证一个方法接收一个期望的输入并返回一个期望的输出。
这个描述要求我们的测试方法通过接口的行为:如果我们给一个值 X,它将返回一个值Y,如果我们使用一个值Z替换X ,那么它将抛出一个正确的异常。
定义(Definition):单元测试检查一个明显的单元工作的行为,在Java程序中,“明显的工作单元”常常是(不总是)一个单独的方法。相比之下,集成测试和验收测试检查的是多个组件之间是如何交互。一个工作单元的任务,不直接依赖于其他任何任务的完成。
单元测试常常关注的测试重点是方法是否遵从了API接口协议。就像人们制定的书面协议以用于在特定的条件下同意交换商品或者服务。一个API契约是一个正式的协议由方法签名产生。一个方法需要它的调用者提供准确的对象引用或者原始值然后返回一个对象的引用或者原始值。假如方法不能满足接口协议,测试将抛出一个异常,这时我们说这个方法破坏了它的协议。
在这一章中,我们从头开始创建一个单元测试来测试一个简单的类。我们先写一个测试运用最小的运行框架,因此你可以到我们是如何做的,然后我们展开JUnit来向你演示正确的工具如何使生活更简单。
定义(Definition):API协议是应用程序接口(application programming interface)的缩写,作为调用者和被调用者之间的正式协议。单元测试常常通过演示期望的行为来帮助定义API协议。API协议的概念起源于实践,由埃菲尔编程语言推广(Eiffel programming language)。
1.2、从零开始
我们的第一个范例,我们创建一个简单的calculator类来计算两数相加,calculator类提供一个API给客户端但是不包括用户界面。请看列表1.1
Listing 1.1 The test calculator class
public class Calculator{
public double add(double number1,double number2){
return number1+number2;
}
}
虽然上面代码没有文档说明,但是Calculator类的add(double,double)方法的明显目的是计算两个double类型数字相加然后返回double类型的和。编译器能够告诉我们代码是否可以编译成功,但是我们也将够确定它在运行的时候也能工作。一个核心的单元测试原则是:不能自动化测试的程序是不应保留的。add方法表示Calculator类的核心功能,我们有一些代码实现这些功能,现在缺少的是一个自动化测试来证明我们的实现是否运行。
是否add方法太简单而不会出错?
add方法的实现太简单而不会出错,如果add方法是一个较小的实用方法,然后我们不能直接地测试它。有这样一个测试用例,如果add方法运行失败,那么运行add方法的测试用例也将失败。add方法能够被间接地测试,尽管如此测试,在Calculator程序的上下文中,add不仅仅是一个方法,它是一个程序的功能。为了对程序更有信心,大多数的开发者希望add功能能够有自动化测试。不管这个实现有多么的简单。在一些测试用例中,我们能够证明程序的功能通过自动化功能测试或者自动化验收测试。更多关于软件测试的论述,请看第三章。
在这一点上的似乎任何测试都是有问题的,我们即使没有一个用户界面来输入一对double类型数字,也能够编写一个小的命令行程序来等待我们输入两个double类型的值然后显示运算结果。然后我们也可以测试我们自己是否有能力去输入数字然后添加结果给自己。这不仅仅是我们想要做的事情,我们想要知道是否这个工作单元添加两个double数字然后返回正确的和,我们不想去测试是否程序员可以输入数字。
同时,如果我们将要努力地测试我们的工作,我们也应该尝试保持这份努力。我们写的add(double,double)方法可以运行是令人高兴的。但是当我们把应用程序其余部分装载后或者每当我们做后续的修改时,我们实际上是想知道这个方法是否将正确地工作。如果我们将这些需求放在一起,我们想出一个主意来编写一个简单的测试程序来测试add()方法。
测试程序通过已知的值传递给add方法然后看返回的结果是否与期望值相匹配。我们也能够再次运行测试程序去确认方法能够持续地运行随着应用程序的不断增长。我们如何编写最简单的测试程序?关于CalculatorTest测试程序如列表1.2所示?
LIsting 1.2 A simple test Calculator program
public class CalculatorTest{
public static void main(String[] args){
Calculator calculator = new Calculator();
double result = calbulator.add(10,50);
if (result != 60){
System.out.println(“Bad result:” + result);
}
}
}
第一个CalculatorTest类测试程序确实很简单。
它创建一个Calculator的实例,输入两个数字,然后检查运算结果。如果结果和我们的期望值不匹配,我们将打印一条标准的输出信息。
如果我们编译然后运行这个测试程序,测试将静默地通过,似乎所有的看起来都很好。但是如果我们修改代码使它失败会发生什么呢?我们将不得不非常仔细地观察屏幕的错误信息。我们不得不提供输入,但是我们依旧测试我们的能力去监视程序的输出。我们希望的是测试代码,而不是我们自己。
在Java里面最方便的方式去发送错误条件是抛出一个异常,我们抛出一个异常而不是指示测试失败。
同时,我们也可能去运行程序测试其它的Calculator的方法,即使我们还没有编写。像减法(subtract)和乘法(multiply)。可迁移模块化设计扩展测试程序后使它更容易捕捉和处理异常。列表1.3展示了一个较好的CalculatorTest程序。
Listing 1.3 A(slightly) better test calculator program
public class CalculatorTest{
private int nberrors = 0;
public void tetAdd(){
Calculator calculator = new Calculator();
double result = calculator.add(10.,50);
if (result != 60){
throw new IllegalStateException(“Bad result:” + result);
}
}
public static void main(String[] args){
CalculatorTest test = new CalculatorTest();
try{
test.tetAdd();
}catch(Throwable e){
test.nbErrors++;
e.printStackTrace();
}
if (test.nbErrors > 0){
throw new IllegalStateException(“Thers were” + test.nbErrors + “error(s)”);
}
}
}
我们将测试到自己的add方法,现在很容易关注测试做了什么,在后面单元测试中我们也能添加更多的方法,没有主方法很难去维持。
当产生错误的时候我们改变主方法去打印堆栈跟踪,如果有许多错误,通过一个总结性的异常来结束。
现在你看到的是一个简单的应用和它的测试程序,你可以看到,即使这样小的类和它的测试都可以从测试代码中获益,我们已经创建运行和管理测试结果。因为应用程序会变得越来越复杂和更多的测试参与,不断地构建和维护我们自定义的测试框架将成为我们的负担。
下一章,我们后退一步,看一下一个单元测试框架的一般测试用例。
1.3理解单元测试框架
单元测试框架应该遵循的几个最佳实践,在CalculatorTest程序中这些看起来较小的改进强调了所有的单元测试框架应遵循如下三条原则(我们的经验):
n 每一个单元测试应当独立于其它单元测试
n 在测试中框架应当监测和报告错误
n 应当很容易地去定义哪个一单元测试将运行
这个“略胜一筹”的测试程序接近符合这些原则,但是依旧有些不足。例如,为了每个单元测试能够真正地独立,每一个测试应当运行在不同的类实例和不同的类加载实例下。
我们通过添加一个新的方法来增加新的单元测试,然后添加一个合适的try/catch 块给main方法。这是一个步骤,但它仍然是不足的,我们想要一个真正的单元测试套件。我们的经验告诉我们大量的try/catch 块会引起许多维护问题。我们很容易地忽略一个单元测试和忘记它。如果我们可以添加新的测试方法然后继续运行,这将是我们想要的。但是如何使程序知道所要运行的方法呢?好吧,一个方法是我们创建有一个简单的注册步骤。一个注册方法将至少有一个清单来决定运行哪些测试。
另一种方法是使用 Java 的反射机制(reflection)和自动测试能力。一个程序可以审视它自己,并决定运行遵循一定的命名约定任何方法,就像以test命名开头的方法,例如:
很容易添加测试(前面表中的第三规则),听起来像一个单元测试框架的一个很好的规则,它支持代码识别这个规则(通过注册或者自动测试)将不会毫无价值,但这将是值得的。
在之前会有很多的工作,但是这一个努力将使我们添加一个新的测试时获得回报。
幸运的是,JUnit团队已经解决我们的问题,JUnit框架已经支持自我测试方法,它也支持使用不同的类实例和不同的类加载实例,它能够在测试试验时报告所有的错误。
现在你有一个更好的主意,为什么需要一个单元测试框架呢?尤其是Junit。
1.4JUnit 设计目标
JUnit团队已经对框架定义了三条独立的目标:
n 框架必须帮助我们编写有用的测试
n 框架必须帮助我们创建测试随着时间的推移并保留价值。
n 框架必须帮助我们重用代码以降低编写测试的成本
我们会回顾这些目标在第二章。
下一节,我们行动之前,我们向你演示如何设置JUnit。
未完待续........
JUnit in Action 2nd Edition 第一章 JUnit 概述 (1)
最新推荐文章于 2017-05-02 14:38:00 发布