在系統(tǒng)開發(fā)過程種使用單元測試,會帶來很多的的好處,最明顯為:
When you become convinced of the value of comprehensive unit testing, you’ll find that it begins to influence how you write code, and the frameworks you choose to use。

應(yīng)用單元測試,首先要解決的是單元測試的關(guān)注點
測試的關(guān)注點在于測試邏輯,只要有邏輯就要寫測試代碼。測試的手段就是驗證所有被測試方法的所有產(chǎn)出物,包括:
1. 測試方法的返回值
2. 測試方法的執(zhí)行流程
例如:
public class DomainService {
private static TheDAO dao = new TheDAO ();
public ReturnObject findByCond(String) {
? ? ? ? return (ReturnObject)dao.getBeanByCondition("select * from ReturnObject where cond="+ paramter, ReturnObject.class);
? ? }
}

在對于測試findByCond方法,有兩個測試用例:
A.測傳遞給TheDAO.getBeanByCondition的參數(shù)的正確性,如果參數(shù)不是”select * from ReturnObject where cond=?”和ReturnObject.class則返回為null。
B.測返回的對象正確性。
?
特別是第二點,在商業(yè)應(yīng)用上比較常見的。通常有些方法無明顯output,通常是執(zhí)行寫表操作的。對于這樣的方法就是測試它的執(zhí)行流程。當然這些方法本身包含邏輯的。
一個簡單的解決方法是利用Access Log來實現(xiàn)(雖然這樣的測試不多,而寫的case代碼也看著怪怪的)。
public class ServiceExample{
??? private DatabaseDao1 dao1;
??? private DatabaseDao2 dao2;
?
??? public void noOutputMethod(){
if(...)
??????????? dao1.update(...);
??? if(...)
??????????? dao2.delete();
}
}

?
相關(guān)的測試代碼可以這樣:
public class MockDatabaseDao1 implements DatabaseDao1 {
private Map map;
public void setMap(Map map){
???? this.map = map;
?}
?
public void update(args){
???? map.put("MockDatabaseDao1.update", args);
}
}

???
public class MockDatabaseDao2 implements DatabaseDao2 {
??? private Map map;
?
??? public void setMap(Map map){
??????? this.map = map;
??? }
?
??? public void delete(args){
??????? map.put("MockDatabaseDao2.delete", args);
}
}

?
public class ServiceExampleTestCase{
??? private Map map = new HashMap();
??? public void testNoOutputMethod(){
??? ??? DaoTest test = new DaoTest();
?? DatabaseDao1 dao1 = new MockDatabaseDao1();
?? dao1.setMap(map);
?? dao2.setMap(map);
?? DatabaseDao2 dao2 = new MockDatabaseDao2();
??? test.setDao1(dao1);
??? test.setDao2(dao2);
??? test.noOutputMethod();
??? assertEquals(new Boolean(true), new Boolean(map.containsKey("MockDatabaseDao1.update")));
??? assertEquals(new Boolean(true), new Boolean(map.containsKey("MockDatabaseDao2.delete")));?
????}
}
?

例子只測試執(zhí)行流程,實際實踐中還可以驗證所有的參數(shù)。?
我們還可以考慮利用AOP來改進這個測試方法。then, we needn't to do the same work,each time. We repeat it only once.

討論完測試的關(guān)注點后,需要看看實際面臨的具體困難
職責(zé)不明確?
???類或類方法的職責(zé)不明確,違反SRP原則.一個類或方法處理了本不該有它處理的邏輯,使得單元測試需要關(guān)心過多的外部關(guān)聯(lián)類
靜態(tài)方法
??? 靜態(tài)方法使得調(diào)用者直接面對實際的服務(wù)類,難以通過其他方式替換其實現(xiàn),也難以擴展
直接訪問對象實例
調(diào)用者直接實例化服務(wù)對象,從而使用服務(wù)對象提供的服務(wù).同靜態(tài)方法一樣,直接面對其服務(wù)類
J2se和J2ee標準庫或者其他類庫
??? 標準類庫中有非常多的接口調(diào)用使得調(diào)用者難以測試 e.g JNDI, JavaMail, JAXP
準備數(shù)據(jù)及其困難
??? 編寫測試用例需要外部準備大量的數(shù)據(jù)

