(同步自http://www.blogjava.net/AndersLin/archive/2006/06/12/52298.html)
在系統開發過程種使用單元測試,會帶來很多的的好處,最明顯為:
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所有的資料.另外,我們還有一種資料庫操作測試的策略,就是使用真實資料庫,在每次操作完畢後都復原事務.