B.1 單元測試(Unit Test)
一個(gè)單元(Unit)是指一個(gè)可獨(dú)立進(jìn)行的工作,獨(dú)立進(jìn)行指的是這個(gè)工作不受前一次或接下來的工作的結(jié)果影響。簡單地說,就是不與程序運(yùn)行時(shí)的上下文(Context)發(fā)生關(guān)系。
如果是在Java程序中,具體來說一個(gè)單元可以是指一個(gè)方法(Method)。這個(gè)方法不依賴于前一次運(yùn)行的結(jié)果,也不牽涉到后一次的運(yùn)行結(jié)果。
舉例來說,下面這個(gè)程序的gcd()方法可視為一個(gè)單元:
package onlyfun.caterpillar;
public class MathTool {
public static int gcd(int num1, int num2) {
int r = 0;
while(num2 != 0) {
r = num1 % num2;
num1 = num2;
num2 = r;
}
return num1;
}
下面的gcd()方法不可視為一個(gè)單元,要完成gcd的計(jì)算,必須調(diào)用setNum1()、setNum2()與gcd() 3個(gè)方法。
package onlyfun.caterpillar;
public class MathFoo {
private static int num1;
private static int num2;
public static void setNum1(int n) {
num1 = n;
}
public static void setNum2(int n) {
num2 = n;
}
public static int gcd() {
int r = 0;
while(num2 != 0) {
r = num1 % num2;
num1 = num2;
num2 = r;
}
return num1;
}
然而要完全使用一個(gè)方法來完成一個(gè)單元操作在實(shí)現(xiàn)上是有困難的,所以單元也可廣義解釋為數(shù)個(gè)方法的集合。這數(shù)個(gè)方法組合為一個(gè)單元操作,目的是完成一個(gè)任務(wù)。
不過設(shè)計(jì)時(shí)仍優(yōu)先考慮將一個(gè)公開的方法設(shè)計(jì)為單元,輔助的方法則使用設(shè)定為私用,盡量不用數(shù)個(gè)公開的方法來完成一件工作,以保持接口簡潔與單元邊界清晰。將工作以一個(gè)單元進(jìn)行設(shè)計(jì),這使得單元可以重用,并且也使得單元可以進(jìn)行測試,進(jìn)而增加類的可重用性。
單元測試指的是對每一個(gè)工作單元進(jìn)行測試,了解其運(yùn)行結(jié)果是否符合我們的要求。例如當(dāng)編寫完MathTool類之后,也許會這么寫一個(gè)小小的測試程序:
package test.onlyfun.caterpillar;
import onlyfun.caterpillar.MathTool;
public class MathToolTest {
public static void main(String[] args) {
if(MathTool.gcd(10, 5) == 5) {
System.out.println("GCD Test OK!");
}
else {
System.out.println("GCD Test Fail!");
}
}
在文字模式下使用文字信息顯示測試結(jié)果,這個(gè)動作是開發(fā)人員經(jīng)常作的事情,然而您必須一行一行看著測試程序的輸出結(jié)果,以了解測試是否成功。另一方面,測試程序本身也是一個(gè)程序,在更復(fù)雜的測試中,也許會遇到測試程序本身出錯(cuò),而導(dǎo)致無法驗(yàn)證結(jié)果的情況。
JUnit是一個(gè)測試框架,通過它所提供的工具,可以減少編寫錯(cuò)誤的測試程序的機(jī)會。另一方面,可以有更好的方法來檢驗(yàn)測試結(jié)果,而不是看著一長串輸出的文字來檢驗(yàn)測試是否成功。JUnit測試框架讓測試的進(jìn)行更有效率且更具可靠性。
B.2 JUnit設(shè)置
JUnit最初是由Erich Gamma與Kent Beck編寫,為單元測試的支持框架,用來編寫與執(zhí)行重復(fù)性的測試。它包括以下特性:
Ü 對預(yù)期結(jié)果作判斷
Ü 提供測試裝備的生成與銷毀
Ü 易于組織與執(zhí)行測試
Ü 圖形與文字接口的測試器
要設(shè)定JUnit,可先到 JUnit官方網(wǎng)站(http://junit.org/)下載JUnit的zip文件,下載后解開壓縮文件,其中會含有junit.jar文件,將這個(gè)文件復(fù)制到所要的數(shù)據(jù)夾中,然后設(shè)定Classpath指向junit.jar。例如:
set classpath=%classpath%;YOUR_JUNIT_DIR\junit.jar
如果是Windows 2000/XP,可以在環(huán)境變量中設(shè)定Classpath變量(可參考附錄A中的Classpath設(shè)置介紹)。
B.3 第一個(gè)JUnit測試
要對程序進(jìn)行測試,首先要設(shè)計(jì)測試案例(Test Case)。一個(gè)測試案例是對程序給予假定條件,然后運(yùn)行程序并看看在給定的條件下,程序的運(yùn)行結(jié)果是否符合要求。
在JUnit下,可以繼承TestCase來編寫測試案例,并定義測試方法,每一個(gè)測試方法是以testXXX()來命名。一個(gè)例子如下所示:
package test.onlyfun.caterpillar;
import onlyfun.caterpillar.MathTool;
import junit.framework.TestCase;
public class MathToolUnitTest extends TestCase {
public void testGcd() {
assertEquals(5, MathTool.gcd(10, 5));
}
public static void main(String[] args) {
junit.textui.TestRunner.run(MathToolUnitTest.class);
}
assertEquals()方法用來斷定您的預(yù)期值與單元方法實(shí)際的返回結(jié)果是否相同,如果預(yù)期值與返回的結(jié)果不同則丟出異常,TestRunner會捕捉異常,并提取其中的相關(guān)信息以報(bào)告測試結(jié)果。這里使用的是文字模式的TestRunner。
接下來根據(jù)測試案例編寫實(shí)際的程序,首先試著讓測試案例能通過編譯:
package onlyfun.caterpillar;
public class MathTool {
public static int gcd(int num1, int num2) {
return 0;
}
}
編譯完MathTool.java并用javac來編譯它。在編譯完成之后,接著運(yùn)行測試案例,會得到以下的結(jié)果:
.F
Time: 0
There was 1 failure:
1) testGcd(test.onlyfun.caterpillar.MathToolUnitTest)junit.framework.AssertionFa
iledError: expected:<5> but was:<0>
...略
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
由于MathTool中并沒有編寫什么實(shí)際的邏輯,所以測試失敗。在測試驅(qū)動中,測試案例所報(bào)告的結(jié)果通常是以測試失敗作為開始,您的挑戰(zhàn)就是要一步步消除這些失敗的信息。接下來根據(jù)測試案例,完成所設(shè)計(jì)的程序:
package onlyfun.caterpillar;
public class MathTool {
public static int gcd(int num1, int num2) {
int r = 0;
while(num2 != 0) {
r = num1 % num2;
num1 = num2;
num2 = r;
}
return num1;
}
再次運(yùn)行測試案例,會得到以下的結(jié)果,通過最后的OK信息,知道測試已經(jīng)成功:
.Time: 0
OK (1 test)
不一定要在main()中指定TestRunner,而可以直接啟動一個(gè)TestRunner,并指定測試案例類(繼承TestCase的類)。例如啟動一個(gè)Swing窗口的測試結(jié)果畫面:
java junit.swingui.TestRunner test.onlyfun.caterpillar.MathToolUnitTest
執(zhí)行的結(jié)果畫面如圖B-1所示。
在Swing窗口的測試結(jié)果顯示中,如果中間的橫棒是顯示綠色,表示所有的測試都已經(jīng)成功,如果中間的橫棒顯示紅色,表示測試失敗。JUnit的名言是Keep the bar green to keep the code clean,意思是保持綠色橫棒以保證測試成功。
也可以指定文字模式的測試結(jié)果。例如:
java junit.textui.TestRunner test.onlyfun.caterpillar.MathToolUnitTest

圖B-1 JUnit的Swing窗口測試結(jié)果
B.4 自動構(gòu)建與測試
Ant可以進(jìn)行自動化構(gòu)建,而JUnit可以進(jìn)行自動化測試,Ant可以與JUnit結(jié)合,使得自動化的構(gòu)建與測試變得可行。
如果要讓Ant能支持JUnit,建議直接將JUnit的junit.jar放置在Ant的lib目錄,并記得改變Classpath中原先有關(guān)junit.jar的設(shè)定。例如將Classpath重新指向%ANT_HOME%\lib\junit.jar(假設(shè)已經(jīng)如附錄A中設(shè)置了ant_home的環(huán)境變量)。雖然也有其他的方式可以設(shè)定,但這是最快也是最簡單的方法。
Ant使用<junit>標(biāo)簽來設(shè)定JUnit測試,下面是一個(gè)簡單的例子:
<?xml version="1.0"?>
<project name="autoBuildTest" default="test">
<target name="setProperties">
<property name="src.dir" value="src"/>
<property name="classes.dir" value="classes"/>
</target>
<target name="prepareDir" depends="setProperties">
<delete dir="${classes.dir}"/>
<mkdir dir="${classes.dir}"/>
</target>
<target name="compile" depends="prepareDir">
<javac srcdir="${src.dir}" destdir="${classes.dir}"/>
</target>
<target name="test" depends="compile">
<junit printsummary="yes">
<test
name="test.onlyfun.caterpillar.MathToolUnitTest"/>
<classpath>
<pathelement location="${classes.dir}"/>
</classpath>
</junit>
</target>
printsummary屬性會將測試的結(jié)果簡單地顯示出來,<test>的name屬性是設(shè)定所要進(jìn)行測試的測試案例類。Ant構(gòu)建與調(diào)用JUnit進(jìn)行測試的信息如下:
C:\workspace\B>ant
Buildfile: build.xml
setProperties:
prepareDir:
[mkdir] Created dir: C:\workspace\B\classes
compile:
[javac] Compiling 4 source files to C:\workspace\B\classes
test:
[junit] Running test.onlyfun.caterpillar.MathToolUnitTest
[junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0 sec
BUILD SUCCESSFUL
Total time: 1 second
B.5 自動生成測試報(bào)告
接上一個(gè)主題,可以將JUnit的測試過程在Ant構(gòu)建過程中顯示出來,只要加入< formatter>標(biāo)簽設(shè)定即可:
<?xml version="1.0"?>
<project name="autoBuildTest" default="test">
...
<target name="test" depends="compile">
<junit printsummary="yes">
<formatter type="plain" usefile="false"/>
<test
name="test.onlyfun.caterpillar.MathToolUnitTest"/>
<classpath>
<pathelement location="${classes.dir}"/>
</classpath>
</junit>
</target>
</project>
Ant構(gòu)建與調(diào)用JUnit進(jìn)行測試的信息如下:
C:\workspace\B>ant
Buildfile: build.xml
setProperties:
prepareDir:
[delete] Deleting directory C:\workspace\B\classes
[mkdir] Created dir: C:\workspace\B\classes
compile:
[javac] Compiling 4 source files to C:\workspace\B\classes
test:
[junit] Running test.onlyfun.caterpillar.MathToolUnitTest
[junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.016 sec
[junit] Testsuite: test.onlyfun.caterpillar.MathToolUnitTest
[junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.016 sec
[junit] Testcase: testGcd took 0.016 sec
BUILD SUCCESSFUL
Total time: 2 seconds
當(dāng)usefile屬性設(shè)定為true時(shí),會自動將產(chǎn)生的結(jié)果保存在文件中,默認(rèn)是TEST-*.txt。其中*是測試案例類名稱。就上例而言,所產(chǎn)生的報(bào)告文件內(nèi)容如下:
Testsuite: test.onlyfun.caterpillar.MathToolUnitTest
Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0 sec
Testcase: testGcd took 0 sec
<formatter>標(biāo)簽還可以設(shè)定將測試的結(jié)果,以XML文件保存下來。一個(gè)編寫的例子如下,它將測試的結(jié)果保存至report目錄中, 文件名稱為TEST-*.xml,*是測試案例類名稱:
<?xml version="1.0"?>
...
<target name="test" depends="compile">
<junit printsummary="yes">
<formatter type="xml"/>
<test
name="test.onlyfun.caterpillar.MathToolUnitTest"/>
<classpath>
<pathelement location="${classes.dir}"/>
</classpath>
</junit>
</target>
</project>
也可以將測試結(jié)果所產(chǎn)生的XML文件轉(zhuǎn)換為HTML文件,使用Ant可以直接完成這個(gè)工作。<junitreport>標(biāo)簽使用 XSLT將XML文件轉(zhuǎn)換為HTML文件。下面的例子將前面的說明作個(gè)總結(jié),以完整呈現(xiàn)編寫的實(shí)例:
<?xml version="1.0"?>
<project name="autoBuildTest" default="report">
<target name="setProperties">
<property name="src.dir" value="src"/>
<property name="classes.dir" value="classes"/>
<property name="report.dir" value="report"/>
</target>
<target name="prepareDir" depends="setProperties">
<delete dir="${report.dir}"/>
<delete dir="${classes.dir}"/>
<mkdir dir="${report.dir}"/>
<mkdir dir="${classes.dir}"/>
</target>
<target name="compile" depends="prepareDir">
<javac srcdir="${src.dir}" destdir="${classes.dir}"/>
</target>
<target name="test" depends="compile">
<junit printsummary="yes">
<formatter type="xml"/>
<test
name="test.onlyfun.caterpillar.MathToolUnitTest"/>
<classpath>
<pathelement location="${classes.dir}"/>
</classpath>
</junit>
</target>
<target name="report" depends="test">
<junitreport todir="${report.dir}">
<fileset dir="${report.dir}">
<include name="TEST-*.xml"/>
</fileset>
<report
format="frames" todir="${report.dir}/html"/>
</junitreport>
</target>
<include>設(shè)定搜尋TEST-*.xml文件,將之轉(zhuǎn)換為HTML文件,而最后的結(jié)果被設(shè)定保存至report/html/目錄下,在format屬性中設(shè)定了HTML文件具有邊框(Frame),如果不設(shè)定這個(gè)屬性,則HTML報(bào)告文件就不具有邊框。在運(yùn)行Ant之后所產(chǎn)生的 HTML測試結(jié)果報(bào)告文件如圖B-2所示。

圖B-2 Ant結(jié)合JUnit所自動產(chǎn)生的測試報(bào)告
附錄B只是對JUnit的一些簡介,如果需要更多有關(guān)JUnit的資料,可以參考以下的網(wǎng)址:
http://caterpillar.onlyfun.net/Gossip/JUnit/JUnitGossip.htm