隨著Refactoring技術和XP軟件工程技術的廣泛推廣單元測試的作用在軟件工程中變得越來越重要而一個簡明易學適用廣泛高效穩定的單元測試框架則對成功的實施單元測試有著至關重要的作用在java編程語句環境裡Junit Framework是一個已經被多數java程序員采用和實證的優秀的測試框架但是多數沒有嘗試Junit Framework的程序員在學習如何Junit Framework來編寫適應自己開發項目的單元測試時依然覺得有一定的難度這可能是因為Junit隨框架代碼和實用工具附帶的用戶指南和文檔的著重點在於解釋單元測試框架的設計方法以及簡單的類使用說明而對在特定的測試框架(Junit)下如何實施單元測試如何在項目開發的過程中更新和維護已經存在的單元測試代碼沒有詳細的解釋因此本文檔就兩個著重點對Junit所附帶的文檔進行進一步的補充和說明使Junit能被更多的開發團隊采用讓單元測試乃至RefactoringXP技術更好在更多的開發團隊中推廣
單元測試的編寫原則
Junit附帶文檔所列舉的單元測試帶有一定的迷惑性因為幾乎所有的示例單元都是針對某個對象的某個方法似乎Junit的單元測試僅適用於類組織結構的靜態約束從而使初學者懷疑Junit下的單元測試所能帶來的效果因此我們需要重新定義如何確定有價值的單元測試以及如何編寫這些單元測試維護這些單元測試從而讓更多的程序員接受和熟悉Junit下的單元測試的編寫
在Junit單元測試框架的設計時作者一共設定了三個總體目標第一個是簡化測試的編寫這種簡化包括測試框架的學習和實際測試單元的編寫第二個是使測試單元保持持久性第三個則是可以利用既有的測試來編寫相關的測試從這三個目標可以看出單元測試框架的基本設計考慮依然是從我們現有的測試方式和方法出發而只是使測試變得更加容易實施和擴展並保持持久性因此編寫單元測試的原則可以從我們通常使用的測試方法借鑒和利用
如何確定單元測試
在我們通常的測試中一個單元測試一般針對於特定對象的一個特定特性譬如假定我們編寫了一個針對特定數據庫訪問的連接池的類包實現我們會建立以下的單元測試
在連接池啟動後是否根據定義的規則在池中建立了相應數量的數據庫連接
申請一個數據庫連接是否根據定義的規則從池中直接獲得緩存連接的引用還是建立新的連接
釋放一個數據庫連接後連接是否根據定義的規則被池釋放或者緩存以便以後使用
後台Housekeeping線程是否按照定義的規則釋放已經過期的連接申請
如果連接有時間期限後台Housekeeping線程是否定期釋放已經過期的緩存連接
這兒只列出了部分的可能測試但是從這個列表我們可以看出單元測試的粒度一個單元測試基本是以一個對象的明確特性為基礎單元測試的過程應該限定在一個明確的線程范圍內根據上面所述一個單元測試的測試過程非常類似於一個Use Case的定義但是單元測試的粒度一般來說比Use Case的定義要小這點是容易理解的因為Use Case是以單獨的事務單元為基礎的而單元測試是以一組聚合性很強的對象的特定特征為基礎的一般而言一個事務中會利用許多的系統特征來完成具體的軟件需求
從上面的分析我們可以得出測試單元應該以一個對象的內部狀態的轉換為基本編寫單元一個軟件系統就和一輛設計好的汽車一樣系統的狀態是由同一時刻時系統內部的各個分立的部件的狀態決定的因此為了確定一個系統最終的行為符合我們起始的要求我們首先需要保證系統內的各個部分的狀態會符合我們的設計要求所以我們的測試單元的重點應該放在確定對象的狀態變換上
然而需要注意的並不是所有的對象組特征都需要被編寫成獨立的測試單元如何在對象組特征裡篩選有價值的測試單元的原則在JUnitTest Infected: Programmers Love Writing Tests一文中得到了正確的描述你應該在有可能引入錯誤的地方引入測試單元通常這些地方存在於有特定邊界條件復雜算法以及需求變動比較頻繁的代碼邏輯中除了這些特性需要被編寫成獨立的測試單元外還有一些邊界條件比較復雜的對象方法也應該被編寫成獨立的測試單元這部分單元測試已經在Junit文檔中被較好的描述和解釋過了
在基本確定了需要編寫的單元測試我們還應該問自己編寫好了這些測試我們是否可以有把握地告訴自己如果代碼通過了這些單元測試我們能認定程序的運行是正確的符合需求的如果我們不能非常的確定就應該看看是否還有遺漏的需要編寫的單元測試或者重新審視我們對軟件需求的理解通常來說在開始使用單元測試的時候更多的單元測試總是沒有錯的
一旦我們確定了需要被編寫的測試單元接下來就應該
如何編寫單元測試
在XP下強調單元測試必須由類包的編寫者負責編寫這個限定對於我們設定的測試目標是必須的因為只有這樣測試才能保證對象的運行時態行為符合需求而僅通過類接口的測試我們只能確保對象符合靜態約束因此這就要求我們在測試的過程中必須開放一定的內部數據結構或者針對特定的運行行為建立適當的數據記錄並把這些數據暴露給特定的測試單元這也就是說我們在編寫單元測試時必須對相應的類包進行修改這樣的修改也發生在我們以前使用的測試方法中因此以前的測試標記及其他一些測試技巧仍然可以在Junit測試中改進使用
由於單元測試的總體目標是負責我們的軟件在運行過程中的正確無誤因此在我們對一個對象編寫單元測試的時候我們不但需要保證類的靜態約束符合我們的設計意圖而且需要保證對象在特定的條件下的運行狀態符合我們的預先設定還是拿數據庫緩沖池的例子說明一個緩沖池暴露給其他對象的是一組使用接口其中包括對池的參數設定池的初始化池的銷毀從這個池裡獲得一個數據連接以及釋放連接到池中對其他對象而言隨著各種條件的觸發而引起池的內部狀態的變化是不需要知道的這一點也是符合封裝原理的但是池對象的狀態變化譬如緩存的連接數在某些條件下會增長一個連接在足夠長的運行後需要被徹底釋放從而使池的連接被更新等等雖然外部對象不需要明確但是卻是程序運行正確的保證所以我們的單元測試必須保證這些內部邏輯被正確的運行
編譯語言的測試和調試是很難對運行的邏輯過程進行跟蹤的但是我們知道無論邏輯怎麼運行如果狀態的轉換符合我們的行為設定那驗證結果顯然是正確的因此在對一個對象進行單元測試的時候我們需要對多數的狀態轉換進行分析和對照從而驗證對象的行為狀態是通過一系列的狀態數據來描述的因此編寫單元測試首先分析出狀態的變化過程(狀態轉換圖對這個過程的描述非常清晰)然後根據狀態的定義確定分析的狀態數據最後是提供這些內部的狀態數據的訪問在數據庫連接池的例子中我們對池實現的對象DefaultConnectionProxy的狀態變換進行分析後我們決定把表征狀態的OracleConnectionCacheImpl對象公開給測試類參見示例一
示例一
/**
* 這個類簡單的包裝了oracle對數據連接緩沖池的實現
*
*/
public class DefaultConnectionProxy extends ConnectionProxy {
private static final String name = Default Connection Proxy;
private static final String description = 這個類簡單的包裝了oracle對數據連接緩沖池的實現;
private static final String author = ;
private static final int major_version = ;
private static final int minor_version = ;
private static final boolean pooled = true;
private ConnectionBroker connectionBroker = null;
private Properties props;
private Properties propDescriptions; >
private Object initLock = new Object();
// Test Code Begin
/* 為了能夠了解對象的狀態變化因此需要把表征對象內部狀態變化的部分私有變量提供公共的訪問接口
(或者提供讓同一個類包的訪問接口)以便使測試單元可以有效地判斷對象的狀態轉變
在本示例中對包裝的OracleConnectionCacheImpl對象提供訪問接口
*/
OracleConnectionCacheImpl getConnectionCache() {
if (connectionBroker == null) {
throw new IllegalStateException(You need start the server first);
}
return connectionBrokergetConnectionCache();
}
<
// Test Code End
在公開內部狀態數據後我們就可以編寫我們的測試單元了單元測試的選擇方法和選擇尺度已經在本文前面章節進行了說明
但是仍然需要注意的是由於assert方法會拋出一個error你應該在測試方法的最後集中用assert相關方法進行判斷 這樣可以確保資源得到釋放
對數據庫連接池的例子我們可以建立測試類DefaultConnectionProxyTest同時建立數個test case如下
示例二
/**
* 這個類對示例一中的類進行簡單的測試
*
*/
public class DefaultConnectionProxyTest extends TestCase {
private DefaultConnectionProxy conProxy = null;
private OracleConnectionCacheImpl cacheImpl = null;
private Connection con = null;
/** 設置測試的fixture建立必要的測試起始環境
*/
protected void setUp() {
conProxy = new DefaultConnectionProxy();
conProxystart();
cacheImpl = conProxygetConnectionCache();
}
/** 對示例一中的對象進行服務啟動後的狀態測試檢查是否在服務啟動後
連接池的參數設置是否正確
*/
public void testConnectionProxyStart() {
int minConnections = ;
int maxConnections = ;
assertNotNull(cacheImpl);
try{
minConnections = IntegerparseInt(PropertyManagergetProperty
(DefaultConnectionProxyminConnections));
maxConnections = IntegerparseInt(PropertyManagergetProperty
(DefaultConnectionProxymaxConnections));
} catch (Exception e) {
// ignore the exception
}
assertEquals(cacheImplgetMinLimit() minConnections);
assertEquals(cacheImplgetMaxLimit() maxConnections);
assertEquals(cacheImplgetCacheSize() minConnections);
}
/** 對示例一中的對象進行獲取數據庫連接的測試看看是否可以獲取有效的數據庫連接
並且看看獲取連接後連接池的狀態是否按照既定的策略進行變化由於assert方法拋出的是
error對象因此盡可能把assert方法放置到方法的最後集體進行測試這樣在方法內打開的
資源才能有效的被正確關閉
*/
public void testGetConnection() {
int cacheSize = cacheImplgetCacheSize();
int activeSize = cacheImplgetActiveSize();
int cacheSizeAfter = ;
int activeSizeAfter = ;
con = conProxygetConnection();
>if (con != null) {
activeSizeAfter = cacheImplgetActiveSize();
cacheSizeAfter = cacheImplgetCacheSize();
try{
conclose();
} catch (SQLException e) {
}
} else {
assertNotNull(con);
}
/*如果連接池中的實際使用連接數小於緩存連接數檢查獲取的新的數據連接是否
從緩存中獲取反之連接池是否建立新的連接
*/
if (cacheSize > activeSize){
assertEquals(activeSize + activeSizeAfter);
assertEquals(cacheSize cacheSizeAfter);
} else {
assertEquals(activeSize + cacheSizeAfter);
}
}
/** 對示例一中的對象進行數據庫連接釋放的測試看看連接釋放後連接池的
狀態是否按照既定的策略進行變化由於assert方法拋出的是error對象因此盡可
能把assert方法放置到方法的最後集體進行測試這樣在方法內打開的
資源才能有效的被正確關閉
*/
public void testConnectionClose() {
int minConnections = cacheImplgetMinLimit();
int cacheSize = ;
int activeSize = ;
int cacheSizeAfter = ;
int activeSizeAfter = ;
con = conProxygetConnection();
if (con != null) {
cacheSize = cacheImplgetCacheSize();
activeSize = cacheImplgetActiveSize();
try{
conclose();
} catch (SQLException e) {
}
activeSizeAfter = cacheImplgetActiveSize();
cacheSizeAfter = cacheImplgetCacheSize();
} else {
assertNotNull(con);
}
assertEquals(activeSize activeSizeAfter + );
/*如果連接池中的緩存連接數大於最少緩存連接數檢查釋放數據連接後是否
緩存連接數比之前減少了一個反之緩存連接數是否保持為最少緩存連接數
*/
if (cacheSize > minConnections){
assertEquals(cacheSize cacheSizeAfter + );
} else {
assertEquals(cacheSize minConnections);
}
}
/** 釋放建立測試起始環境時的資源
*/
protected void tearDown() {
cacheImpl = null;
conProxydestroy();
}
public DefaultConnectionProxyTest(String name) {
super(name);
}
/** 你可以簡單的運行這個類從而對類中所包含的測試單元進行測試
*/
public static void main(String args[]) {
junittextuiTestRunnerrun(DefaultConnectionProxyTestclass);
}
}
當單元測試完成後我們可以用Junit提供的TestSuite對象對測試單元進行組織你可以決定測試的順序然後運行你的測試
如何維護單元測試
通過上面的描述我們對如何確定和編寫測試有了基本的了解但是需求總是變化的因此我們的單元測試也會根據需求的變化不斷的演變如果我們決定修改類的行為規則可以明確的是我們當然會對針對這個類的測試單元進行修改以適應變化但是如果對這個類僅有調用關系的類的行為定義沒有變化則相應的單元測試仍然是可靠和充分的同時如果包含行為變化的類的對象的狀態定義與其沒有直接的關系測試單元仍然起效這種結果也是封裝原則的優勢體現
From:http://tw.wingwit.com/Article/program/Java/ky/201311/28688.html