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