針對這些困難,可用解決方法如下:?
重構(gòu)系統(tǒng)
??? 對于職責(zé)不明確的代碼,只有通過重構(gòu)才可以達到單元測試的目的。
Self-Delegate test pattern
 ? 針對于class的測試,使用自代理測試模式, 使得測試時,可以重寫被測試類的一些方法.達到測試的目的.通過extend class override methods來實現(xiàn)。Inner class mock方法也一樣。不過這種方法比較別扭
編寫Stubs和Mock object?
???1.???接口的mock比較容易,測試時,編寫stubs和mock object來輔助測試,是非常重要的技術(shù). Mock object分動態(tài)mock和靜態(tài)mock.采用EasyMock可以很好的實現(xiàn)動態(tài)mock。?
???2.??具體類的mock,也很簡單,通常利用子類繼承的方式實現(xiàn),利用cglib框架可以很好大達到測試目的。?
???3.??靜態(tài)方法的mock。靜態(tài)方法由于是直接面對服務(wù)對象,比較麻煩。不過,并非不可以測試,實際我們可以利用classpath的特點來實現(xiàn)。
方法很簡單,mock類與建立一個將被mock的類的package,class name以及方法簽名完全一樣,但方法實現(xiàn)卻是mock過的。在運行測試用例時,把mock類打成jar(不一定要這么做), 在配置classpath時確保,該jar的位置在當前class之前,就可以實現(xiàn)替換。代碼如下:StaticMock.rar
使用成熟單元測試框架
???除了最基本的Junit外,Opensource提供了很多非常有價值的單元測試框架,熟練使用這些工具,可以提高測試的效率。包括對準備大量的數(shù)據(jù),以及j2ee的框架代碼。
???現(xiàn)有代碼的可選自動化測試工具:
???1.?POJO:JUnit, JMock或者EasyMock
???2.?Data Object:DDTUnit。準備大量數(shù)據(jù)。
???3.?Dao:DBUnit。初始化數(shù)據(jù)庫。批量產(chǎn)生數(shù)據(jù)庫數(shù)據(jù)。
???4.?EJB: MockEJB或者MockRunner
???5.?Servlet:Cactus
???6.?Struts:StrutsUnitTest
???7.?XML:XMLUnit
???8.?J2EE: MockRunner
???9.?GUI: JFCUnit, Marathor
???10.?Other: JTestCase(采用XML定義測試過程)

分層架構(gòu)下的單元測試
1 Web層的單元測試
主要測試Controller的數(shù)據(jù)結(jié)構(gòu)化邏輯
如果View是利用模板引擎的,需要測試頁面的控制腳本是否正確。

2 Domain Service的單元測試
包括業(yè)務(wù)規(guī)則和業(yè)務(wù)流程。
Service有四種參與對象,如下:
???1.?Domain Object
???2.?Dao對象
???3.?其它Service服務(wù)。
???4.?工具類
產(chǎn)出物:
???1.?返回值包括POJO,和結(jié)構(gòu)化的數(shù)據(jù)(如XML)
???2.?傳遞給流程節(jié)點的參數(shù)值。
特點:
???概念上,業(yè)務(wù)邏輯和業(yè)務(wù)流程是相對獨立的。實際代碼,雖然一些業(yè)務(wù)邏輯是相對獨立的。但是有一些業(yè)務(wù)邏輯與流程合在一起。由于業(yè)務(wù)邏輯有明確的返回值,業(yè)務(wù)規(guī)則可以獨立成一個方法,其是有顯示的返回值,這樣UnitTest就可以focus在業(yè)務(wù)規(guī)則的測試上。而業(yè)務(wù)流程通常沒有顯示的返回值,在很多實踐中表現(xiàn)為寫表動作,測試比較麻煩。
???同時,不過的實際情況是業(yè)務(wù)規(guī)則和業(yè)務(wù)流程是合并在一起的。

