软件开发方法学的泰斗Kent Beck先生最为推崇"模式、极限编程和测试驱动开发"。在他所创造的极限编程(XP)方法论中,就向你们推荐"测试先行"这一最佳实践,而且还专门撰写了《测试驱动开发》一书,详细说明如何实现。测试驱动开发是极限编程的重要特色,它以不断的测试推进代码的开发,从而实现既简化代码,又保证质量的目标。java
一看到"测试先行"、"测试驱动"这样的名字,就深深地激起了我强烈的好奇心,开始了本身的探索之旅..程序员
心灵震憾编程
一段时间的学习,让个人心里受到了深深的震撼。咱们原来的方法竟然如此的笨我面对测试先行这一名字时,当时最大的疑问就是"程序都尚未写出来, 测试什么呀!"。后来一想,其实这是一个泥瓦匠都明白的道理,倒是本身在画地为牢。咱们来看看两个不一样泥瓦匠是数据结构
如何工做的吧:框架
工匠一:先拉上一根水平线,砌每一块砖时,都与这根水平线进行比较,使得每一块砖都保持水平。ide
工匠二:先将一排砖都砌完,而后拉上一根水平线,看看哪些砖有问题,再进行调整。工具
你会选择哪一种工做方法呢?你必定会骂工匠二笨吧!这样多浪费时间呀! 然而你本身想一想,你平时在编写程序的时候又是怎么作的呢?咱们就是按工匠二的方法在干活的呀!甚至有时候比工匠二还笨,是整面墙都砌完了,直接进行"集成测试",常常让整面的墙倒塌。看到这里,你还以为本身的方法高明吗?单元测试
单元测试长期以来被忽视学习
每个程序员都知道应该为本身的代码编写测试程序,但却不多这样作。当人们问为何的时候,最常听到的回答就是:"咱们的开发工做太紧张了"。但这样却致使了一个恶性循环,越是没空编写测试程序,代码的效率与质量越差,花在找Bug、解决Bug的时间也愈来愈多, 实际效率大大下降。因为效率下降了,所以时间更紧张,压力更大。你想一想,为何不拉上一根水平线呢?难道,咱们不可以将后面浪费的时间花在单元测试上,使得咱们的程序一开始就更加健壮,更加易于修改吗?抛弃原来的托词吧!测试
咱们的自动化水平过低了
有人还会解释说,那是由于拉根水平线很简单,而写测试程序倒是十分复杂的。我暂且对这句话自己不置能否。不过也体现了一个新问题,咱们须要更加方便、省时的编写测试程序的方法。
要测试一个类,最简单的方法是直接在调试器中使用表达式观察对象的值与状态,你也能够在程序中加上一些断言、打印中间值等,固然还能够编写专门的测试程序。可是这些方法都有一个很大的局限性,都须要加入人工的判断和分析。
由此,自动化测试的引入才是解决之道。正是由于如此,提倡"测试驱动开发"的人群,开发出一系列的自动化单元测试框架xUnit,如今已经有针对Java、Pyhton、C++、PHP等各类经常使用语言的测试框架。这足以搪塞住那些以"编写测试代码太麻烦"为理由的开发人员,让他们没有理由逃避单元测试。
正如Robert Martin所说:"测试套件运行起来越简单,就会越频繁地运行它们。测试运行越多,就会越快地发现和那些测试的任何背离。若是可以一天屡次地运行全部的测试,那么系统的失效时间就决不会超过几分钟"。
认清测试驱动开发
测试驱动开发理论最初源于对这些问题的思考:
1)若是咱们可以在编写程序代码以前先进行测试方案的设计,会怎样?
2)若是咱们保证除非没有这个功能将致使测试失败,不然就不在程序中实现该功能,会怎样?
3)换一个角度,若是当测试时发现必须增长某项功能才可以经过测试时, 咱们就增长这一功能,会怎样?
大师们经过带着这些问题的实践, 发现这的确是一个提升软件代码质量, 使得效率获得保障的一个很好出发点。
以这样的思路进行软件开发,能够保证程序中的每一项功能都有测试来验证它是正确的,并且每当功能被无心修改时, 测试程序会发现。同时,也使咱们得到了一个新的观察点,从对程序调用者有利的视角来观察咱们的程序,这使得咱们在关心程序功能的自己还可以对接口予以足够感悟测试驱动开发的关注,使得其更容易被调用。另外,这种思路下的代码,将变得更加易于调用,也就必须使其与其它代码保持低耦合性。而且,当你想复用这些模块时,测试代码给出了很好的示例。这一切,使得软件开发工做的质量一会儿变得有保障了。
所以,测试驱动开发的精髓在于: 将测试方案设计工做提早,在编写代码以前先作这一项工做; 从测试的角度来验证设计,推导设计; 同时将测试方案看成行为的准绳,有效地利用其检验代码编写的每一步,实时验证其正确性,实现软件开发过程的"小步快走"。
实践测试驱动开发
下面,我就结合一个实际的小例子,来讲明如何进行"测试驱动开发"。本实例在J2SE SDK 1.4.2环境下开发,以及配套工具JUnit 3.8.1。
任务简述
队列是一种在程序开发中十分经常使用的数据结构,在此我就以编写一个实现队列功能的类--Queue为例进行说明。该类将实现如下基本运算:
判断队列是否为空:empty()
插入队列(即在队列未尾增长一个数据元素):inqueue(x)
出队列(也就是将队列首数据元素删除):outqueue()
取列头(也就是读者队列首数据元素的值):gethead()
清空队列(也就是将队列的全部数据元素全删除): clear()
查询x在队列中的位置:search(x)
测试案例分析
在测试驱动开发实践中,第一步就是考虑测试方案,经过分析该类的功能,咱们能够获得如下测试案例:
1) 队列为空测试
TC01: 队列新建时,应为空;
TC02: 清空队列后,应为空;
TC03: 当出队列操做次数与插入队列操做次数同样时,应为空;
2) 插入队列测试:
TC04: 插入队列操做后,新数据元素将插入在队列的未尾;
TC05: 插入队列操做后,队列将必定不为空;
3) 出队列测试
TC06: 出队列操做后,第一个数据元素将被从队列中删除;
4) 取队头测试
TC07: 取队头操做将得到队列中的第一个数据元素。
5) 清空队列测试
TC08: 清空队列操做后,队列将为空队列;
注: 此处为了讲解的方便,并未将全部的测试用例都列出,同时也选择了一些十分简单的测试用例。
第一次迭代
咱们首先编写第一个测试代码,这一测试代码只考虑了测试案例TC01, 也就是保证新建的队列为空:
import junit.framework.*;
//每一个使用JUnit编写的测试代码都应该包括本行
public class testQueue extends TestCase
//建立一个测试用例,继承TestCase
{
protected Queue q1;
public static void main (String[] args)
{
junit.textui.TestRunner.run (suite());
//执行测试用例
}
protected void setUp() //环境变量准备
{
q1= new Queue();
}
public static Test suite() //通用格式,指定测试内容
{
return new TestSuite(testQueue.class);
}
public void testEmpty() //如下每一个方法就是一个测试
{
assertTrue(q1.empty());
//当队列新建时,应为空-TC01
}
}
安装JUnit十分简单,只需在www.junit.org中下载最新的软件包(ZIP格式), 而后将其解压缩,而且将"JUnit安装目录\junit.jar" 以及"JUnit安装目录"都加到系统环境变量CLASSPATH中去便可。
执行套件能够像上述程序同样在main方法中使用,也能够直接在命令行调用:java junit.textui.TestRunner 测试类名(文本格式)、java junit.awtui.TestRunner 测试类名(图形格式,AWT版)、java junit.swingui.TestRunner测试类名(图形版,Swing版)。
编译执行(即在命令行执行javac testQueue.java和javatestQueue), 你会发现屏幕上出现提示:
.E 一个小点说明执行了一个测试用例,E表示其失败
Time: 0.11 说明执行测试共花费了0.11秒
There was 1 error: 说明存在一个错误
1) testEmpty(testQueue)java.lang.NoClassDefFoundError: Queue
at testQueue.setUp(testQueue.java:13)
at testQueue.main(testQueue.java:9)
FAILURES!!!
Tests run: 1, Failures: 0, Errors: 1
测试没有经过是确定的,由于Queue类都尚未写呢?怎么可能经过测试,所以,咱们就编写如下代码,以使测试经过:
public class Queue extends java.util.Vector
{
public Queue()
{
super();
}
public boolean empty()
{
return super.isEmpty();
}
}
将这个类编译后,再次执行测试程序,这时将出如下提示:
. 一个小点说明执行了一个测试用例,没有E表示其成功
Time: 0.11
OK (1 test)
你还可使用前面咱们说到的另两个命令,使测试反馈以图形化的形式体现出来,例如,执行java junit.awtui.TestRunner testQueue, 将出现:
图1
第二次迭代
接下来,咱们修改测试程序,加入测试案例TC0四、TC05的考虑。
import junit.framework.*;
public class testQueue extends TestCase
{
protected Queue q1,q2;
public static void main (String[] args)
{
junit.textui.TestRunner.run (suite());
}
protected void setUp() {
q1= new Queue();
q2= new Queue();
q2.inqueue("first"); /对队列q2执行插入队列操做
q2.inqueue("second");
}
public static Test suite()
{
return new TestSuite(testQueue.class);
}
public void testEmpty()
{
assertTrue(q1.empty());
//当队列新建时,应为空-TC01
}
public void testInqueue()
{
assertTrue(!(q2.empty()));
//执行了插入队列操做,队列就应不为空-TC05
assertEquals(1,q2.search("second"));
//search方法用于肯定元素在队列中的位置
//后插入的数据元素,应在未尾-TC04
//插入两个,第一个在位置0, 第二在位置1
}
}
根据这个测试代码,咱们须要在Queue类中添加上inqueue() 和search() 两个方法,以下所示:
public class Queue extends java.util.Vector
{
public Queue()
{
super();
}
public boolean empty()
{
return super.isEmpty();
}
public synchronized void inqueue (Object x)
{
super.addElement(x);
}
public int search(Object x)
{
return super.indexOf(x);
}
}
编译以后,再次执行java junit.awtui.TestRunnertestQueue, 你将再次看到成功的绿色。
图2
咱们仔细看一下这一界面。
1) 最上面列出了测试代码的类名,右边有一个"Run" 按钮,当你须要再次运行这一测试代码时,只需单击这个按钮。另外,将"Reload classesevery run" 选项打上勾颇有用,当你测试未经过(出现红色时), 你能够转身去修改代码,修改完后,只需再按"Run" 按钮就能够再次运行。
2) 中间区域是一个状态汇报区,红色表示未经过,统计了共运行了多少个测试(也就是在TestCase类中方法的数量)。
3) 若是测试时出现错误,例如,咱们不当心将"assertTrue(!(q2.empty()));" 误写成为"assertTrue(q2.empty());" 就将形成测试失败:
注:因为第一个测试仍是经过的,所以你会看到绿色条一闪。这时,你将会发现JUnit会将错误列出来,而且对应的"Run"按钮也由灰变成了亮,这表示你能够转身修改,完成后单击这个"Run按钮"能够只作刚才失效的这个测试,这将节省大量的时间。
同时,在最下面的窗体里,列出了失效的详细缘由。
后面的迭代
到这里,开发尚未完成,但这种思想却已经经过这样两个短小的实践传递出去了,后面的活你们能够动手试一下。
另外值得一提的是,这里虽然洋洋洒洒一大篇,实际两次迭代花费了我不到15分钟就完成了。并且,当看到绿条时,内心十分舒畅。
一些遗憾
文章到此就告一段落,但却有些许遗憾。
遗憾之一:这只是一篇文章,没有办法把全部方面都讲得面面俱到,以至于你们可能没法立刻上手。
正是因为这样的缘由,本文取名为"感悟", 与你们交流一下体会,但愿可以帮助你们更好地接受"测试驱动开发"的理念,并开始着手实践。
遗憾之二:笔者水平有限,没法解决你们的各类问题。
让笔者感到欣慰的是,记载着这些答案的《测试驱动开发》、《敏捷软件开发》、《拥抱变化: 解析极限编程》等大做都已悉数摆上了中国的书店。路虽难走,但明师已有。
实践永远是学习的最好方法,看到笔者的感悟,就开始极限之旅吧,由于那里风光无限,乐趣无限。当你掌握了测试驱动开发的精髓,那你就可以对你本身编写的全部代码充满信心,再也不担忧它们何时在你的后面放一冷箭,今后告别这给你带来无限压力的苦恼。