本章導讀
上一章介紹的Ant,主要用來構建、包裝和發布Java程序,而Java程序的測試則由另外一個工具來完成,那就是JUnit。
本章首先對JUnit進行介紹,闡述為什么要使用JUnit,然后介紹JUnit的下載和安裝及使用方法,最后介紹JUnit的新特性。
8.1 JUnit介紹
JUnit是一個開源的Java單元測試框架,由 Erich Gamma 和 Kent Beck 開發完成。
8.1.1 JUnit簡介
JUnit主要用來幫助開發人員進行Java的單元測試,其設計非常小巧,但功能卻非常強大。
下面是JUnit一些特性的總結:
— 提供的API可以讓開發人員寫出測試結果明確的可重用單元測試用例。
— 提供了多種方式來顯示測試結果,而且可以擴展。
— 提供了單元測試批量運行的功能,而且可以和Ant很容易地整合。
— 對不同性質的被測對象,如Class,JSP,Servlet等,JUnit有不同的測試方法。
8.1.2 為什么要使用JUnit
以前,開發人員寫一個方法,如下代碼所示:
//******* AddAndSub.java**************
public Class AddAndSub {
public static int add(int m, int n) {
int num = m + n;
return num;
}
public static int sub(int m, int n) {
int num = m - n;
return num;
}
}
如果要對AddAndSub類的add和sub方法進行測試,通常要在main里編寫相應的測試方法,如下代碼所示:
//******* MathComputer.java**************
public Class AddAndSub {
public static int add(int m, int n) {
int num = m + n;
return num;
}
public static int sub(int m, int n) {
int num = m - n;
return num;
}
public static void main(String args[]) {
if (add (4, 6) == 10)) {
System.out.println(“Test Ok”);
} else {
System.out.println(“Test Fail”);
}
if (sub (6, 4) ==2)) {
System.out.println(“Test Ok”);
} else {
System.out.println(“Test Fail”);
}
}
}
從上面的測試可以看出,業務代碼和測試代碼放在一起,對于復雜的業務邏輯,一方面代碼量會非常龐大,另一方面測試代碼會顯得比較凌亂,而JUnit就能改變這樣的狀況,它提供了更好的方法來進行單元測試。使用JUnit來測試前面代碼的示例如下:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
public Class TestAddAndSub extends TestCase {
public void testadd() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(4, 6));
}
public void testsub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(6, 4));
}
public static void main(String args[]){
junit.textui.TestRunner.run(TestAddAndSub .class); }
}
這里先不對JUnit的使用方法進行講解,從上可以看到,測試代碼和業務代碼分離開,使得代碼比較清晰,如果將JUnit放在Eclipse中,測試起來將會更加方便。
8.2 建立JUnit的開發環境
為了不使讀者在環節配置上浪費太多時間,這里將一步一步地講解如何下載和配置JUnit。具體步驟如下:
8.2.1 下載JUnit
從www.junit.org可以進入到JUnit的首頁,JUnit的首頁畫面如圖8.1所示。
本書使用的版本是4.3版本,單擊“Junit4.3.zip”即可進入下載JUnit的畫面,如圖8.2所示。
下載Junit4.3.zip,下載后解壓縮即可。
8.2.2 配置JUnit
下載Junit4.3.zip完畢,并解壓縮到D盤根目錄下后,即可開始配置環境變量。用前面介紹的設定系統變量的方法,設定ClassPath,ClassPath=***;D:"junit"junit.jar,如圖8.3所示。
圖8.1 JUnit的首頁畫面
圖8.2 下載JUnit的畫面
查看是否配置好JUnit,在類里添加如下語句:
import junit.framework.TestCase;
圖8.3 設定系統變量ClassPath
如果編譯沒有錯誤,則說明配置成功。
8.3 JUnit的使用方法
JUnit的使用非常簡單,共有3步:第一步、編寫測試類,使其繼承TestCase;第二步、編寫測試方法,使用test+×××的方式來命名測試方法;第三步、編寫斷言。如果測試方法有公用的變量等需要初始化和銷毀,則可以使用setUp,tearDown方法。
8.3.1 繼承TestCase
如果要使用JUnit,則測試類都必須繼承TestCase。當然目前的最新版JUnit是不需要繼承它的,但并不是說TestCase類就沒有用了,它仍然是JUnit工作的基礎。這里先講述繼承TestCase類的方式,稍后再介紹不繼承的方式。
下面是前面使用JUnit進行測試AddAndSub類的代碼,這里進行詳細的分析:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
public Class TestAddAndSub extends TestCase {
public void testadd() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(4, 6));
}
public void testsub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(6, 4));
}
public static void main(String args[]){
junit.textui.TestRunner.run(TestAddAndSub .class); }
}
代碼說明:
— 這里繼承TestCase,表示該類是一個測試類。
— 然后使用junit.textui.TestRunner.run方法來執行這個測試類。
這里給出TestCase的源代碼:
//******* TestCase.java**************
package junit.framework;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public abstract class TestCase extends Assert implements Test {
/**測試案例的名稱*/
private String fName;
/**構造函數
*/
public TestCase() {
fName= null;
}
/**帶參數的構造函數
*/
public TestCase(String name) {
fName= name;
}
/**獲取被run執行的測試案例的數量
*/
public int countTestCases() {
return 1;
}
/**創建一個TestResult
* @see TestResult
*/
protected TestResult createResult() {
return new TestResult();
}
/**執行run方法,返回TestResult
* @see TestResult
*/
public TestResult run() {
TestResult result= createResult();
run(result);
return result;
}
/**執行run方法,參數為TestResult
*/
public void run(TestResult result) {
result.run(this);
}
/**執行測試方法,包括初始化和銷毀方法
* @throws Throwable if any exception is thrown
*/
public void runBare() throws Throwable {
Throwable exception= null;
setUp();
try {
runTest();
} catch (Throwable running) {
exception= running;
}
finally {
try {
tearDown();
} catch (Throwable tearingDown) {
if (exception == null) exception= tearingDown;
}
}
if (exception != null) throw exception;
}
/**執行測試方法
* @throws Throwable if any exception is thrown
*/
protected void runTest() throws Throwable {
assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);
Method runMethod= null;
try {
//利用反射機制
runMethod= getClass().getMethod(fName, (Class[])null);
} catch (NoSuchMethodException e) {
fail("Method """+fName+""" not found");
}
if (!Modifier.isPublic(runMethod.getModifiers())) {
fail("Method """+fName+""" should be public");
}
//利用反射機制
try {
runMethod.invoke(this);
}
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
/**測試前的初始化方法
*/
protected void setUp() throws Exception {
}
/**測試后的銷毀方法
*/
protected void tearDown() throws Exception {
}
/**返回測試案例的名稱
* @return the name of the TestCase
*/
public String getName() {
return fName;
}
/**設定測試案例的名稱
* @param name the name to set
*/
public void setName(String name) {
fName= name;
}
}
代碼說明:
— 該類繼承了Assert 類,實現了Test接口。
— 可以看出,TestCase類正是通過runBare實現了在測試方法前初始化相關變量和環境,在測試方法后銷毀相關變量和環境。
8.3.2 編寫測試方法
測試方法名要以test+方法名來命名,當然最新版的JUnit支持直接以方法名來命名測試方法。這是通過TestCase類里的runTest方法來實現的,主要利用了Java的反射機制,runTest方法的代碼如下:
protected void runTest() throws Throwable {
assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);
Method runMethod= null;
try {
// 獲取要測試的方法
runMethod= getClass().getMethod(fName, (Class[])null);
} catch (NoSuchMethodException e) {
fail("Method """+fName+""" not found");
}
//判斷要測試的方法是否為公用方法
if (!Modifier.isPublic(runMethod.getModifiers())) {
fail("Method """+fName+""" should be public");
}
//Java的反射機制
try {
runMethod.invoke(this);
}
//拋出調用異常
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
8.3.3 編寫斷言
JUnit主要有以下斷言:
— assertEquals(期望值,實際值),檢查兩個值是否相等。
— assertEquals(期望對象,實際對象),檢查兩個對象是否相等,利用對象的equals()方法進行判斷。
— assertSame(期望對象,實際對象),檢查具有相同內存地址的兩個對象是否相等,利用內存地址進行判斷,注意和上面assertEquals方法的區別。
— assertNotSame(期望對象,實際對象),檢查兩個對象是否不相等。
— assertNull(對象1,對象2),檢查一個對象是否為空。
— assertNotNull(對象1,對象2),檢查一個對象是否不為空。
— assertTrue(布爾條件),檢查布爾條件是否為真。
— assertFalse(布爾條件),檢查布爾條件是否為假。
這些斷言主要定義在JUnit的Assert類里,Assert類的示例代碼如下:
//******* Assert.java**************
package junit.framework;
/**一系列的斷言方法
*/
public class Assert {
/**構造函數
*/
protected Assert() {
}
/**斷言是否為真,帶消息
*/
static public void assertTrue(String message, boolean condition) {
if (!condition)
fail(message);
}
/**斷言是否為真
*/
static public void assertTrue(boolean condition) {
assertTrue(null, condition);
}
/**斷言是否為假,帶消息
*/
static public void assertFalse(String message, boolean condition) {
assertTrue(message, !condition);
}
/**斷言是否為假
*/
static public void assertFalse(boolean condition) {
assertFalse(null, condition);
}
/**斷言是否為失敗
*/
static public void fail(String message) {
throw new AssertionFailedError(message);
}
/**斷言是否為失敗
*/
static public void fail() {
fail(null);
}
/**是否相等的斷言,帶消息Object
*/
static public void assertEquals(String message, Object expected, Object actual) {
if (expected == null && actual == null)
return;
if (expected != null && expected.equals(actual))
return;
failNotEquals(message, expected, actual);
}
/**是否相等的斷言,Object
*/
static public void assertEquals(Object expected, Object actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息String
*/
static public void assertEquals(String message, String expected, String actual) {
if (expected == null && actual == null)
return;
if (expected != null && expected.equals(actual))
return;
throw new ComparisonFailure(message, expected, actual);
}
/**是否相等的斷言,String
*/
static public void assertEquals(String expected, String actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息double
*/
static public void assertEquals(String message, double expected, double actual, double delta) {
if (Double.compare(expected, actual) == 0)
return;
if (!(Math.abs(expected-actual) <= delta))
failNotEquals(message, new Double(expected), new Double(actual));
}
/**是否相等的斷言,double
*/
static public void assertEquals(double expected, double actual, double delta) {
assertEquals(null, expected, actual, delta);
}
/**是否相等的斷言,帶消息float
*/
static public void assertEquals(String message, float expected, float actual, float delta) {
if (Float.compare(expected, actual) == 0)
return;
if (!(Math.abs(expected - actual) <= delta))
failNotEquals(message, new Float(expected), new Float(actual));
}
/**是否相等的斷言, float
*/
static public void assertEquals(float expected, float actual, float delta) {
assertEquals(null, expected, actual, delta);
}
/**是否相等的斷言,帶消息long
*/
static public void assertEquals(String message, long expected, long actual) {
assertEquals(message, new Long(expected), new Long(actual));
}
/**是否相等的斷言, long
*/
static public void assertEquals(long expected, long actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息boolean
*/
static public void assertEquals(String message, boolean expected, boolean actual) {
assertEquals(message, Boolean.valueOf(expected), Boolean.valueOf(actual));
}
/**是否相等的斷言,boolean
*/
static public void assertEquals(boolean expected, boolean actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息byte
*/
static public void assertEquals(String message, byte expected, byte actual) {
assertEquals(message, new Byte(expected), new Byte(actual));
}
/**是否相等的斷言, byte
*/
static public void assertEquals(byte expected, byte actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息char
*/
static public void assertEquals(String message, char expected, char actual) {
assertEquals(message, new Character(expected), new Character(actual));
}
/**是否相等的斷言,char
*/
static public void assertEquals(char expected, char actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息short
*/
static public void assertEquals(String message, short expected, short actual) {
assertEquals(message, new Short(expected), new Short(actual));
}
/**是否相等的斷言,short
static public void assertEquals(short expected, short actual) {
assertEquals(null, expected, actual);
}
/**是否相等的斷言,帶消息int
*/
static public void assertEquals(String message, int expected, int actual) {
assertEquals(message, new Integer(expected), new Integer(actual));
}
/**是否相等的斷言,int
*/
static public void assertEquals(int expected, int actual) {
assertEquals(null, expected, actual);
}
/**是否不為null的斷言 Object
*/
static public void assertNotNull(Object object) {
assertNotNull(null, object);
}
/**是否不為null的斷言,帶消息Object
*/
static public void assertNotNull(String message, Object object) {
assertTrue(message, object != null);
}
/**是否為null的斷言Object
*/
static public void assertNull(Object object) {
assertNull(null, object);
}
/**是否為null的斷言,帶消息Object
*/
static public void assertNull(String message, Object object) {
assertTrue(message, object == null);
}
/**是否相同的斷言,帶消息*/
static public void assertSame(String message, Object expected, Object actual) {
if (expected == actual)
return;
failNotSame(message, expected, actual);
}
/**是否相同的斷言,Object
*/
static public void assertSame(Object expected, Object actual) {
assertSame(null, expected, actual);
}
/**是否不相同的斷言,帶消息
*/
static public void assertNotSame(String message, Object expected, Object actual) {
if (expected == actual)
failSame(message);
}
/**是否不相同的斷言Object
*/
static public void assertNotSame(Object expected, Object actual) {
assertNotSame(null, expected, actual);
}
/**相同時失敗
*/
static public void failSame(String message) {
String formatted= "";
if (message != null)
formatted= message+" ";
fail(formatted+"expected not same");
}
/**不相同時失敗
*/
static public void failNotSame(String message, Object expected, Object actual) {
String formatted= "";
if (message != null)
formatted= message+" ";
fail(formatted+"expected same:<"+expected+"> was not:<"+actual+">");
}
/**不相等時失敗
*/
static public void failNotEquals(String message, Object expected, Object actual) {
fail(format(message, expected, actual));
}
/**格式化消息
*/
public static String format(String message, Object expected, Object actual) {
String formatted= "";
if (message != null)
formatted= message+" ";
return formatted+"expected:<"+expected+"> but was:<"+actual+">";
}
}
從上述代碼中,讀者可以研讀JUnit中有關斷言的實現方式,其實,最終都是使用后面的幾個static方法來實現的。
8.4 JUnit的新特性
Java 5的發布為JUnit帶來了新的特性。自JUnit 4.0之后,JUnit大量使用了annotations特性,使編寫單元測試變得更加簡單。
8.4.1 改變測試方法的命名方式
前面講過,使用JUnit 4.0以上版本可以不用遵循以前JUnit約定的測試方法命名方法,以前命名方法的示例代碼如下:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
public class TestAddAndSub extends TestCase {
public void testadd() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(4, 6));
}
public void testsub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(6, 4));
}
public static void main(String args[]){
junit.textui.TestRunner.run(TestAddAndSub .class); }
}
JUnit 4.0以上版本的命名方式,是在測試方法前使用@Test注釋,示例代碼如下:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
import org.junit.*;
public class TestAddAndSub extends TestCase {
@Test public void add() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(4, 6));
}
@Test public void sub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(6, 4));
}
}
這個時候,測試方法的命名將不再重要,開發人員可以按照自己的命名方式來命名。
8.4.2 不再繼承TestCase
新版本的JUnit將不再強制繼承TestCase,但需要import org.junit.Assert來實現斷言,示例代碼如下:
//******* TestAddAndSub.java**************
import static org.junit.Assert.assertEquals;
import org.junit.*;
public class TestAddAndSub{
@Test public void add() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(4, 6));
}
@Test public void sub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(6, 4));
}
}
8.4.3 改變初始化和銷毀方式
以前,JUnit使用SetUp和TearDown方法來進行初始化和銷毀動作,JUnit 4.0以上版本將不再強制使用SetUp和TearDown方法來進行初始化和銷毀,原來使用SetUp和TearDown方法的示例代碼如下:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
public class TestAddAndSub extends TestCase {
private int m = 0;
private int n = 0;
//初始化
protected void setUp() {
m = 4;
n = 6;
}
public void testadd() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(m, n));
}
public void testsub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(n, m));
}
//銷毀
protected void tearDown() {
m = 0;
n = 0;
}
}
不使用SetUp和TearDown方法的示例代碼如下:
//******* TestAddAndSub.java**************
import static org.junit.Assert.assertEquals;
import org.junit.*;
public class TestAddAndSub {
protected int m = 0;
protected int n = 0;
//初始化
@Before public void init() {
m = 4;
n = 6;
}
@Test public void add() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(m, n));
}
@Test public void sub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(n, m));
}
//銷毀
@After public void destory() {
m = 0;
n = 0;
}
}
上面示例中的初始化和銷毀都是針對一個方法來說的,每個方法執行前都要進行初始化,執行完畢都要進行銷毀。而JUnit的最新版本則提供了新的特性,針對類進行初始化和銷毀。也就是說,該類中的方法只進行一次初始化和銷毀,方法就是使用@Before和@After,示例代碼如下:
//******* TestAddAndSub.java**************
import static org.junit.Assert.assertEquals;
import org.junit.*;
public class TestAddAndSub {
protected int m = 0;
protected int n = 0;
//初始化
@BeforeClass public void init() {
m = 4;
n = 6;
}
@Test public void add() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(m, n));
}
@Test public void sub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(n, m));
}
//銷毀
@AfterClass public void destory() {
m = 0;
n = 0;
}
}
上述初始化和銷毀動作,只執行一次即可。
8.4.4 改變異常處理的方式
以前,使用JUnit進行單元測試時,如果遇到異常情況,需要使用try…catch的形式來捕捉異常,示例代碼如下:
//******* TestAddAndSub.java**************
import junit.framework.TestCase;
public class TestAddAndSub extends TestCase {
private int m = 0;
private int n = 0;
//初始化
protected void setUp() {
m = 4;
n = 6;
}
public void testadd() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(m, n));
}
public void testsub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(n, m));
}
public void testdiv() {
//斷言除數為0
try {
int n = 2 / 0;
fail("Divided by zero!");
}
catch (ArithmeticException success) {
assertNotNull(success.getMessage());
}
}
//銷毀
protected void tearDown() {
m = 0;
n = 0;
}
}
JUnit4.0以后的版本將不再使用try…catch的方式來捕捉異常,示例代碼如下:
//******* TestAddAndSub.java**************
import static org.junit.Assert.assertEquals;
import org.junit.*;
public class TestAddAndSub {
protected int m = 0;
protected int n = 0;
//初始化
@Before public void init() {
m = 4;
n = 6;
}
@Test public void add() {
//斷言計算結果與10是否相等
assertEquals(10, AddAndSub.add(m, n));
}
@Test public void sub() {
//斷言計算結果與2是否相等
assertEquals(2, AddAndSub.sub(n, m));
}
@Test t(expected=ArithmeticException.class) public void div() {
//斷言除數是否為0
int n = 2 / 0;
}
//銷毀
@After public void destory() {
m = 0;
n = 0;
}
}
當然,JUnit還有許多新的特性,限于篇幅原因,這里只對比較重要的特性進行講解,其余將不再多講,想要了解的讀者可以到JUnit的相關網站進行學習。
8.5 小結
本章首先講述了JUnit的下載和安裝,接著又講解了JUnit的相關知識,最后講解了JUnit的新特性。
JUnit對開發人員進行大規模的單元測試來說,是非常有用的,但對于大量的代碼如何來管理就只有靠CVS了。CVS是一個版本控制系統,主要用來管理開發人員代碼的歷史,下一章主要講如何使用CVS。