測試的應(yīng)覆蓋:
1.?返回值包括POJO,或者結(jié)構(gòu)化的數(shù)據(jù)如XML可以利用XMLUnit來解決。
2.?流程節(jié)點的訪問,以及傳遞給流程節(jié)點的參數(shù)值。即對業(yè)務(wù)流程的測試,可以使用上面的訪問點的方法。

3.Dao的單元測試
第一個面臨的問題是:做Dao數(shù)據(jù)訪問層的單元測試時機。another word也就是要不要做單元測試。
幾種情況是不用測試的
1. 如果Dao就是簡單的CRUD,那么不用測;在未來當我們使用1.5的范型后,這些CRUD只要在父類做一邊里就可以了。
2. 如果hbm文件是自動生成的,那也不用測。
以下是要測的情況:
1. 如果hbm文件是手工寫的,那么需要你保證hbm的正確性。如何測試,后面再說。
2. 如果Dao中包括了一些組合查詢,那么這是一種邏輯,就應(yīng)該去測;如果Dao的查詢還包含了某個排序機制,這個排序邏輯依據(jù)的是業(yè)務(wù)字段,那么也是要測的。(理由是:這些邏輯可以在java代碼實現(xiàn),不過是性能太差了,但是既然java代碼的邏輯要測,那么我們沒有理由不去測在sql中的邏輯)。

第二個問題如何測試:
0. 測試數(shù)據(jù)準備
可以將BA準備的數(shù)據(jù)導(dǎo)出。在利用Excel編輯產(chǎn)生一批數(shù)據(jù)。
但是每個UnitTest測試本身應(yīng)該focus一個關(guān)注點上,所以每個UnitTest的數(shù)據(jù)保持在較少的水平上。
另外由于DBUnit導(dǎo)入數(shù)據(jù)的順序是依據(jù)sheet的順序的,請注意把所有外鍵表在前,否則插入數(shù)據(jù)時,會報外鍵不存在錯誤。

1. 數(shù)據(jù)庫的選擇
a.可以直接用小組用的開發(fā)數(shù)據(jù)庫。優(yōu)點:現(xiàn)成的, 所有schema都建好了。缺點:目前數(shù)據(jù)庫的數(shù)據(jù)干凈性無法保證,連接速度太慢。
b.使用hsqldb。優(yōu)點:利用其內(nèi)存模式,可以隨測試程序啟動,簡單小巧,schema可以自行定義,每人各自一套互不影響。 缺點:無法提供PLSQL支持。出于UnitTest本身的要求,以及性能上考量,大部分情況下,建議使用hsqldb,對于涉及到PLSQL的,需要mock處理。

2.測試hbm
利用hsqldb內(nèi)存數(shù)據(jù)庫,在setup的時候,利用hibernate的SchemaExport工具類,將hbm導(dǎo)出成數(shù)據(jù)庫的schema,如果有確實有潛在問題,那么測試程序?qū)⒉煌ㄟ^。

3.測試Dao
很簡單了,調(diào)用dao程序操作。對于save,update和delete操作的。需要利用原始的connection執(zhí)行查詢驗證。對于組合查詢的和邏輯排序的,就是一般的做法了。

4.在使用DBUnit時,測試非只讀操作時,我們經(jīng)常會采用 DatabaseOperation.CLEAN_INSERT 策略.在關(guān)聯(lián)表比較多時,效率會很差.因為每次setUp,tearDown時都會重新先Delete,再Insert所有的數(shù)據(jù).另外,我們還有一種數(shù)據(jù)庫操作測試的策略,就是使用真實數(shù)據(jù)庫,在每次操作完畢后都回滾事務(wù).