在系統開發過程種使用單元測試,會帶來很多的的好處,
最明顯為: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。
應用單元測試,首先要解決的是
單元測試的關注點。
測試的關注點在于測試邏輯,只要有邏輯就要寫測試代碼。測試的手段就是驗證所有被測試方法的所有產出物,包括:
1. 測試方法的返回值
2. 測試方法的執行流程
例如:
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的參數的正確性,如果參數不是”select * from ReturnObject where cond=?”和ReturnObject.class則返回為null。
B.測返回的對象正確性。
?
特別是第二點,在商業應用上比較常見的。通常有些方法無明顯output,通常是執行寫表操作的。對于這樣的方法就是測試它的執行流程。當然這些方法本身包含邏輯的。
一個簡單的解決方法是利用Access Log來實現(雖然這樣的測試不多,而寫的case代碼也看著怪怪的)。
public class ServiceExample{ ??? private DatabaseDao1 dao1; ??? private DatabaseDao2 dao2; ? ??? public void noOutputMethod(){ if(...) ??????????? dao1.update(...); ??? if(...) ??????????? dao2.delete(); } } |
?
相關的測試代碼可以這樣:
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")));? ????} } ? |
例子只測試執行流程,實際實踐中還可以驗證所有的參數。?
我們還可以考慮利用AOP來改進這個測試方法。then, we needn't to do the same work,each time. We repeat it only once.
討論完測試的關注點后,需要看看實際面臨的具體困難
職責不明確?
???類或類方法的職責不明確,違反SRP原則.一個類或方法處理了本不該有它處理的邏輯,使得單元測試需要關心過多的外部關聯類
靜態方法
??? 靜態方法使得調用者直接面對實際的服務類,難以通過其他方式替換其實現,也難以擴展
直接訪問對象實例
調用者直接實例化服務對象,從而使用服務對象提供的服務.同靜態方法一樣,直接面對其服務類
J2se和J2ee標準庫或者其他類庫
??? 標準類庫中有非常多的接口調用使得調用者難以測試 e.g JNDI, JavaMail, JAXP
準備數據及其困難
??? 編寫測試用例需要外部準備大量的數據
針對這些困難,可用解決方法如下:?
重構系統。
??? 對于職責不明確的代碼,只有通過重構才可以達到單元測試的目的。
Self-Delegate test pattern
? 針對于class的測試,使用自代理測試模式, 使得測試時,可以重寫被測試類的一些方法.達到測試的目的.通過extend class override methods來實現。Inner class mock方法也一樣。不過這種方法比較別扭
編寫Stubs和Mock object?
???1.???接口的mock比較容易,測試時,編寫stubs和mock object來輔助測試,是非常重要的技術. Mock object分動態mock和靜態mock.采用EasyMock可以很好的實現動態mock。?
???2.??具體類的mock,也很簡單,通常利用子類繼承的方式實現,利用cglib框架可以很好大達到測試目的。?
???3.??靜態方法的mock。靜態方法由于是直接面對服務對象,比較麻煩。不過,并非不可以測試,實際我們可以利用classpath的特點來實現。
方法很簡單,mock類與建立一個將被mock的類的package,class name以及方法簽名完全一樣,但方法實現卻是mock過的。在運行測試用例時,把mock類打成jar(不一定要這么做), 在配置classpath時確保,該jar的位置在當前class之前,就可以實現替換。代碼如下:StaticMock.rar
使用成熟單元測試框架
???除了最基本的Junit外,Opensource提供了很多非常有價值的單元測試框架,熟練使用這些工具,可以提高測試的效率。包括對準備大量的數據,以及j2ee的框架代碼。
???現有代碼的可選自動化測試工具:
???1.?POJO:JUnit, JMock或者EasyMock
???2.?Data Object:DDTUnit。準備大量數據。
???3.?Dao:DBUnit。初始化數據庫。批量產生數據庫數據。
???4.?EJB: MockEJB或者MockRunner
???5.?Servlet:Cactus
???6.?Struts:StrutsUnitTest
???7.?XML:XMLUnit
???8.?J2EE: MockRunner
???9.?GUI: JFCUnit, Marathor
???10.?Other: JTestCase(采用XML定義測試過程)
分層架構下的單元測試
1 Web層的單元測試
主要測試Controller的數據結構化邏輯
如果View是利用模板引擎的,需要測試頁面的控制腳本是否正確。
2 Domain Service的單元測試
包括業務規則和業務流程。
Service有四種參與對象,如下:
???1.?Domain Object
???2.?Dao對象
???3.?其它Service服務。
???4.?工具類
產出物:
???1.?返回值包括POJO,和結構化的數據(如XML)
???2.?傳遞給流程節點的參數值。
特點:
???概念上,業務邏輯和業務流程是相對獨立的。實際代碼,雖然一些業務邏輯是相對獨立的。但是有一些業務邏輯與流程合在一起。由于業務邏輯有明確的返回值,業務規則可以獨立成一個方法,其是有顯示的返回值,這樣UnitTest就可以focus在業務規則的測試上。而業務流程通常沒有顯示的返回值,在很多實踐中表現為寫表動作,測試比較麻煩。
???同時,不過的實際情況是業務規則和業務流程是合并在一起的。
測試的應覆蓋:
1.?返回值包括POJO,或者結構化的數據如XML可以利用XMLUnit來解決。
2.?流程節點的訪問,以及傳遞給流程節點的參數值。即對業務流程的測試,可以使用上面的訪問點的方法。
3.Dao的單元測試
第一個面臨的問題是:做Dao數據訪問層的單元測試時機。another word也就是要不要做單元測試。
幾種情況是不用測試的
1. 如果Dao就是簡單的CRUD,那么不用測;在未來當我們使用1.5的范型后,這些CRUD只要在父類做一邊里就可以了。
2. 如果hbm文件是自動生成的,那也不用測。
以下是要測的情況:
1. 如果hbm文件是手工寫的,那么需要你保證hbm的正確性。如何測試,后面再說。
2. 如果Dao中包括了一些組合查詢,那么這是一種邏輯,就應該去測;如果Dao的查詢還包含了某個排序機制,這個排序邏輯依據的是業務字段,那么也是要測的。(理由是:這些邏輯可以在java代碼實現,不過是性能太差了,但是既然java代碼的邏輯要測,那么我們沒有理由不去測在sql中的邏輯)。
第二個問題如何測試:
0. 測試數據準備
可以將BA準備的數據導出。在利用Excel編輯產生一批數據。
但是每個UnitTest測試本身應該focus一個關注點上,所以每個UnitTest的數據保持在較少的水平上。
另外由于DBUnit導入數據的順序是依據sheet的順序的,請注意把所有外鍵表在前,否則插入數據時,會報外鍵不存在錯誤。
1. 數據庫的選擇
a.可以直接用小組用的開發數據庫。優點:現成的, 所有schema都建好了。缺點:目前數據庫的數據干凈性無法保證,連接速度太慢。
b.使用hsqldb。優點:利用其內存模式,可以隨測試程序啟動,簡單小巧,schema可以自行定義,每人各自一套互不影響。 缺點:無法提供PLSQL支持。出于UnitTest本身的要求,以及性能上考量,大部分情況下,建議使用hsqldb,對于涉及到PLSQL的,需要mock處理。
2.測試hbm
利用hsqldb內存數據庫,在setup的時候,利用hibernate的SchemaExport工具類,將hbm導出成數據庫的schema,如果有確實有潛在問題,那么測試程序將不通過。
3.測試Dao
很簡單了,調用dao程序操作。對于save,update和delete操作的。需要利用原始的connection執行查詢驗證。對于組合查詢的和邏輯排序的,就是一般的做法了。
4.在使用DBUnit時,測試非只讀操作時,我們經常會采用 DatabaseOperation.CLEAN_INSERT 策略.在關聯表比較多時,效率會很差.因為每次setUp,tearDown時都會重新先Delete,再Insert所有的數據.另外,我們還有一種數據庫操作測試的策略,就是使用真實數據庫,在每次操作完畢后都回滾事務.