JUnit入门
01、什么是JUnit?
如果您要对撰写的程序进行测试,该如何进行呢?传统的测试方式通常依赖于人工对输出结果的判断,缺少效率且通常难以组织,且针对单一程序通常要设计专门的测试程序,如果您是在撰写Java,您可以使用JUnit来为您提供有效率的测试。
什么是JUnit?在这边引述一下JUnit FAQ中的解释。
JUnit是一个开放原始码的Java测试框架(testing framwork),它用来撰写与执行重复性的测试,它是用于单元测试框架的xUnit架构的实例。
JUnit包括以下的特性:
对预期结果的断言
对相同共享资料的测试装备
易于组织与执行测试的测试套件
图型与文字界面的测试器
JUnit最初是由Erich Gamma与Kent Beck撰写
代码: |
JUnit is an open source Java testing framework used to write and run repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
JUnit features include: Assertions for testing expected results Test fixtures for sharing common test data Test suites for easily organizing and running tests Graphical and textual test runners JUnit was originally written by Erich Gamma and Kent Beck. |
JUnit的用途是单元测试(Unit Test),它针对单一个套件(package)中的类别进行测试,它假设其它套件中的类别运作是正常的。另一个测试的主题是功能测试(Functional Test),它将整个系统当作一个黑盒子,测试时假设除了提供的界面操作之外(例如GUI或是Web界面操作),剩下的内部细节一无所知,在某些程度上来说,功能测试简单的说就是对成品的测试,对于一个中大型的系统来说,功能测试依赖一个团队,他们的目的就是找出系统的臭虫(bug),通常使用一些工具或是相关技术来进行测试。
JUnit的官方网站在:
02、设定
要设定JUnit,请先至JUnit官方网站下载JUnit3.8.1.zip(写这篇文章时的版本),下载后解开压缩档,当中会含有junit.jar档案,将这个档案复制至您所要的资料夹中,然后设定CLASSPATH,例如:
代码: |
set classpath=%classpath%;INSTALL_DIR\junit3\junit.jar |
如果是Windows 2000/XP,请在[系统内容/进阶/设定环境变量]中设定[系统变量]中的CLASSPATH,如果没有就自行新增,象是:
您可以在解开JUnit3.8.1.zip的目录下运行以下的测试范例,看看您的CLASSPATH是否设定正确:
文字模式测试范例:
代码: |
java junit.textui.TestRunner junit.samples.AllTests |
AWT图形模式测试范例:
代码: |
java junit.awtui.TestRunner junit.samples.AllTests |
SWING图形模式测试范例:
代码: |
java junit.swingui.TestRunner junit.samples.AllTests |
Swing测试范例的一个运行画面如下:
如果您使用Eclipse整合开发环境(IDE),在Eclipse中内含JUnit套件,如果在使用它的话,请在Package Explorer中的顶层项目名称上按右键执行[properties],在[Java Build Path]的[libraries]标签页中按[Add JARs...],选择junit.jar的所在位置将之加入,Eclipse是将之放置在plugins\org.junit_3.8.1中:
设定好之后在Package Explorer中就会出现JUnit套件相关信息,接下来您就可以直接在Eclpise中新增TestCase文件。
其它的IDE可以至这个网页看看如何在其中使用JUnit:
03、从Assert类开始
Assert提供一个断言方法(assert methods)的集合,象是assertXXXX()等,这些方法都是静态方法,它们会在断言失败时丢出一个实作Throwable的物件,当中会带有断言失败的讯息。
我们先撰写一个简单的Student类别,以利用它来进行一些测试:
代码: |
public class Student { private String _number; private String _name; private int _score;
public Student() { _number = null; _name = null; _score = 0; }
public Student(String num, String name, int score) { _number = num; _name = name; _score = score; }
public String getNumber() { return _number; }
public String getName() { return _name; }
public int getScore() { return _score; }
public void setNumber(String num) { _number = num; }
public void setName(String name) { _name = name; }
public void setScore(int score) { _score = score; } } |
我们先来看看在不使用JUnit的情况下,我们如何撰写一个测试表达式(Test Expression)以测试我们的类别撰写无误,例如您可能是这么撰写的:
代码: |
public class Test2 { public static void main(String[] args) { Student student = new Student("B83503124", "Justin", 100);
if(!(student.getName() != null && student.getName().equals("Justin"))) System.out.println("getName method failure"); } } |
同样一个功能,如果我们使用Assert提供的断言方法,则可以如下撰写:
代码: |
import junit.framework.*;
public class Test3 { public static void main(String[] args) { Student student = new Student("B83503124", "Justin", 100);
Assert.assertEquals("getName metohd failure", "Justin", student.getName()); } } |
Assert被归类于junit.framework中;如果断言失败,我们会收到一个Throwable物件,有这个例子中会是ComparisonFailure物件,例如我们故意让以上断言失败的话,可能收到以下的讯息:
代码: |
Exception in thread "main" junit.framework.ComparisonFailure: getName metohd failure expected: but was: at junit.framework.Assert.assertEquals(Assert.java:81) at Test3.main(Test3.java:7) |
如果您不想收到这些冗长的讯息,您可以使用ComparisonFailure的getMessage()方法取得您指定的错误显示讯息,象是上面的"getName method failure"。
assertEquals()被重载为多个版本,以应付各种的资料型态,象是int、String等等,如果是比较物件的话,则根据equals()方法传回的true或false来比较,所以在您想要比较两个物件的field是否相同时,您要重新定义您的equals()方法,例如在我们的Student中,我们的equals()可以定义如下:
代码: |
public boolean equals(Object anObject) { if (anObject instanceof Student) { Student aStudent = (Student) anObject; return aStudent.getNumber().equals(getNumber()) && aStudent.getName() == getName() && aStudent.getScore() == getScore(); } return false; } |
传统的测试表达式可以这么撰写:
代码: |
Student student1 = new Student("B83503124", "Justin", 100); Student student2 = new Student("B83503124", "Justin", 100);
if(!student1.equals(student2)) System.out.println("instances are unequal"); |
使用Assert的assertEquals()则可以这么撰写:
代码: |
Student student1 = new Student("B83503124", "Justin", 100); Student student2 = new Student("B83503124", "Justin", 100);
Assert.assertEquals("instances are unequal", student1, student2); |
比较两个物件内容是否相同,通常是测试时很常进行的一个动作,而Assert类别也提供了象是assertTure()、assertFalse()、assertSame() 、assertNotSame()、assertNull()、assertNotNull()等等的方法,以便利各种测试场合的需要,例如我们上面的测试也可以这么撰写:
代码: |
Student student1 = new Student("B83503124", "Justin", 100); Student student2 = new Student("B83503124", "Justin", 100);
Assert.assertTure(student1.equals(student2)) |
在JUnit中会很常使用到Assert中的各种方法,但以上只是单一个Assert类别的几个方法介绍,还谈不上是一个测试,只要将各个测试进行组合,才谈的上是真正的测试开始,之后再慢慢一一介绍。
04、测试案例(TestCase)
使用Assert类别中所提供的assertXXX()方法可以让您进行各种断言,如果断言失败,则可能传回AssertionFailedError或ComparisonFailure物件,您可以利用try....catch区块收集并显示这些物件所夹带的讯息,然后重新返回测试,然而事实上您不用自行设计,JUnit提供TestCase类别,您可以继承这个类别进行测试案例的撰写,并使用它的run()方法进行测试,TestCase物件会自行帮您收集测试失败时的相关讯息,之后您只要取得TestResult物件,就可以显示相关的讯息。
我们一步一步来看如何使用测试案例,首先还是哪个简单的Student类别:
代码: |
public class Student { private String _number; private String _name; private int _score;
public Student() { _number = null; _name = null; _score = 0; }
public Student(String num, String name, int score) { _number = num; _name = name; _score = score; }
public String getNumber() { return _number; }
public String getName() { return _name; }
public int getScore() { return _score; }
public void setNumber(String num) { _number = num; }
public void setName(String name) { _name = name; }
public void setScore(int score) { _score = score; } } |
基本上您每设计一个类别,您就应该为它撰写一个测试,这个测试将成为您日后重构(refactor)的测试准测;我们先从简单的测试案例开始下手,如下所示:
代码: |
import junit.framework.*;
public class TestIt extends TestCase { public TestIt(String name) { super(name); }
public void testGetMethod() { Student student = new Student("B83503124", "Justin", 100); assertEquals("B83503124", student.getNumber()); assertEquals("Justin", student.getName()); assertEquals(100, student.getScore()); }
public void testSetMethod() { Student student = new Student(); student.setNumber("B83503124"); student.setName("Justin"); student.setScore(100); assertEquals("B83503124", student.getNumber()); assertEquals("Justin", student.getName()); assertEquals(10, student.getScore()); } } |
您如上继承TestCase类别,并撰写您想要进行的测试内容,TestCase继承了Assert类别,所以您可以直接使用Assert中的一些断言方法;我故意在testSetMethod()方法中撰写会发生错误的断言,这样待会才看得到一些错误报告,接下来我们撰写一个主函式来进行测试:
代码: |
import java.util.*; import junit.framework.*;
public class Main { public static void main(String[] args) { TestCase test1 = new TestIt("TestGet") { protected void runTest() { testGetMethod(); } }; showResult(test1.run());
TestCase test2 = new TestIt("TestSet") { protected void runTest() { testSetMethod(); } }; showResult(test2.run()); }
public static void showResult(TestResult result) { if(result.errorCount() > 0) { System.out.println("error: " + result.errorCount()); Enumeration error = result.errors(); while(error.hasMoreElements()) { System.out.println(error.nextElement()); } }
if(result.failureCount() > 0) { System.out.println("failure: " + result.failureCount()); Enumeration failure = result.failures(); while(failure.hasMoreElements()) { System.out.println(failure.nextElement()); } } } } |
在这边我所使用的是测试案例进行测试时的一个静态指定方式,我使用一个匿名类别并重新定义runTest()方法,TestCase的run()方法会调用runTest()方法,并将测试的结果收集出来,传回一个TestResult物件。
TestResult物件拥有一些显示测试结果的相关方法,象是上面所示的errorCount()、failureCount()等等方法,errors()与failures()方法会传回Enumeration物件,测试的结果讯息就包括在当中,我们在这边直接将物件的内容显示出来,以显示一些简单的讯息,执行这个程序,其结果如下所示:
代码: |
failure: 1 TestSet(Main$2): expected:<10> but was:<100> |
我们也可以使用动态的方法进行案例测试,这会使用到Java的Reflection机制来实作runTest()的内容,我们只要在建构TestCase物件时传入我们所想要的测试方法名称即可,例如:
代码: |
TestCase test1 = new TestIt("testGetMethod"); showResult(test1.run());
TestCase test2 = new TestIt("testSetMethod"); showResult(test2.run()); |
将上面的程序片段取代之前main函式中的内容就可以进行测试,其结果显示与之前是相同的。
您可以自行执行TestCase的run()方法以收集测试结果并显示出来,事实上JUnit提供了TestRunner工具类别可以帮您省去这个工夫,而我们之后也会介绍使用TestSuite类别来进行测试的组织,这会比自行定义runTest()的实作并调用TestCase的run()方法好用的多。
05、测试套件(TestSuite)
在之前的文件中,我们说明如何继承TestCase来撰写测试,如何静态或动态的调用我们撰写好的测试方法,以及如何使用收集到的TestResult将结果显示出来,您会问:有没有更好的方法可以更有弹性的组织测试方法、进行测试并显示结果?答案是有的,您可以将测试组织在一个测试套件(TestSuit)中,并使用TestRunner工具类别来帮您完成测试并显示结果。
首先还是先最出我们的那个Student类别:
代码: |
public class Student { private String _number; private String _name; private int _score;
public Student() { _number = null; _name = null; _score = 0; }
public Student(String num, String name, int score) { _number = num; _name = name; _score = score; }
public String getNumber() { return _number; }
public String getName() { return _name; }
public int getScore() { return _score; }
public void setNumber(String num) { _number = num; }
public void setName(String name) { _name = name; }
public void setScore(int score) { _score = score; } } |
接下来我们撰写我们的一些测试方法:
代码: |
import junit.framework.*;
public class TestIt extends TestCase { public TestIt(String name) { super(name); }
public void testGetMethod() { Student student = new Student("B83503124", "Justin", 100); assertEquals("B83503124", student.getNumber()); assertEquals("Justin", student.getName()); assertEquals(100, student.getScore()); }
public void testSetMethod() { Student student = new Student(); student.setNumber("B83503124"); student.setName("Justin"); student.setScore(100); assertEquals("B83503124", student.getNumber()); assertEquals("Justin", student.getName()); assertEquals(10, student.getScore()); } } |
同样的,我们故意在testSetMethod()中造成测试失败,这样我们才可以看到一些测试讯息,我们先介绍最简单的一种测试执行方式,直接使用Reflection来取得要执行的测试:
代码: |
import junit.framework.*;
public class Main { public static void main(String[] args) { junit.textui.TestRunner.run(TestIt.class); } } |
junit.textui.TestRunner是JUnit中所提供的一个测试工具类别,我们传给它一个TestIt的Class物件,这样它就可以使用Reflection机制,预设是将所有testXXXX()开头的方法辨识出来并进行测试,然后自动收集结果并显示出来,执行结果如下:
代码: |
..F Time: 0.027 There was 1 failure: 1) testSetMethod(TestIt)junit.framework.AssertionFailedError: expected:<10> but was:<100> at TestIt.testSetMethod(TestIt.java:22) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at Main.main(Main.java:5)
FAILURES!!! Tests run: 2, Failures: 1, Errors: 0 |
这个测试结果显示比自行处理讯息显示来得丰富的多了。
JUnit也提供了图形界面的TestRunner类别,您可以用以下的程序来取代main()中的陈述:
AWT测试界面:
代码: |
junit.awtui.TestRunner.run(TestIt.class); |
Swing测试界面:
代码: |
junit.swingui.TestRunner.run(TestIt.class); |
下图为Swing测试界面的一个画面:
您可以在TestIt重新定义suite()方法,在JUnit 2.0之后,提供一个简单的动态方法产生TestSuit物件,您只要将TestCase的Class物件传给它作为参数就可以了:
代码: |
public static Test suite() { return new TestSuite(TestIt.class); } |
这个方式会使用Reflection来辨识出所有的testXXXX()方法并加入测试,注意到传回型态是Test,因为JUnit的TestCase与TestSuite是组合(Composite)模式关系,TestSuite实现了Test界面,关于组合模式的说明请见:
您可以使用静态的方法来建构TestSuite物件,例如:
代码: |
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest( new TestIt("TestGet") { protected void runTest() { testGetMethod(); } } );
suite.addTest( new TestIt("TestSet") { protected void runTest() { testSetMethod(); } } ); return suite; } |
如果您了解之前的TestCase文件介绍,您应该也可以知道使用动态方法建构TestSuite:
代码: |
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new TestIt("testGetMethod")); suite.addTest(new TestIt("testSetMethod")); return suite; } |
定义好suite()方法之后,您可以如下撰写测试:
代码: |
import junit.framework.*;
public class Main { public static void main(String[] args) { junit.textui.TestRunner.run(TestIt.suite()); } } |
如果您使用swingui的测试程序,您可以直接这么执行所定义的测试套件:
代码: |
junit.swingui.TestRunner.run(TestIt.class); |
使用swingui的测试程序时,如果不定义suite()方法,则预设会取出所有的testXXXX()进行测试。
您也许会问到,如果不使用TestRunner,可不可以自行定义测试的执行,答案是可以的,您甚至可以合并其它的测试套件:
代码: |
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new TestIt("testGetMethod")); suite.addTest(new TestIt("testSetMethod")); suite.addTestSuite(AnotherTestSuiteImplementation.class); return suite; } |
代码: |
import junit.framework.*;
public class Main { public static void main(String[] args) { TestSuite suite = TestIt.suite(); TestResult result = new TestResult(); suite.run(result); showResult(result); }
public static void showResult(TestResult result) { if(result.errorCount() > 0) { System.out.println("error: " + result.errorCount()); Enumeration error = result.errors(); while(error.hasMoreElements()) { System.out.println(error.nextElement()); } }
if(result.failureCount() > 0) { System.out.println("failure: " + result.failureCount()); Enumeration failure = result.failures(); while(failure.hasMoreElements()) { System.out.println(failure.nextElement()); } } } } |
JUnit提供各种弹性的测试组合与方便的工具类别,以上所示的只是几个简单的例子,您可以根据自行的需求来进行测试的组织。
06、测试设备(Test fixture)
测试设备(Test fixture)是测试时都会用到的一组固定物件,说是固定物件,是表示这个物件在每次测试开始时都处于一个固定的初始状态,每个测试方法要用到这组物件时,都由这个状态开始操作起。
在JUnit中,我们定义一个测试案例(Test case),而在测试案例中,我们可以定义setUp()与tearDown()这两个方法来建立测试设备与拆除测试设备,我们直接使用个简单的例子来说明何时使用这两个方法。
首先我们定义一个简单的类别,它可以支援物件的档案写入与读出:
代码: |
import java.util.*; import java.io.*;
public class ObjectIOManager { private String _filename;
public ObjectIOManager(String filename) { _filename = filename; }
// 读出档案中的物件信息 public Object[] readObjects() { ObjectInputStream objInput = null; ArrayList students = new ArrayList();
try { objInput = new ObjectInputStream(new FileInputStream(_filename));
System.out.print("Reading data from " + _filename + " ... "); try { while(true) students.add(objInput.readObject()); } catch(EOFException e) {}
objInput.close(); System.out.println("done"); } catch(ClassNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } return students.toArray(); }
// 写入物件至档案中 public void writeObjects(Object[] objs) { ObjectOutputStream objOutput = null;
try { objOutput = new ObjectOutputStream(new FileOutputStream(_filename));
System.out.print("Writing data to " + _filename + " ... "); for(int i = 0; i < objs.length; i++) objOutput.writeObject(objs[i]); objOutput.flush(); objOutput.close(); System.out.println("done"); } catch(IOException e) { e.printStackTrace(); } } } |
我们要测试这个类别是否如我们所设计般正常工作,我们先测试可不可以写字串物件至档案中再读出,然后测试一个实作Serializable的物件是否可以写入档案再读出,这个实作Serializable的类别我们定义如下:
代码: |
import java.io.*;
public class WritableObject implements Serializable { private String _name;
public WritableObject(String name) { _name = name; }
public boolean equals(Object obj) { if(_name.equals(((WritableObject)obj)._name)) return true; else return false; } } |
我们撰写一个测试案例以进行测试:
代码: |
import junit.framework.TestCase;
public class ObjectIOManagerTest extends TestCase {
public ObjectIOManagerTest(String arg0) { super(arg0); }
public void testSimpleObjectIO() { ObjectIOManager manager = new ObjectIOManager("test.dat"); String[] simpleObjects = {"Test1", "Test2", "Test3", "Test4"}; manager.writeObjects(simpleObjects); Object[] objs = manager.readObjects(); for(int i = 0; i < simpleObjects.length; i++) assertEquals(simpleObjects[i], (String)objs[i]); }
public void testStudentObjectIO() { ObjectIOManager manager = new ObjectIOManager("test.dat"); String[] simpleObjects = {"Test1", "Test2", "Test3", "Test4"}; WritableObject[] writable = new WritableObject[simpleObjects.length]; for(int i = 0; i < simpleObjects.length; i++) writable[i] = new WritableObject(simpleObjects[i]); manager.writeObjects(writable); Object[] objs = manager.readObjects(); for(int i = 0; i < writable.length; i++) assertEquals(writable[i], (WritableObject)objs[i]); }
public static void main(String[] args) { junit.textui.TestRunner.run(ObjectIOManagerTest.class); } } |
在testSimpleObjectIO()与testStudentObjectIO()这两个方法中,很显然的有重复的程序码,它们是可以重复使用的物件,我们将这些重复的程序码重新撰写有setUp()方法中,我们重新改写如下:
代码: |
import junit.framework.TestCase;
public class ObjectIOManagerTest extends TestCase { private ObjectIOManager _manager; String[] _simpleObjects = {"Test1", "Test2", "Test3", "Test4"};
public ObjectIOManagerTest(String arg0) { super(arg0); }
// 设定测试设备 protected void setUp() throws Exception { super.setUp(); _manager = new ObjectIOManager("test.dat"); }
// 拆除测试装备 protected void tearDown() throws Exception { super.tearDown(); _manager = null; }
public void testSimpleObjectIO() { _manager.writeObjects(_simpleObjects); Object[] objs = _manager.readObjects(); for(int i = 0; i < _simpleObjects.length; i++) assertEquals(_simpleObjects[i], (String)objs[i]); }
public void testStudentObjectIO() { WritableObject[] writable = new WritableObject[_simpleObjects.length]; for(int i = 0; i < _simpleObjects.length; i++) writable[i] = new WritableObject(_simpleObjects[i]); _manager.writeObjects(writable); Object[] objs = _manager.readObjects(); for(int i = 0; i < writable.length; i++) assertEquals(writable[i], (WritableObject)objs[i]); }
public static void main(String[] args) { junit.textui.TestRunner.run(ObjectIOManagerTest.class); } } |
在上面的程序中,我们将字符串阵列物件_simpleObjects设定为field,这样可以避免每次都生成新的字符串物件,而_manager则撰写在setUp()方法中,setUp()方法会在每一次执行testXXXX()方法前呼叫,以建立一些测试装备物件,而tearDown()会在每一次testXXXX()方法执行完毕后呼叫,以拆除测试装备物件,在这个程序中使用tearDown()比较没有意义,它只是将_manager参考至null物件,然后对于一些物件,例如该物件涉及到网络连线或数据库连线时,可以在tearDown()中撰写一些关闭连线或是关闭资源的程序,这时就显示tearDown()的重
要。
注意到setUp()与tearDown()方法在每一次testXXXX()执行的前后都会被调用,所以不要被_manager设定为field而迷惑了,在testXXXX()开始执行时,测试设备都会重新产生一组测试装备物件,所以您的每一个testXXXX()都会得到新的测试装备物件,这个测试装备物件与前一次testXXXX()执行时的装备物件是没有关系的。
07、失败(falure)与错误(error)
JUnit将测试不通过的结果区分为失败(failure)与错误(error),失败指的是断言结果为false,也是您所预期应该成功的测试失败了,而错误指的是意料之外的异常,这个异常是您没有考虑到的,也许会造成程序的错误,或是程序的终止,例如ArrayIndexOutOfBound***ception就是一个错误。
有时候您设计好一个类别,在测试的时候您注重于一些断言的测试,然而程序中可能有一些部份可能是您没有考虑到的,JUnit的错误就是在帮您捕捉这些没有考虑到的部份,我们使用一个简单的例子作为说明,首先我们设计一个类别:
代码: |
public class SimpleArray { private int[] _array;
public SimpleArray() { _array = new int[10]; }
public SimpleArray(int length) { _array = new int[length]; }
public void set(int index, int value) { _array[index] = value; }
public int get(int index) { return _array[index]; }
public int length() { return _array.length; } } |
为了测试这个简单的类别,我们撰写一个测试案例:
代码: |
import junit.framework.TestCase;
public class SimpleArrayTest extends TestCase { private SimpleArray _simpleArray;
public SimpleArrayTest(String arg0) { super(arg0); }
protected void setUp() throws Exception { super.setUp(); _simpleArray = new SimpleArray(); }
protected void tearDown() throws Exception { super.tearDown(); _simpleArray = null; }
public void testSetGet() { for(int i = 0; i < _simpleArray.length(); i++) _simpleArray.set(i, i); for(int i = 0; i < _simpleArray.length()+1; i++) assertTrue(i == _simpleArray.get(i)); }
public static void main(String[] args) { junit.textui.TestRunner.run(SimpleArrayTest.class); } } |
我故意在测式get()方法时存取超过阵列长度的元素,来看一下测试的结果如何?
代码: |
.E Time: 0.01 There was 1 error: 1) testSetGet(SimpleArrayTest)java.lang.ArrayIndexOutOfBound***ception: 10 at SimpleArray.get(SimpleArray.java:17) at SimpleArrayTest.testSetGet(SimpleArrayTest.java:24) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at SimpleArrayTest.main(SimpleArrayTest.java:28)
FAILURES!!! Tests run: 1, Failures: 0, Errors: 1 |
测试指出了一个错误,并告知了这个错误来自于存取超过阵列边界,这表示您的这个类别最好为它加上边界检查了。
如果您的类别在执行某些功能时,指明必须处理某些例外,例如IOException,您可以在testXXXX()中使用try.....catch来处理它,或是将之丢出,由呼叫testXXXX()的来源来处理它,下面这个程序是个实例,假设java.io.FileReader是您所设计的类别,我们提供一个测试档案test.txt,当中只撰写了一个字符'1':
代码: |
import java.io.*; import junit.framework.TestCase;
public class FileReaderTest extends TestCase { private FileReader _reader;
public FileReaderTest(String arg0) { super(arg0); }
public void setUp() throws FileNotFoundException { _reader = new FileReader("test.txt"); }
public void tearDown() throws IOException { _reader.close(); }
public void testRead() throws IOException { assertTrue('1' == _reader.read()); }
public void testClose() throws IOException { _reader.close(); try { _reader.read(); fail("reading file after closed and didn't throw IOException"); } catch(IOException e) { } }
public static void main(String[] args) { junit.textui.TestRunner.run(FileReaderTest.class); } } |
注意到 testClose() 这个方法所进行的测试,有时候您所测试的可能是一个预期会丢出的例外,您想要看看当错误的情况成立时,是不是真会丢出例外,例如 testClose() 测试 FileReader 在 close() 之后如果再 read(),是不是会如期丢出IOException,我们先行在方法中用try....catch捕捉了这个例外,如果没有如期丢出例外,则不会被catch捕捉,而程序流程继续往下,执行到 fail() 陈述,这表示例外处理没有发生,我们此时主动丢出一个失败(failure),表示程序的执行并不如我们所预期的。
08、基本构建过程
了解JUnit的基本建构过程,可以了解测试的目标是什么、测试的基本流程与架构,而进一步的了解JUnit是如何运作的。
这些文章主要从下面这个网址的文章撷取重点部份,并以自己的心得撰写而成,如果您需要更多详细的内容或是原音重现,可以直接参考原文章:
我们从一个测试案例(Test Case)开始,这个测试案例设计为可以重用(这是JUnit的目的之一,一个可重用的测试框架),我们设计一个TestCase类别,它可以执行测试,所有的一切都是由run()开始:
代码: |
public abstract class TestCase { private final String fName;
public TestCase(String name) { fName= name; }
public abstract void run(); } |
测试的执行过程中,我们会需要一些测试设备(Test Fixture),也就是之前所说的,我们需要一些可重用的测试物件,这些测试物件在执行测试之前初始化,并在执行测试之后拆除,当然,设计是以重用为目标,为此,我们采用「样版方法」(template method)():
代码: |
public abstract class TestCase { private final String fName;
public TestCase(String name) { fName= name; }
protected void runTest() { }
protected void setUp() { }
protected void tearDown() { }
public void run() { setUp(); runTest(); tearDown(); } } |
一个测试案例中可能进行多种操作的测试,我们希望对测试的结果进行收集,我们定义一个TestResult类别来包括测试的结果报告:
代码: |
public class TestResult extends Object { protected Vector fFailures; protected Vector fErrors; protected int fRunTests;
public TestResult() { fFailures= new Vector(); fErrors= new Vector(); fRunTests= 0; }
public synchronized void startTest(Test test) { fRunTests++; }
// 断言失败 public synchronized void addFailure(Test test, Throwable t) { fFailures.addElement(new TestFailure(test, t)); }
// 非预期的异常 public synchronized void addError(Test test, Throwable t) { fErrors.addElement(new TestFailure(test, t)); }
} |
TestFailure是一个辅助类别,它用于将失败的测试与发起的例外绑定在一起,以待之后报告时使用。
代码: |
public class TestFailure extends Object { protected Test fFailedTest; protected Throwable fThrownException; } |
TestResult用fRunTest来记录被执行测试的数目,为了使用TestResult来收集测试报告,我们将它当作参数传给run()方法,为此,我们修改TestCase的内容:
代码: |
public abstract class TestCase { private final String fName;
public TestCase(String name) { fName= name; }
protected void runTest() { }
protected void setUp() { }
protected void tearDown() { }
protected TestResult createResult() { return new TestResult(); }
public TestResult run() { TestResult result= createResult(); run(result); return result; }
public void run(TestResult result) { result.startTest(this); setUp(); try { runTest(); } catch (AssertionFailedError e) { // 断言失败 result.addFailure(this, e); } catch (Throwable e) { // 非预期的异常 result.addError(this, e); } finally { tearDown(); } } } |
AssertionFailedError继承自Error类别,在断言失败所丢出的,象是:
代码: |
protected void assert(boolean condition) { if (!condition) throw new AssertionFailedError(); } |
到这边为止,我们该如何使用我们设计的这些类别,一个方法是继承TestCase,撰写testXXXX()方法,然后新增一个TestCase实例,在当中复写runTest()方法,例如我们实作一个MoneyTest类别,它继承TestCase,我们可以这么执行测试:
代码: |
TestCase test= new MoneyTest("testMoneyEquals ") { protected void runTest() { testMoneyEquals(); } }; |
MoneyTest继承了TestCase,并重新定义了runTest(),这实作了一种「配接模式」(Adapter Pattern)()。我们也可以采另一个动态的方法,在实例化TestCast时,指定testXXXX()的名称给它,以Java的Reflection机制,动态的找出指定的方法,所以我们可以在TestCase中的runTest()先实作这样的内容:
代码: |
public abstract class TestCase { private final String fName;
public TestCase(String name) { fName= name; }
protected void runTest() throws Throwable { Method runMethod= null; try { runMethod= getClass().getMethod(fName, new Class[0]); } catch (NoSuchMethodException e) { assertTrue("Method \""+fName+"\" not found", false); } try { runMethod.invoke(this, new Class[0]); } // catch InvocationTargetException and IllegalAcces***ception }
protected void setUp() { }
protected void tearDown() { }
protected TestResult createResult() { return new TestResult(); }
public TestResult run() { TestResult result= createResult(); run(result); return result; }
public void run(TestResult result) { result.startTest(this); setUp(); try { runTest(); } catch (AssertionFailedError e) { // 断言失败 result.addFailure(this, e); } catch (Throwable e) { // 非预期的异常 result.addError(this, e); } finally { tearDown(); } } } |
我们可以这样实例化一个TestCase:
代码: |
TestCase test= new MoneyTest("testMoneyEquals "); |
测试不仅是单一个测试的进行,我们希望能组合几个以上的测试为一个测试套件(Test Suite),为了简单的组合个别的测试,我们定义一个Test界面:
代码: |
public interface Test { public abstract void run(TestResult result); } |
TestCase要实作Test界面:
代码: |
public abstract class TestCase implements Test { private final String fName;
public TestCase(String name) { fName= name; }
protected void runTest() throws Throwable { Method runMethod= null; try { runMethod= getClass().getMethod(fName, new Class[0]); } catch (NoSuchMethodException e) { assertTrue("Method \""+fName+"\" not found", false); } try { runMethod.invoke(this, new Class[0]); } // catch InvocationTargetException and IllegalAcces***ception }
protected void setUp() { }
protected void tearDown() { }
protected TestResult createResult() { return new TestResult(); }
public TestResult run() { TestResult result= createResult(); run(result); return result; }
public void run(TestResult result) { result.startTest(this); setUp(); try { runTest(); } catch (AssertionFailedError e) { // 断言失败 result.addFailure(this, e); } catch (Throwable e) { // 非预期的异常 result.addError(this, e); } finally { tearDown(); } } } |
对于Test型态的物件来说,它只要调用run(TestResult result)就可以进行动作,这实现了「命令模式」(Command Pattern)()。
为了组合个别的TestCase,我们定义一个测试套件TestSuite,它也实作了Test界面:
代码: |
public class TestSuite implements Test { private Vector fTests= new Vector();
public void addTest(Test test) { fTests.addElement(test); }
public void run(TestResult result) { for (Enumeration e= fTests.elements(); e.hasMoreElements(); ) { Test test= (Test)e.nextElement(); test.run(result); } } } |
TestCase与TestSuite都实作了Test界面,TestSuite可以藉由addTest()来组合TestCase,而调用时都是调用run(TestResult result)方法,这是一种「组合模式」(Composite Pattern)()。
有了TestSuite类别,我们可以在TestCase中撰写一个静态的suite()方法来组合TestCase并传回TestSuite物件,有两种方式,第一种可以自行组合:
代码: |
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new MoneyTest("testMoneyEquals")); suite.addTest(new MoneyTest("testSimpleAdd")); } |
第二种利用Java的Reflection机制,找出TestCase中所有的testXXXX()方法:
代码: |
public static Test suite() { return new TestSuite(MoneyTest.class); } |
第二种方式的实作部份这边不再说明;以上的说明只是程序建构片段,有了这些说明,您可以自行阅读JUnit的原始码,以进一步了解更多的细节!
我们将以上的构造用UML类别图来表示,您可以更清楚的看出JUnit的基本架构:
这篇文章我省略了一些细节与我还没有接触过的模式说明,再次提醒您的是若您要更详细的说明,请参考原文。
09、测试的分解与组合
以下将一些在线文章阅读过后,撷取部份加上个人心得整理而来,文中附上出处,如果您感兴趣想进一步看看原文,可以直接点选。
基本上,您所设计的每一个类别都要伴随着一个测试,单元测试通常在检验类别中的公用方法,然而并不是每个接口都要测试,象是get()或set()方法,例如您在某个测试中测试set()与get()方法:
代码: |
public class SomeClass { private int x; public void setX(int x) { this.x = x; }
public int getX() { return x; } } |
代码: |
testSetGetXOfSomeClass() { someClass.setX(10); assertEquals(10, someClass.getX()); } |
其实您上面的测试就等于在进行下面的测试:
代码: |
testSetGetXOfSomeClass() { x = 10; assertEquals(10, x); } |
或者也等同于下面的测试:
代码: |
testSetGetXOfSomeClass() { assertEquals(10, 10); } |
简单的说,您等于是在测试编译器的正确性,或是直译器的正确性,您的SomeClass在编译完成之后,其实编译器已经验证过您的程序是否正确的,像这样简单的set()、get()方法,您并不用再亲自验证一次。
()
不过这也不是告诉您set()、get()方法不用测试,只不过您要有所选择,如果您的set()方法中使用了某些方法来过滤设定给它的参数,或是您的get()方法在传回数值之前先进行了若干处理,而您的类所表现的行为会因此而受到影响,则set()、get()方法还是有测试的必要,如果您的程序已经有了错误,而您怀疑是set()、get()方法所导致,那就要进行测试。
另外,有些方法中只是委托其它物件进行处理,它可能传回一个值或只是简单的进行其它的工作,例如:
代码: |
public void myMethod(final int a, final String b) { myCollaborator.anotherMethod(a, b); } |
测试myMethod()是不被建议的,因为行为的主要处理者是myCollaborator,您要测试的应该是myCollaborator的所属类别,而不是myMethod()。
()
再者,您该为每一个类撰写一个TestCase吗?提出这个问题似乎与我们一开始所提出的建议矛盾,其实就TestCase来说,它只是提供一个方法让您进行测试组合,为每一个类别撰写一个测试是比较方便,但不是一定需要,有时候您要发现某几个类会使用同一组测试装备,您会发现为这几个类分别撰写TestCase,并在其中包括这组测试装备有些麻烦且重复了一些工作,此不如在同一个TestCase中包括这些类,以方便使用同一组测试装备。
相反的,有时您会发现虽然我要测试的是同一个类别的行为,但是它们使用了两组不同的测试设备,某些行为只使用其中一组,而另一些行为使用另一组,然而一个TestCase是TestSuite是最小的单位,这时您要作的,可以为这个类别撰写两个TestCase,而不是只有一个类别伴随着一个TestCase。
()
有新的相关心得,会直接再发表于这个主题下。。。。
10、先写测试(一)
在您开始设计一个类别之前,先写测试!您的测试就是有关于类别职责的描述,您在测试中说明您要类别作些什么,然后接下来您开始撰写程序,您的目的就是为了让测试能够通过。
在开始撰写类别之前先写测试,您会有一些疑问:
代码: |
我连类别都还没写,怎么写测试? 不可能吧!这个测试甚至连编译都不能通过! 我连类别要作些什么都还没写出来,怎么会知道要测试什么? ...... |
以上这些理由都不是拒绝先写测试的理由,没有人规定您要先将类别写好再来写测试,您的测试是可以通过编译的,如果您连类别要作些什么都不知道,您怎么写程序?
这么写太空洞,我们实际来举个例子,告诉您如何先写测试,如何通过测试以及先写测试的好处。
我先举一个需求:
您想要设计一个类别,该类别可以用简单的方法让您直接进行物件的写入档案与读出,让物件的功能为:可以藉由提供一个物件阵列与文件名称给它,就可以将物件写入指定的档案,藉由提供一个文件名称,就可以要它从指定的档案中读出资料。
该类别除了指定了一个不存在的档案而引发的错误要调用它的物件亲自处理之外,其它的错误由该类别处理,提示错误讯息给使用者知道。
假设我们设计的类别名称是SimpleObjectIO,看了这个需求,我们一一为它们设计测试,首先我们要测试物件写入档案,要测试写入档案,最好的方式就是出档案中的物件资料,然后再比较与之前写入的物件是否相等,于是我们可以动手写我们的第一个测试,我们写入10个String物件至档案,然后读回它们并作比较:
代码: |
public void testSimpleDataObjectIO() throws Exception { SimpleObjectIO objIO = new SimpleObjectIO(); String[] writableObjs = new String[10];
for(int i = 0; i < writableObjs.length; i++) { writableObjs[i] = new String("TestString" + i); }
bjIO.writeObjectsToFile(writableObjs, "test.dat"); Object[] objs = objIO.readObjectsFromFile("test.dat");
for(int i = 0; i < writableObjs.length; i++) assertEquals(writableObjs[i], objs[i].toString()); } |
再来我们设计测试,看看物件的附加是否正确,方法是先写入一些物件,然后附加一些物件,接着我们读回物件,比较之前写入与附加的物件是否相同:
代码: |
public void testSimpleDataObjectAppend() throws Exception { SimpleObjectIO objIO = new SimpleObjectIO(); String[] writableObjs = new String[10];
for(int i = 0; i < writableObjs.length; i++) { writableObjs[i] = new String("TestString" + i); }
objIO.writeObjectsToFile(writableObjs, "test.dat"); String[] append = new String[1]; append[0] = new String("TestString10"); objIO.appendObjectsToFile(append, "test.dat");
Object[] objs = objIO.readObjectsFromFile("test.dat"); for(int i = 0; i < writableObjs.length; i++) assertEquals(writableObjs[i].toString(), objs[i].toString());
assertEquals(append[0], objs[writableObjs.length]); } |
我们发现了这两个测试的前两行是重复的,这部份我们可以当作固定的测试设备并写在setUp()方法中:
代码: |
protected void setUp() throws Exception { super.setUp(); objIO = new SimpleObjectIO(); writableObjs = new String[10]; for(int i = 0; i < writableObjs.length; i++) { writableObjs[i] = new String("TestString" + i); } } |
再来我们设计需求中的第二段之测试,第一,读回档案时指定的名称若不存在,这个错误必须由呼叫它的物件处理,所以正常来说,这个错误必须丢出一个例外,我们必须测试这个例外是否真的被丢出:
代码: |
public void testReadFormNonExistedFile() { try { objIO.readObjectsFromFile("nothisfile.dat"); fail("FileNotFoundException not thrown"); } catch(FileNotFoundException e) { // exception catched, do nothing } } |
在上面的测试中,我们指定一个不存在的档案来进行写入,预期中会丢出FileNotFoundException例外,如果没有丢出例外,则程序会继承执行至fail()方法,这时我们声明发生一个failure,表示测试未符合预期。同样的,我们再设计一个测试,看看如果对一个不存在的档案进行附加写入时,会不会丢出FileNotFoundException例外:
代码: |
public void testAppendToNonExistedFile() { try { objIO.appendObjectsToFile(writableObjs, "nothisfile.dat"); fail("FileNotFoundException not thrown"); } catch(FileNotFoundException e) { // exception catched, do nothing } } |
至此,我们已为所要设计的类别先写好测试了,而在您写好测试的同时,您也确认了类别所要进行的动作行为,即使您一开始对该类别的职责还不明确,藉由先写下测试,您对该类别的要求也可以确定下来,这是先写测试的好处,在测试写好的同时,相当于也写下了对该类别的职责说明书。
下面我们将完成的测试案例程序写出如下:
代码: |
import junit.framework.TestCase; import java.io.*;
public class SimpleObjectIOTest extends TestCase { private SimpleObjectIO objIO; private String[] writableObjs;
public SimpleObjectIOTest(String arg0) { super(arg0); }
protected void setUp() throws Exception { super.setUp(); objIO = new SimpleObjectIO(); writableObjs = new String[10]; for(int i = 0; i < writableObjs.length; i++) { writableObjs[i] = new String("TestString" + i); } }
protected void tearDown() throws Exception { super.tearDown(); objIO = null; writableObjs = null; }
public void testSimpleDataObjectIO() throws Exception { objIO.writeObjectsToFile(writableObjs, "test.dat"); Object[] objs = objIO.readObjectsFromFile("test.dat"); for(int i = 0; i < writableObjs.length; i++) assertEquals(writableObjs[i], objs[i].toString()); }
public void testSimpleDataObjectAppend() throws Exception { objIO.writeObjectsToFile(writableObjs, "test.dat"); String[] append = new String[1]; append[0] = new String("TestString10"); objIO.appendObjectsToFile(append, "test.dat");
Object[] objs = objIO.readObjectsFromFile("test.dat"); for(int i = 0; i < writableObjs.length; i++) assertEquals(writableObjs[i].toString(), objs[i].toString()); assertEquals(append[0], objs[writableObjs.length]); }
public void testReadFormNonExistedFile() { try { objIO.readObjectsFromFile("nothisfile.dat"); fail("FileNotFoundException not thrown"); } catch(FileNotFoundException e) { // exception catched, do nothing } }
public void testAppendToNonExistedFile() { try { objIO.appendObjectsToFile(writableObjs, "nothisfile.dat"); fail("FileNotFoundException not thrown"); } catch(FileNotFoundException e) { // exception catched, do nothing } }
public static void main(String[] args) { junit.textui.TestRunner.run(SimpleObjectIOTest.class); } } |
11、先写测试(二)
在撰写程序之前,先为它写下测试,您的第一个疑问是,这个测试根本就不能通过编译,写下它又如何?
可以的!它是可以通过编译的,想想编译器要的是什么?!您就给它什么!看看我们之前写的测试程序,其中有关于SimpleObjectIO类别的有以下几个:
代码: |
SimpleObjectIO objIO = new SimpleObjectIO(); Object[] objs = objIO.readObjectsFromFile("test.dat"); objIO.writeObjectsToFile(writableObjs, "test.dat"); objIO.appendObjectsToFile(append, "test.dat"); |
也就是一个没有参数的建构函式,三个可呼叫的方法,其中我们希望在读取或附加资料时,若指定的档案不存在,必须丢出FileNotFoundException,所以我们可以先写下SimpleObjectIO来通过编译:
代码: |
import java.io.*; public class SimpleObjectIO { public SimpleObjectIO() {}
public Object[] readObjectsFromFile(String filename) throws FileNotFoundException { if(false) throw new FileNotFoundException(); return null; }
public void writeObjectsToFile(Object[] objs, String filename) { }
public void appendObjectsToFile(Object[] objs, String filename) throws FileNotFoundException { if(false) throw new FileNotFoundException(); } } |
OK!现在编译刚刚的SimpleObjectIOTest,编译可以通过了,您会说:「这太扯了,这算什么呢?只为了通过编译写下这些无用的程序码。」您的抱怨并没有错,当然!运行它的话会得一堆测试失败报告:
代码: |
...... at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at SimpleObjectIOTest.main(SimpleObjectIOTest.java:67)
FAILURES!!! Tests run: 4, Failures: 2, Errors: 2 |
您说:「看吧!这根本是个无用的程序!」其实,您的任务就是将SimpleObjectIO类别的内容完成,以通过这些测试,当所有的测试都通过之后,您的类别基本上就算完成了,而在为了通过测试的过程中,您会发现一些您之前所没有注意到的错误,并且可以加以改正。
OK!我们该从为了通过哪一个测试而开始实现我们的类别?我选择从testReadFormNonExistedFile开始,在指定的档案不存在时,可以丢出FileNotFoundException:
代码: |
public Object[] readObjectsFromFile(String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException(); return null; } |
记得!通过测试是您的目标,您现在不用急于完整的实现readObjectsFromFile()的完整功能,光是上面的程序就可以让您通过testReadFromNonExistedFile测试了,运行看看,这次failure少了一个:
代码: |
........ Tests run: 4, Failures: 1, Errors: 2 |
同样的,我们选择通过testAppendToNonExistedFile测试为目标:
代码: |
public void appendObjectsToFile(Object[] objs, String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException(); } |
再一次运行,测试的failure又少了一个:
代码: |
........ Tests run: 4, Failures: 0, Errors: 2 |
接下来我们选择通过testSimpleDataObjectIO这个测试,为了完成这个测试,我们必须要完成SimpleObjectIO的writeObjectsToFile与readObjectsFromFile两个方法:
代码: |
public Object[] readObjectsFromFile(String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException();
FileInputStream inputstream = null; ObjectInputStream objstream = null; java.util.ArrayList array = null;
try { objstream = new ObjectInputStream( inputstream = new FileInputStream(file)); array = new java.util.ArrayList(); while(inputstream.available() > 0) array.add(objstream.readObject()); objstream.close(); } catch(ClassNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); }
return array.toArray(); } public void writeObjectsToFile(Object[] objs, String filename) { File file = new File(filename); ObjectOutputStream objstream = null;
try { objstream = new ObjectOutputStream(new FileOutputStream(file)); for(int i = 0; i < objs.length; i++) objstream.writeObject(objs[i]); objstream.close(); } catch(IOException e) { e.printStackTrace(); } } |
OK!再来测试看看:
代码: |
..E.. Time: 0.161 There was 1 error: 1) testSimpleDataObjectAppend(SimpleObjectIOTest)java.lang.ArrayIndexOutOfBounds Exception: 10 at SimpleObjectIOTest.testSimpleDataObjectAppend(SimpleObjectIOTest.java :43) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl. java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces sorImpl.java:25) at SimpleObjectIOTest.main(SimpleObjectIOTest.java:67)
FAILURES!!! Tests run: 4, Failures: 0, Errors: 1 |
这次Errors少了一个,而且从讯息中我们可以看到,这个Errors来自于testSimpleDataObjectAppend,接下来我们再为了通过这个测试而努力:
代码: |
public void appendObjectsToFile(Object[] objs, String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException();
ObjectOutputStream objstream = null;
try { objstream = new ObjectOutputStream(new FileOutputStream(file, true)); for(int i = 0; i < objs.length; i++) objstream.writeObject(objs[i]); objstream.close(); } catch(IOException e) { e.printStackTrace(); } } |
OK!再来测试一下:
代码: |
..java.io.StreamCorruptedException at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1301) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:324) at SimpleObjectIO.readObjectsFromFile(SimpleObjectIO.java:20) at SimpleObjectIOTest.testSimpleDataObjectAppend(SimpleObjectIOTest.java:40) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:324) at junit.framework.TestCase.runTest(TestCase.java:154) at junit.framework.TestCase.runBare(TestCase.java:127) at junit.framework.TestResult$1.protect(TestResult.java:106) at junit.framework.TestResult.runProtected(TestResult.java:124) at junit.framework.TestResult.run(TestResult.java:109) at junit.framework.TestCase.run(TestCase.java:118) at junit.framework.TestSuite.runTest(TestSuite.java:208) at junit.framework.TestSuite.run(TestSuite.java:203) at junit.textui.TestRunner.doRun(TestRunner.java:116) at junit.textui.TestRunner.doRun(TestRunner.java:109) at junit.textui.TestRunner.run(TestRunner.java:72) at junit.textui.TestRunner.run(TestRunner.java:57) at SimpleObjectIOTest.main(SimpleObjectIOTest.java:67) E.. Time: 0.83 There was 1 error: 1) testSimpleDataObjectAppend(SimpleObjectIOTest)java.lang.ArrayIndexOutOfBound***ception: 10 at SimpleObjectIOTest.testSimpleDataObjectAppend(SimpleObjectIOTest.java:43) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at SimpleObjectIOTest.main(SimpleObjectIOTest.java:67)
FAILURES!!! Tests run: 4, Failures: 0, Errors: 1 |
喔!这次可真要命!不仅没有通过测试,这次的Error也与上一个测试不一样,且由SimpleObjectIO所自行捕捉到的例外讯息中,我们看到是一个StreamCorruptedException例外,这个问题我之前可没有遇过,查询API资料,发现到这个例外是发生在试图将物件附加至一个先前已写入物件的档案时,由于ObjectOutputStream在写入资料时,还会加上一个特别的标示头,而读取档案时会检查这个标示头,如果一个档案中被多次附加物件,那么该档案中会有多个标示头,如此读取检查时就会发现不一致,因此丢出StreamCorrupedException。
这纯綷是个小插曲,为了解决这个问题,我们可以用一个子类别继承ObjectOutputStream,并重新定义writeStreamHeader(),如果是以附加的方式来写入物件,就不写入标示头,运用匿名类别是个好的方法,这样一来,使用SimpleObjectIO类别的对象就不用知道这个子类的存在:
代码: |
public void appendObjectsToFile(Object[] objs, String filename) throws FileNotFoundException { File file = new File(filename); if(!file.exists()) throw new FileNotFoundException();
ObjectOutputStream objstream = null;
try { objstream = new ObjectOutputStream(new FileOutputStream(file, true)) { protected void writeStreamHeader() throws IOException {} };
for(int i = 0; i < objs.length; i++) objstream.writeObject(objs[i]); objstream.close(); } catch(IOException e) { e.printStackTrace(); } } |
OK!这次再来测试看看吧!
代码: |
.... Time: 0.148
OK (4 tests) |
这次所有的测试真的就顺利通过了,而您的设计目标也已经完成,当然,这是一个很简单的例子,我们对SimpleObjectIO的功能并不是作的很齐全,您可以为它加上更多的功能,在加上功能之前,您应该先将相关的测试写好,这可以帮您确定您所要加上的功能是什么,然后您的目标就是通过这些新加上的测试。
事实上,透过先写测试,可以迫使您的类别必须成为可测试的,这一点很重要,一但遵守这个规定,您会发现,由于您的类别必须成为可测试的,您的类别将会被设计为与其它物件耦合度低的类别,这是先写测试所意外带来的好处。
12、组合测试
我们可以在继承TestCase之后,撰写一个静态suite()方法组合单元测试,也就是使用addTest()方法,例如:
代码: |
public static Test suite() { TestSuite suite= new TestSuite(); suite.addTest(new TestIt("testGetMethod")); suite.addTest(new TestIt("testSetMethod")); suite.addTestSuite(AnotherTestSuiteImplementation.class); return suite; } |
TestCase与TestSuite都继承了Test界面,执行测试时会调用它们的run()方法,这是组合(composite)模式的应用,这意味着我们可以组合TestCase或TestSuite为一个更大的测试套件。
如果您想要将之前所曾经撰写过的TestCase集合一起来一起进行测试,您可以撰写一个TestAll之类的类别,基本上它只简单的含有一个静态方法suite(),它传回一个实作Test的物件,也就是TestSuite,例如:
代码: |
import junit.framework.Test; import junit.framework.TestSuite;
public class TestAll { public static Test suite() { TestSuite suite = new TestSuite("TestAll class"); suite.addTestSuite(TestCase1.class); suite.addTestSuite(TestCase2.class); suite.addTestSuite(TestCase3.suite()); } } |
这个例子中示范了两种方式,一种是直接使用Reflection机制识别testXXX()方法出来,一种是直接使用TestCase中定义的静态suite()方法,接下来您就可以使用TestRunner来执行所有的测试:
代码: |
junit.swingui.TestRunner.run(TestAll.suite()); |
13
、什么是Ant?Ant的全名是"Another Neat Tool",是由James Duncan Davidson在Make工具无法满足他的需求下所撰写出来的构建(build)工具,目前由Apache Software Foundation持续进行开发,根据官方网站上的FAQ中"What is Apache Ant"的回答:
代码: |
Ant是以Java为基础的构建工具,理论上,它有些类似Make,但没有Make的缺点,并具有纯Java撰写的可携性优点。
Ant is a Java-based build tool. In theory, it is kind of like Make, without Make's wrinkles and with the full portability of pure Java code. |
对于没有使用过Make工具的初学者来说,想象一下您如何管理您的原始码?自动处理ClassPath的问题?在编译之后将编译过后的档案指定至某个目录?包装您的类库?甚至执行自动测试并将报告储存下来?这一切都可以透过Ant来完成!您不用不断的使用javac、copy、cd、java指令来达成这些目的,只要撰写好构建文件(buildfile),一个以XML组织的文件档案,之后,最简单的情况下,您只要下达ant指令,所有的一切就可以完成。
或许有人会说这些东西有些IDE也可以办到,这并不是正确的说法,Ant并不取代IDE,它补强了IDE,而也没有IDE可以取代Ant,他们是互补的,不是相互取代的。
简单的归纳一下Ant可以帮您自动完成的任务:
*编译Java原始码
*建立jar、war、zip档案
*自动测试与生成报告
*从CVS等管理系统取得原始码
您可以先行至以下的网站取得一些信息:
Ant的官方网站:
Ant使用者手册:
13、设定
至Ant的官方网站 下载Ant,写本文时的版本是1.6.1,将压缩档解压缩至您想要的目录,假设是c:\develop\apache-ant-1.6.1
建议您新增系统变量:JAVA_HOME,内容为您的Java安装路径,例如:c:\develop\j2sdk1.4.2
新增系统变量:ANT_HOME,内容:c:\develop\apache-ant-1.6.1
在PATH环境变量中加入Ant的bin目录:%ANT_HOME%\bin
如果您要让Ant能支援JUnit,建议您直接将JUnit的junit.jar放置在Ant的lib目录,并记得改变CLASSPATH中原先有关于JUnit的设定,例如:%ANT_HOME\lib\junit.jar,虽然也有其它的方式可以设定,但这是最快最简单的方法。
如果是Windows 2000/XP,请在[系统内容/进阶/设定环境变量]中设定[系统变量],以完成以上的设定。
14、了解buildfile
Ant透过一个XML文件来进行组态,我们通常称这个档案为buildfile,预设命名为build.xml,在Ant的buildfile中可以定义构建项目时的「属性」(property)、「任务」(task),一个build.xml中可以定义多个任务,这些任务可能是建立目录、编译Java原始码、搬移档案、产生doc文件、进行测试、产生测试报告等等,这些任务通常组织为一个「目标」(target)。
我们以一个简单的HelloWorld.java程序来示范如何建立buildfile,并大致了解「属性」(property)、「任务」(task)与「目标」(target)的作用,虽然这个程序使用Ant来构建显示过于夸张,但可以作为一个快速了解Ant的例子,首先我们在src目录中编译HelloWorld.java档案:
代码: |
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!!"); } } |
先描述一下即将进行的工作,我们要编译src中的HelloWorld.java,其编译成果将储存至build目录中,如果build目录不存在就建立它,每一次编译将build中前一次编译的成果复制至bak目录,如果bak目录不存在就建立它,我们在src外编辑build.xml如下:
项目中包括数个目标,每一个目标有一个名称,项目的进入点我们预设是"compile"目标,而"compile"目标的完成依赖于"prepare"目标的完成,所以在构建开始时,会先执行"prepare"目标,完成后再执行"compile"目标。
标签可以让您设定一些在构建项目时的常用属性值,每一个属性值会有一个名称对应,以这个例子而言,我们设定了程序码来源位置、编辑目标与备份目录。
一个构建中可以包括数个目标,在这个例子中主要分作两个目标:编辑前的准备工作与进行编辑。在编译之前我们先建立所需要的目录,如果目录已经存在就自动跳过该项工作,然后我们复制build中的档案至bak目录。接下来进行编译,我们指定编译的源文件来源与目标。
编辑好build.xml之后,在文字模式下直接下ant指令即可开始构建项目,ant预设会读取同一个目录下的build.xml,第一次执行ant时会出现以下的文字提示建构过程:
代码: |
Buildfile: build.xml
prepare: [mkdir] Created dir: D:\temp\build [mkdir] Created dir: D:\temp\bak
compile: [javac] Compiling 1 source file to D:\temp\build
BUILD SUCCESSFUL Total time: 11 seconds |
您可以看看ant是不是已经完成了您所指定的工作,现在假设您修改了HelloWorld.java并存档,接下来再次下达ant指令,这次出现以下的文字提示建构过程:
代码: |
Buildfile: build.xml
prepare: [copy] Copying 1 file to D:\temp\bak
compile: [javac] Compiling 1 source file to D:\temp\build
BUILD SUCCESSFUL Total time: 7 seconds |
这次由于build与bak目录已经存在,就不用再进行新建目录的工作,ant检查build中有之前构建的档案,于是将它复制至bak目录中。
16、常用任务标签
构建一个基本的程序有几个常用的任务,象是设定通用属性、清除前一次构建的档案或目录、复制档案、建立目标目录、编译程序、打包程序等等,以下介绍如何在build.xml中编写对应的任务目标。
当您在编写build.xml时发现到有一些属性设定出现过两次以上,例如目录的指定,您可以将这些属性使用加以设定,这样以后若要改变属性设定,就只要改变对应的即可,例如:
在每一次构建程序前,您会想要删除前一次的构建结果,您可以使用来指定删除目录或档案,例如:
在每一次构建程序前,您会想要复制前一次的构建结果,您可以使用来指定复制档案,例如:
在构建程序时,您可以使用建立一些必要的目录,例如:
编译程序的任务当然是最常用的,您还可以在编译程序时,加入一些CLASSPATH的指定,这是个相当方便的功能 ,例如:
您也可以使用来为您将编译完成的档案打包为jar档案,并可以指定manifest档案,例如:
为了完成jar打包的任务,您必须在META-INF目录下提供一个manifest档案,例如:
代码: |
Manifest-Version: 1.0 Created-By: Justin Main-Class: JNameIt Class-Path: JNameIt.jar |
17、构建实例
这边举一个较实际的例子来示范Ant建构程序的过程,这边使用的例子是站上的JNameIt程序:
我们将程序原始码放在src目录下,将所要用到的图档放在images目录下,并在META-INF目录下撰写MANIFEST.MF档案,内容如下:
代码: |
Manifest-Version: 1.0 Created-By: Justin Main-Class: JNameIt Class-Path: JNameIt.jar |
我们将要编译src中的原始码,class档存放的目的地为classes目录,最后并打包一个可执行的jar档案,而每一次重新建构时,删除前一次编译的成果与jar档案,我们的build.xml可以如下撰写:
接下来在文字模式下达ant指令即可开始建构程序:
代码: |
D:\temp>ant Buildfile: build.xml
setProperties:
clean:
prepareDir: [mkdir] Created dir: D:\temp\classes [mkdir] Created dir: D:\temp\lib [copy] Copying 1 file to D:\temp\classes\images
compile: [javac] Compiling 7 source files to D:\temp\classes
package: [jar] Building jar: D:\temp\lib\JNameIt.jar
all:
BUILD SUCCESSFUL Total time: 8 seconds |
接下来假设我们要重新建构程序,其运行后的提示如下:
代码: |
D:\temp>ant Buildfile: build.xml
setProperties:
clean: [delete] Deleting directory D:\temp\classes [delete] Deleting: D:\temp\lib\JNameIt.jar
prepareDir: [mkdir] Created dir: D:\temp\classes [copy] Copying 1 file to D:\temp\classes\images
compile: [javac] Compiling 7 source files to D:\temp\classes
package: [jar] Building jar: D:\temp\lib\JNameIt.jar
all:
BUILD SUCCESSFUL Total time: 35 seconds |
18、路径(Path)参考
使用Ant可以轻易的解决您设定classpath的问题,您可以使用来设定路径参考,使用来指定目录或jar档案,例如:
上面的例子也可以使用分号设定一系列位置,设定:
您也可以使用来指定某个目录下的档案,例如:
之后在进行任务时,您可以如下参考之前设定的路径:
您也可以直接在进行任务时指定classpath,例如:
19、条件式目标
在构建程序时,有些目标所定义的任务可能是可选的,您可以藉由在设定时设定一个条件,并在建构时指定该条件为true或false,以决定该目标是否要执行,例如您可以这么设定:
使用if设定条件目标时,表示只有在conditional被设置时才会被执行,在执行ant指令时,您就可以如下来决定sometarget是否要执行:
代码: |
ant -buildfile build.xml -Dconditional=true |
您也可以使用unless来设定条件目标:
由您使用了unless来设定条件目标,所以只有在conditional没有被设定时,目标才会被执行,例如:由于ant可以在一个buildfile中调用另一个buildfile,在调用的时候,两个buildfile之间可能会有一些重复的属性设定,如果您想要避免某个属性被重复设定了,您可以这么撰写:
在调用这个buildfile时,我们可以使用任务来指定条件的设定,这将在下一篇文章中统一说明。
20、条用其他buildfile
您可以在一个buildfile中调用另一个buildfile,一个简单的例子如下:
在中设定value为true,当您在另一个buildfile中有目标使用了条件式(if或unless)时,这可以给予该条件一个true的特性值。
您也可以指定项目的目录来调用预设的build.xml,例如:
您也可以仅仅调用另一个build中的某个目标,例如:
21、自动化构建与测试-Ant结合Junit
Ant可以进行自动化建构,而JUnit可以进行自动化测试,Ant可以与JUnit结合,使得自动化的建构与测变得可行。
我们使用之前的测试案例来示范如何将Ant结合JUnit以进行自动化建构与测试,之前的测试案例是:
Ant使用任务来设定JUnit测试,下面直接示范一个简单的例子:
printsummary属性会将测试的结果简单的显示出来,的name属性是设定要进行测试的案例类别,Ant建构与调用JUnit进行测试的讯息如下:
代码: |
Buildfile: build.xml
setProperties:
prepareDir: [mkdir] Created dir: D:\temp\classes
compile: [javac] Compiling 3 source files to D:\temp\classes
test: [junit] Running ObjectIOManagerTest [junit] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 0.14 sec
BUILD SUCCESSFUL Total time: 7 seconds |
您也可以将JUnit的测试过程在Ant建构的过程讯息中显示出来,只要加入标签设定即可:
Ant建构与调用JUnit进行测试的讯息如下:
代码: |
Buildfile: build.xml
setProperties:
prepareDir: [mkdir] Created dir: D:\temp\classes
compile: [javac] Compiling 3 source files to D:\temp\classes
test: [junit] Running ObjectIOManagerTest [junit] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 0.18 sec
[junit] Testsuite: ObjectIOManagerTest [junit] Tests run: 2, Failures: 0, Errors: 0, Time elapsed: 0.18 sec [junit] ------------- Standard Output --------------- [junit] Writing data to test.dat ... done [junit] Reading data from test.dat ... done [junit] Writing data to test.dat ... done [junit] Reading data from test.dat ... done [junit] ------------- ---------------- ---------------
[junit] Testcase: testSimpleObjectIO took 0.09 sec [junit] Testcase: testStudentObjectIO took 0.08 sec
BUILD SUCCESSFUL Total time: 8 seconds |
标签还可以设定将测试的结果,以XML文件储存下来,一个撰写的例子如下,它将测试的结果储存至report目录中,文件名称为TEST-*.xml,*是您的测试案例类别名称:
您也可以将测试结果所产生的XML文件转换为HTML文件,使用Ant可以直接帮您完成这个工作,标签使用XSLT将XML文件转换为HTML文件,一个撰写的例子如下所示:
设定搜寻TEST-*.xml文件,将之转换为HTML文件,而最后的结果我们设定储存至report/html/目录下,format属性中我们设定HTML文件具有框架,如果不设定这个属性则HTML报告文件就不具有框架。
*********************************************************************************************
*********************************************************************************************