人在江湖

            BlogJava :: 首頁(yè) :: 聯(lián)系 :: 聚合  :: 管理
            82 Posts :: 10 Stories :: 169 Comments :: 0 Trackbacks

          轉(zhuǎn)載自 http://tech.it168.com/j/2007-10-19/200710190919953.shtml

           

          在單元測(cè)試時(shí),我們盡量在屏蔽模塊間相互干擾的情況下,重點(diǎn)關(guān)注模塊內(nèi)部邏輯的正確性。而集成測(cè)試則是在將模塊整合在一起后進(jìn)行的測(cè)試,它的目的在于發(fā)現(xiàn)一些模塊間整合的問(wèn)題。有些功能很難通過(guò)模擬對(duì)象進(jìn)行模擬,相反它們往往只能在真實(shí)模塊整合后,才能真正運(yùn)行起來(lái),如事務(wù)管理就是其中比較典型的例子。
          按照Spring的推薦(原話:You should not normally use the Spring container for unit tests: simply populate your POJOs in plain JUnit tests!),在單元測(cè)試時(shí),你不應(yīng)該依賴于Spring容器。換言之,你不應(yīng)該在單元測(cè)試時(shí)啟動(dòng)ApplicatonContext并從中獲取Bean,相反你應(yīng)該通過(guò)模擬對(duì)象完成單元測(cè)試。而集成測(cè)試的前提則是事先裝配好模塊和模塊之間的關(guān)聯(lián)類(lèi),如將DAO層真實(shí)的UserDao和LoginLogDao裝配到UserServiceImpl再進(jìn)行測(cè)試。具體裝配工作是在Spring配置文件中完成的,因此在一般情況下,集成測(cè)試需要啟動(dòng)Spring容器,你可以在測(cè)試類(lèi)中簡(jiǎn)單地從Spring容器中取出目標(biāo)Bean進(jìn)行測(cè)試。
          需要測(cè)試的業(yè)務(wù)接口
          假設(shè)我們的應(yīng)用中擁有一個(gè)UserService業(yè)務(wù)層接口,它擁有4個(gè)業(yè)務(wù)方法,其代碼如下所示:
          代碼清單1 UserServie接口

             1: package com.baobaotao.service;
             2: import com.baobaotao.domain.User;
             3: import org.springframework.transaction.annotation.Transactional;
             4: @Transactional
             5: public interface UserService {
             6: boolean hasMatchUser(String userName,String password);
             7: User findUserByUserName(String userName);
             8: void loginSuccess(User user);
             9: void registerUser(User user);
            10: }

          我們通過(guò)UserServiceImpl對(duì)UserService提供了實(shí)現(xiàn):
          代碼清單2 UserServiceImpl實(shí)現(xiàn)UserService接口

             1: package com.baobaotao.service;
             2: import com.baobaotao.dao.LoginLogDao;
             3: import com.baobaotao.dao.UserDao;
             4: import com.baobaotao.domain.LoginLog;
             5: import com.baobaotao.domain.User;
             6: public class UserServiceImpl implements UserService {
             7: private UserDao userDao;
             8: private LoginLogDao loginLogDao;
             9: public boolean hasMatchUser(String userName, String password) {
            10: int matchCount =userDao.getMatchCount(userName, password);
            11: return matchCount > 0;
            12: }
            13: public User findUserByUserName(String userName) {
            14: return userDao.findUserByUserName(userName);
            15: }
            16: public void loginSuccess(User user) {
            17: user.setCredits( 5 + user.getCredits());
            18: LoginLog loginLog = new LoginLog();
            19: loginLog.setUserId(user.getUserId());
            20: loginLog.setIp(user.getLastIp());
            21: loginLog.setLoginDate(user.getLastVisit());
            22: userDao.updateLoginInfo(user);
            23: loginLogDao.insertLoginLog(loginLog);
            24: }
            25: public void setLoginLogDao(LoginLogDao loginLogDao) {
            26: this.loginLogDao = loginLogDao;
            27: }
            28: public void setUserDao(UserDao userDao) {
            29: this.userDao = userDao;
            30: }
            31: }

          UserServiceImpl引用了兩個(gè)DAO層的類(lèi)(UserDao和LoginLogDao)共同實(shí)現(xiàn)UserService的接口,在UserServiceImpl開(kāi)放使用之前,我們有必須對(duì)其進(jìn)行集成測(cè)試,以保證實(shí)現(xiàn)邏輯的正確性。

          使用傳統(tǒng)的方式進(jìn)行集成測(cè)試
          下面,我們通過(guò)傳統(tǒng)的方式為UserServiceImpl編寫(xiě)了一個(gè)集成測(cè)試用例,測(cè)試代碼如下所示:
          代碼清單 3 TestUserService:UserService集成測(cè)試用例

             1: package com.baobaotao.service;
             2:
             3: public class TestUserService extends TestCase {
             4: public ApplicationContext ctx = null; ①Spring容器引用
             5: private static String[] CONFIG_FILES = { ②Spring配置文件
             6: "baobaotao-dao.xml",
             7: "baobaotao-service.xml" };
             8: protected void setUp() throws Exception {③啟動(dòng)Spring容器
             9: ctx = new FileSystemXmlApplicationContext(CONFIG_FILES);
            10: }
            11: public void testHasMatchUser() { ④測(cè)試方法一
            12: ④-1從容器中獲取Bean
            13: UserService userService = (UserService) ctx.getBean("userService");
            14: boolean b1 = userService.hasMatchUser("admin", "123456");
            15: boolean b2 = userService.hasMatchUser("admin", "1111");
            16: assertTrue(b1);
            17: assertTrue(!b2);
            18: }
            19: public void testAddLoginLog() {⑤測(cè)試方法二
            20: ⑤-1從容器中獲取Bean
            21: UserService userService = (UserService) ctx.getBean("userService");
            22: User user = userService.findUserByUserName("admin");
            23: user.setUserId(1);
            24: user.setUserName("admin");
            25: user.setLastIp("192.168.12.7");
            26: user.setLastVisit(new Date());
            27: userService.loginSuccess(user);
            28: }
            29://省略其余的測(cè)試方法 
            30: } 

          在這個(gè)測(cè)試用例中,我們使用了最原始的JUnit的TestCase進(jìn)行集成測(cè)試,乍一看并沒(méi)有多大的問(wèn)題,但仔細(xì)分析一下,我們就可以總結(jié)出以下四點(diǎn)明顯的不足:
          1)導(dǎo)致多次Spring容器初始化問(wèn)題:根據(jù)JUnit測(cè)試方法的調(diào)用流程(參見(jiàn)錯(cuò)誤!未找到引用源。小節(jié)的描述),每執(zhí)行一個(gè)測(cè)試方法都會(huì)創(chuàng)建一個(gè)TestUserService實(shí)例并調(diào)用setUp()方法。由于我們?cè)趕etUp()方法中初始化Spring容器,這意味著TestUserService有多少個(gè)測(cè)試方法,Spring容器就會(huì)被重復(fù)初始化多少次。雖然初始化Spring容器的速度并不會(huì)太慢,但由于可能會(huì)在Sprnig容器初始化時(shí)執(zhí)行加載Hibernate映射文件等耗時(shí)的操作,如果每執(zhí)行一個(gè)測(cè)試方法都必須重復(fù)初始化Spring容器,則對(duì)測(cè)試性能的影響是不容忽視的;
          2)需要使用硬編碼方式手工獲取Bean:在④-1和⑤-1處,我們通過(guò)ctx.getBean()方法從Spring容器中獲取需要測(cè)試的目標(biāo)Bean,并且還要進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換的造型操作。這種乏味的操作迷漫在測(cè)試用例的代碼中,讓人覺(jué)得繁瑣不堪;
          3)數(shù)據(jù)庫(kù)現(xiàn)場(chǎng)容易遭受破壞:⑤處的測(cè)試方法會(huì)對(duì)數(shù)據(jù)庫(kù)記錄進(jìn)行插入操作,雖然是針對(duì)開(kāi)發(fā)數(shù)據(jù)庫(kù)進(jìn)行操作,但如果數(shù)據(jù)操作的影響是持久的,可能會(huì)影響到后面的測(cè)試行為。舉個(gè)例子,你在測(cè)試方法中插入一條ID為1的User記錄,第一次運(yùn)行不會(huì)有問(wèn)題,第二次運(yùn)行時(shí),就會(huì)因?yàn)橹麈I沖突而導(dǎo)致測(cè)試用例失敗。所以應(yīng)該既能夠完成功能邏輯檢查,又能夠在測(cè)試完成后恢復(fù)現(xiàn)場(chǎng),不會(huì)留下“后遺癥”;
          4)沒(méi)有對(duì)數(shù)據(jù)操作正確性進(jìn)行檢查:⑤處我們向登錄日志表插入了一條成功登錄日志,可是我們卻沒(méi)有對(duì)t_login_log表中是否確實(shí)添加了一條記錄進(jìn)行檢查。原來(lái)我們的方式是打開(kāi)數(shù)據(jù)庫(kù),肉眼觀察是否插入了相應(yīng)的記錄,但這嚴(yán)重違背了自動(dòng)測(cè)試的原則。試想,你在測(cè)試包括成千上萬(wàn)個(gè)數(shù)據(jù)操作行為的程序時(shí),如何用肉眼進(jìn)行檢查?
          既然使用傳統(tǒng)方式對(duì)Spring應(yīng)用進(jìn)行集成測(cè)試存在這么多不足,Spring責(zé)無(wú)旁貸地?fù)?dān)當(dāng)起革新之任。它通過(guò)擴(kuò)展JUnit框架提供了一套專(zhuān)門(mén)測(cè)試Spring應(yīng)用的有力工具。借助Spring集成測(cè)試工具的幫助,以上所羅列的種種問(wèn)題將冰消雪融、云開(kāi)霧散。

          Spring在org.springframework.test包中為測(cè)試提供了幾個(gè)有用的類(lèi),它們都是JUnit TestCase的子類(lèi)。通過(guò)層層擴(kuò)展,不斷豐富測(cè)試的功能,我們可以通過(guò)下圖了解這些類(lèi)的繼承關(guān)系:

          圖 1 Spring測(cè)試工具類(lèi)
          下面,我們來(lái)逐個(gè)了解這棵承繼類(lèi)樹(shù)中每個(gè)節(jié)點(diǎn)測(cè)試類(lèi)的功用,第一個(gè)要認(rèn)識(shí)的是直接擴(kuò)展于TestCase的ConditionalTestCase測(cè)試類(lèi)。
          ConditionalTestCase
          如果你直接通過(guò)擴(kuò)展TestCase創(chuàng)建測(cè)試用例,則所有帶test前綴的測(cè)試方法都會(huì)被毫無(wú)例外地執(zhí)行。而ConditionalTestCase可以讓你在某些情況下,有選擇地關(guān)閉掉一些測(cè)試方法,不讓他們?cè)跍y(cè)試用例中執(zhí)行。這給開(kāi)發(fā)者帶來(lái)了很大的靈活性,因?yàn)樗麄兛梢栽谀炒螠y(cè)試中關(guān)閉掉一些測(cè)試方法,而僅運(yùn)行當(dāng)前特別關(guān)注的測(cè)試方法,將問(wèn)題域聚集到一定范圍內(nèi)。
          如果你要關(guān)閉某個(gè)測(cè)試方法行,僅需實(shí)現(xiàn)ConditionalTestCase的 isDisabledInThisEnvironment(String testMethodName)方法就可以了,ConditionalTestCase在運(yùn)行每一個(gè)測(cè)試方法前會(huì)根據(jù)isDisabledInThisEnvironment()方法判斷是簡(jiǎn)單放棄目標(biāo)方法的運(yùn)行,還是按正常方式執(zhí)行之。該方法默認(rèn)情況下對(duì)所有的測(cè)試方法都返回false,也即執(zhí)行所有的測(cè)試方法。讓我們來(lái)看一個(gè)具體例子:
          代碼清單 4 ConditionalTest1:有條件執(zhí)行測(cè)試方法

             1: package com.baobaotao.test;
             2: import org.springframework.test.ConditionalTestCase;
             3: public class ConditionalTest1 extends ConditionalTestCase {
             4: ①被忽略不執(zhí)行的測(cè)試方法
             5: private static String[] IGNORED_METHODS = {"testMethod1","testMethod3"};
             6: @Override
             7: protected boolean isDisabledInThisEnvironment(String testMethodName) {②所有在
             8: for (String method : IGNORED_METHODS) { IGNORED_METHODS數(shù)組中
             9: if (method.equals(testMethodName)) { 的方法都忽略執(zhí)行。
            10: return true;
            11: }
            12: }
            13: return false;
            14: }
            15: public void testMethod1(){ ③不執(zhí)行
            16: System.out.println("method1");
            17: }
            18: public void testMethod2(){ ④執(zhí)行
            19: System.out.println("method2");
            20: }
            21: public void testMethod3(){ ⑤不執(zhí)行
            22: System.out.println("method3");
            23: }
            24: } 

          如果我們直接承繼JUnit的TestCase,③、④及⑤處的三個(gè)測(cè)試方法都會(huì)被執(zhí)行,但現(xiàn)在我們通過(guò)繼承ConditionalTestCase編寫(xiě)測(cè)試類(lèi),并覆蓋了isDisabledInThisEnvironment()方法,當(dāng)測(cè)試方法名位于IGNORED_METHODS數(shù)組中時(shí),測(cè)試方法就被旁路掉了。因此當(dāng)運(yùn)行ConditionalTest1時(shí),你會(huì)發(fā)現(xiàn)只有④處的testMethod2()測(cè)試方法得到了執(zhí)行,其它兩個(gè)測(cè)試方法看起來(lái)也被成功執(zhí)行,只不過(guò)會(huì)程序日志會(huì)給出報(bào)告,告訴你哪些測(cè)試方法是真正被執(zhí)行,而哪些方法被“偽執(zhí)行”的。
          ConditionalTestCase其實(shí)可用于任何程序的單元測(cè)試中,它本身并沒(méi)有和Spring容器有任何關(guān)聯(lián),它僅添加了一個(gè)按條件執(zhí)行測(cè)試方法的功能。
          AbstractSpringContextTests
          AbstractSpringContextTests擴(kuò)展于ConditionalTestCase,它維護(hù)了一個(gè)static類(lèi)型的緩存器(HashMap),它使用鍵保存Spring ApplicationContext實(shí)例,這意味著Spring ApplicationContext是JVM級(jí)的,不同測(cè)試用例、不同測(cè)試方法都可以共享這個(gè)實(shí)例。也就是說(shuō),在運(yùn)行多個(gè)測(cè)試用例和測(cè)試方法時(shí),Spring容器僅需要實(shí)例化一次就可以了,極大地提高了基于Spring容器測(cè)試程序的運(yùn)行效率。Spring通過(guò)這個(gè)測(cè)試幫助類(lèi)解決了前面我們所指出的第1)個(gè)問(wèn)題。
          AbstractSingleSpringContextTests

          AbstractSingleSpringContextTests繼承于AbstractSpringContextTests,它通過(guò)一些方法讓你方便地指定Spring配置文件所在位置:
          ? String[] getConfigLocations():該方法允許你在指定Spring配置文件時(shí)使用資源類(lèi)型前綴,這些資源類(lèi)型前綴包括:classpath:、file:。以類(lèi)似于“com/baobaotao/beans.xml”形式指定的資源被當(dāng)成類(lèi)路徑資源處理;
          ? String[] getConfigPaths():以“/”開(kāi)頭的地址被當(dāng)成類(lèi)路徑處理,如“/com/baobaotao/beans.xml”,而未以“/”開(kāi)頭的地址被當(dāng)成相對(duì)于測(cè)試類(lèi)所在包的文件路徑,如“beans.xml”表示配置文件在測(cè)試類(lèi)所在類(lèi)包的目錄下;
          ? String getConfigPath():和getConfigPaths()類(lèi)似,在僅需指定一個(gè)配置文件中使用。
          以上三個(gè)方法,它們的優(yōu)先級(jí)和我們介紹的先后順序?qū)?yīng),也就是說(shuō),當(dāng)你在子類(lèi)中覆蓋了getConfigLocations()方法后,其它兩個(gè)方法就沒(méi)有意義了。所以你僅需選擇三者當(dāng)中適合的方法進(jìn)行覆蓋,而沒(méi)有必要同時(shí)覆蓋多個(gè)方法。
          AbstractSingleSpringContextTests將根據(jù)這些方法指定的Spring配置文件初始化Spring容器,然后將Spring容器引用添加到static緩存中。并通過(guò)getApplicationContext()向子類(lèi)開(kāi)放ApplicationContext的引用。
          一般情況下,所有的測(cè)試類(lèi)和測(cè)試方法都可以共享這個(gè)Spring容器直到測(cè)試完結(jié),不過(guò)在某些極端情況下,測(cè)試方法可能會(huì)對(duì)Spring容器進(jìn)行改動(dòng)(比如通過(guò)程序改變Bean的配置定義),如果這種改變對(duì)于其它測(cè)試方法來(lái)說(shuō)是有干擾的,這就相當(dāng)于“弄臟”了作為測(cè)試現(xiàn)場(chǎng)的Spring容器,因此在下一個(gè)測(cè)試方法執(zhí)行前必須“抹除”這個(gè)改變。你可以簡(jiǎn)單地在會(huì)“弄臟”Spring容器的測(cè)試方法中添加setDirty()方法向AbstractSingleSpringContextTests報(bào)告這一行為,這樣在下一個(gè)測(cè)試方法執(zhí)行前,AbstractSingleSpringContextTests就會(huì)重新加載Spring容器以修補(bǔ)被“弄臟”的部分。
          雖然你可以直接繼承AbstractSpringContextTests或AbstractSingleSpringContextTests創(chuàng)建自己的集成測(cè)試用例,不過(guò)你大可不必如此著急。Spring已經(jīng)提供了幾個(gè)功能齊全、實(shí)踐性更強(qiáng)的子類(lèi),讓我們繼續(xù)探索Spring集成測(cè)試工具類(lèi)的精彩篇章吧。

          一般集成測(cè)試
          應(yīng)該說(shuō),Spring通過(guò)AbstractSpringContextTests或AbstractSingleSpringContextTests準(zhǔn)備好了集成測(cè)試的一些基礎(chǔ)設(shè)施,在建筑學(xué)上,這叫夯實(shí)地基,而AbstractDependencyInjectionSpringContextTests是在此地基之上建起的第一幢樓房。
          AbstractDependencyInjectionSpringContextTests所新添的主要功能是其子類(lèi)的屬性能被Spring容器中的Bean自動(dòng)裝配,你無(wú)需手工通過(guò)ApplicationContext#getBean()從容器中獲取目標(biāo)Bean自行裝配。它很好回答了前面我們所指出第2)問(wèn)題,下面我們通過(guò)實(shí)例進(jìn)行學(xué)習(xí):

             1: package com.baobaotao.test;
             2: import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
             3: import com.baobaotao.service.UserService;
             4: public class DependencyInjectionCtxTest
             5: extends AbstractDependencyInjectionSpringContextTests {
             6: private UserService userService;
             7: public void setUserService(UserService userService) {①該屬性設(shè)置方法會(huì)被自動(dòng)調(diào)動(dòng)
             8: this.userService = userService;
             9: }
            10: @Override
            11: protected String[] getConfigLocations() { ②指定Spring配置文件所在位置
            12: return new String[]{"baobaotao-service.xml","baobaotao-dao.xml"};
            13: }
            14: public void testHasMatchUser(){ ③測(cè)試方法
            15: boolean match = userService.hasMatchUser("tom","123456");
            16: assertEquals(true, match);
            17: }
            18:
            19: } 

          代碼清單 5 DependencyInjectionCtxTest

          在②處,我們指定了Spring配置文件所在的位置,AbstractDependencyInjectionSpringContextTests將使用這些配置文件初始化好Spring容器,并將它們保存于static的緩存中。然后馬上著手根據(jù)類(lèi)型匹配機(jī)制(byType),

          自動(dòng)將Spring容器中匹配測(cè)試類(lèi)屬性的Bean通過(guò)Setter注入到測(cè)試類(lèi)中。為了方便說(shuō)明這一重要的特性,我們先看一下baobaotao-service.xml的內(nèi)容:

             1: <beans>
             2: <tx:annotation-driven/>
             3: ①按類(lèi)型匹配于DependencyInjectionCtxTest的userService屬性
             4: <bean id="userService" class="com.baobaotao.service.UserServiceImpl">
             5: <property name="userDao" ref="userDao"/>
             6: <property name="loginLogDao" ref="loginLogDao"/>
             7: </bean>
             8:
             9: </beans> 

          根據(jù)baobaotao-service.xml配置文件的內(nèi)容,我們知道Spring容器中有一個(gè)UserService Bean,AbstractDependencyInjectionSpringContextTests探測(cè)到Spring容器中存在一個(gè)匹配于userService屬性的Bean后,就將其注入到DependencyInjectionCtxTest的userService屬性中。userService是這個(gè)集成測(cè)試類(lèi)的測(cè)試固件,因此我們說(shuō)AbstractDependencyInjectionSpringContextTests可以自己裝配測(cè)試固件。

          解決自動(dòng)裝配問(wèn)題
          如果Spring容器中擁有多個(gè)匹配UserService類(lèi)型的Bean,由于Spring沒(méi)有足夠的信息做出取舍決策,因此會(huì)拋出UnsatisfiedDependencyException異常。假設(shè)我們采用以下傳統(tǒng)的事務(wù)管理的配置方式對(duì)UserService進(jìn)行配置,按類(lèi)型匹配的自動(dòng)裝配機(jī)制就會(huì)引發(fā)問(wèn)題:
          ①用于被代理的目標(biāo)Bean,按類(lèi)型匹配于UserService

             1: <bean id="userServiceTarget" class="com.baobaotao.service.UserServiceImpl">
             2: <property name="userDao" ref="userDao" />
             3: <property name="loginLogDao" ref="loginLogDao"></property>
             4: </bean> 

          ②通過(guò)事務(wù)代理工廠為UserServiceImpl創(chuàng)建的代理Bean,也按匹配于UserService

             1: <bean id="userService"
             2: class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
             3: <property name="transactionManager" ref="transactionManager" />
             4: <property name="target" ref="userServiceTarget" />
             5: <property name="transactionAttributes">
             6:
             7: </property>
             8: </bean> 

          由于①處和②處的Bean都按類(lèi)型匹配于UserService,在對(duì)DependencyInjectionCtxTest的userService屬性進(jìn)行自動(dòng)裝配將會(huì)引發(fā)問(wèn)題。有兩種針對(duì)該問(wèn)題的解決辦法:
          ? 調(diào)整配置文件,使按類(lèi)型匹配于UserService的Bean僅有一個(gè),具體有以下兩個(gè)方法:
          ? 將①處的Bean作為②處的內(nèi)部Bean進(jìn)行裝配;
          ? 使用基于注解驅(qū)動(dòng)的事務(wù)管理配置機(jī)制,這樣就無(wú)需在配置文件中定義兩個(gè)UserService的Bean了。關(guān)于注解驅(qū)動(dòng)事務(wù)管理配置的詳細(xì)信息,請(qǐng)參見(jiàn)9.6小節(jié)的內(nèi)容。
          ? 改變DependencyInjectionCtxTest的自動(dòng)裝配機(jī)制:Spring默認(rèn)使用byType類(lèi)型的自動(dòng)裝配機(jī)制,但它允許你通過(guò)setAutowireMode()的方法改變默認(rèn)自動(dòng)裝配的機(jī)制,比如你可以調(diào)用setAutowireMode(AUTOWIRE_BY_NAME)方法啟用按名稱(chēng)匹配的自動(dòng)裝配機(jī)制。AbstractDependencyInjectionSpringContextTests定義了三個(gè)代表自動(dòng)裝配機(jī)制類(lèi)型的常量,分別說(shuō)明如下:
          ? AUTOWIRE_BY_TYPE:按類(lèi)型匹配的方式進(jìn)行自動(dòng)裝配,這個(gè)默認(rèn)的機(jī)制;
          ? AUTOWIRE_BY_NAME:按名字匹配的方式進(jìn)行自動(dòng)裝配
          ? AUTOWIRE_NO:不使用自動(dòng)裝配機(jī)制,這意味著你需要手工調(diào)用getBean()進(jìn)行裝配。
          現(xiàn)在我們解決了在自動(dòng)裝配時(shí),因Spring容器中存在多個(gè)匹配Bean而導(dǎo)致的問(wèn)題,接下來(lái)讓我們考察另一個(gè)自動(dòng)裝配的問(wèn)題。
          依賴檢查
          假設(shè)我們?cè)贒ependencyInjectionCtxTest添加一個(gè)User類(lèi)型的屬性并提供Setter方法,而Spring容器中沒(méi)有匹配該屬性的Bean:

             1: package com.baobaotao.test;
             2:
             3: import com.baobaotao.domain.User;
             4: public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
             5: private User user;
             6: public void setUser(User user) {
             7: this.user = user;
             8: }
             9:
            10: } 

          猜想一下重新運(yùn)行DependencyInjectionCtxTest將會(huì)發(fā)生什么情況呢?答案可能讓你失望:UnsatisfiedDependencyException再次象黑幕一樣降臨。在默認(rèn)情況下, AbstractDependencyInjectionSpringContextTests要求所有屬性都能在Spring容器中找到對(duì)應(yīng)Bean,否則拋出異常。
          仔細(xì)思考一下,這種運(yùn)行機(jī)制并非沒(méi)有道理,因?yàn)榧热荒阋呀?jīng)提供了Setter方法,就相當(dāng)于給出了這樣的暗示信息:“這個(gè)屬性測(cè)試類(lèi)自身創(chuàng)建不了,必須由外部提供”。而在使用自動(dòng)裝配機(jī)制的情況下,測(cè)試類(lèi)屬性自動(dòng)從Spring容器中注入匹配的屬性,一般情況下不會(huì)手工去調(diào)用Setter方法準(zhǔn)備屬性。
          如果你出于一些特殊的理由,希望在采用自動(dòng)裝配的情況下,如果有屬性未得到裝配也不在乎,那么你可以在測(cè)試類(lèi)構(gòu)造函數(shù)中調(diào)用setDependencyCheck(false)方法達(dá)到目的:

             1: package com.baobaotao.test;
             2:
             3: public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
             4: public DependencyInjectionCtxTest(){
             5: setDependencyCheck(false); ①告知不進(jìn)行屬性依賴性檢查
             6: }
             7:
             8: } 

          這個(gè)AbstractDependencyInjectionSpringContextTests就不會(huì)對(duì)測(cè)試類(lèi)有些屬性找不到匹配Bean而拋出異常了。

          在不提供Setter方法的情況下自動(dòng)注入
          大多數(shù)IDE都提供了為屬性變量自動(dòng)生成Setter方法的操作,因此客觀地說(shuō),為屬性編寫(xiě)一個(gè)Setter方法的工作根本不值一提。如果你覺(jué)得眾多的Setter方法影響了視覺(jué)感觀,但又希望享受測(cè)試類(lèi)屬性自動(dòng)裝配的好處,Spring也不會(huì)讓你失望的。你需要做的是以下兩步的工作:
          1) 將需要自動(dòng)裝配的屬性變量聲明為protected;
          2) 在測(cè)試類(lèi)構(gòu)造函數(shù)中調(diào)用setPopulateProtectedVariables(true)方法。

             1: package com.baobaotao.test;
             2:
             3: public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
             4: protected UserService userService; ①將屬性聲明為protected
             5: // public void setUserService(UserService userService) { ②大膽將Setter方法移除掉
             6: // this.userService = userService;
             7: // } 
             8: public DependencyInjectionCtxTest(){
             9: setDependencyCheck(false);
            10: setPopulateProtectedVariables(true); ③啟用直接對(duì)屬性變量進(jìn)行注釋的機(jī)制
            11: }
            12:
            13: } 

          將屬性聲明為protected后并通過(guò)setPopulateProtectedVariables(true)啟用對(duì)屬性變量直接注入的機(jī)制(啟用反射機(jī)制注入),你就可以避免為屬性變量編寫(xiě)對(duì)應(yīng)的Setter方法了。
          提示 屬性如果聲明為public,雖然你也調(diào)用了setPopulateProtectedVariables(true)方法,屬性變量依然不會(huì)被自動(dòng)注入。所以這種機(jī)制僅限于protected的屬性變量。
          方便地恢復(fù)測(cè)試數(shù)據(jù)庫(kù)現(xiàn)場(chǎng)
          我們現(xiàn)在已經(jīng)可以通過(guò)AbstractDependencyInjectionSpringContextTests的屬性自動(dòng)裝配機(jī)制方便地建立起測(cè)試固件,省卻手工調(diào)用getBean()自行準(zhǔn)備測(cè)試固件的煩惱。當(dāng)我們對(duì)UserService的hasMatchUser()和findUserByUserName()方法進(jìn)行測(cè)試時(shí),不會(huì)有任何問(wèn)題,因?yàn)檫@兩個(gè)方法僅對(duì)數(shù)據(jù)庫(kù)執(zhí)行讀操作。但UserService以下兩個(gè)接口方法會(huì)對(duì)數(shù)據(jù)庫(kù)執(zhí)行更改操作:

          void loginSuccess(User user); void registerUser(User user);

          當(dāng)我們對(duì)這兩個(gè)接口方法進(jìn)行測(cè)試時(shí),它們將會(huì)在數(shù)據(jù)庫(kù)中產(chǎn)生持久化數(shù)據(jù)。考慮對(duì)registerUser(User user)方法進(jìn)行測(cè)試時(shí),我們可能編寫(xiě)如下所示的測(cè)試方法:

             1: public void testRegisterUser(){
             2: User user = new User();
             3: user.setUserId(2);
             4: user.setUserName("john");
             5: user.setPassword("123456");
             6: userService.registerUser(user);
             7: } 

          當(dāng)?shù)谝淮纬晒\(yùn)行testRegisterUser()測(cè)試方法時(shí),將在數(shù)據(jù)庫(kù)中產(chǎn)生一條主鍵為2的記錄,如何第二次重新運(yùn)行testRegisterUser()測(cè)試方法其結(jié)果將不言自明:因主鍵沖突導(dǎo)致測(cè)試方法執(zhí)行失敗,最終報(bào)告測(cè)試用例沒(méi)有通過(guò)。在這種情況下,測(cè)試用例未通過(guò)并不是因?yàn)閁serServiceImpl#registerUser(User user)存在邏輯錯(cuò)誤,而是因?yàn)闇y(cè)試方法的積累效應(yīng)導(dǎo)致外在設(shè)施的現(xiàn)場(chǎng)發(fā)生變化而引起的問(wèn)題。
          為了防止這種問(wèn)題,測(cè)試用例必須在保證不對(duì)數(shù)據(jù)庫(kù)狀態(tài)產(chǎn)生持久化變化的情況下,對(duì)目標(biāo)類(lèi)的數(shù)據(jù)操作邏輯正確性進(jìn)行檢測(cè)。乍一聽(tīng)這一要求有點(diǎn)貌似于“既想馬兒跑,又想馬兒不吃草”一樣充滿悖論,實(shí)則不然。只要我們讓測(cè)試方法不提交事務(wù),在測(cè)試完后自動(dòng)回滾事務(wù),就皆大歡喜了。

          讓測(cè)試方法自動(dòng)擁有回滾能力
          AbstractTransactionalSpringContextTests專(zhuān)為解決以上問(wèn)題而生,也就是說(shuō)前面我們所提及的第3)個(gè)問(wèn)題在此得到了回答。只要繼承該類(lèi)創(chuàng)建測(cè)試用例,在默認(rèn)情況下,測(cè)試方法中所包含的事務(wù)性數(shù)據(jù)操作都會(huì)在測(cè)試方法返回前被回滾。由于事務(wù)回滾操作發(fā)生在測(cè)試方法返回前的點(diǎn)上,所以你可以象往常一樣在測(cè)試方法體中對(duì)數(shù)據(jù)操作的正確性進(jìn)行校驗(yàn)。

             1: package com.baobaotao.service;
             2: import org.springframework.test.AbstractTransactionalSpringContextTests;
             3: import com.baobaotao.domain.User;
             4: public class UserServiceIntegrateTest extends AbstractTransactionalSpringContextTests {
             5: private UserService userService;
             6: public void setUserService(UserService userService) {
             7: this.userService = userService;
             8: }
             9: @Override
            10: protected String[] getConfigLocations() {
            11: return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
            12: }
            13: public void testRegisterUser(){ ①測(cè)試方法中的數(shù)據(jù)操作將在方法返回前被回滾,不會(huì)對(duì)數(shù)據(jù)庫(kù)
            14: User user = new User(); 產(chǎn)生永久性數(shù)據(jù)操作,第二次運(yùn)行該測(cè)試方法時(shí),依舊可以
            15: user.setUserId(2); 成功運(yùn)行。
            16: user.setUserName("john");
            17: user.setPassword("123456");
            18: userService.registerUser(user);
            19: User user1 = userService.findUserByUserName("john"); ②對(duì)數(shù)據(jù)操作進(jìn)行
            20: assertEquals(user.getUserId(), user1.getUserId()); 正確性檢驗(yàn)
            21: }
            22: } 

          代碼清單 6 UserServiceIntegrateTest:

          如果testRegisterUser()是直接繼承于AbstractDependencyInjectionSpringContextTests類(lèi)的測(cè)試方法,則重復(fù)運(yùn)行該測(cè)試方法就會(huì)發(fā)生數(shù)據(jù)沖突問(wèn)題。但因?yàn)樗挥诶^承于

          AbstractTransactionalSpringContextTests的測(cè)試用例類(lèi)中,測(cè)試方法中對(duì)數(shù)據(jù)庫(kù)的操作會(huì)被正確回滾,所以重復(fù)運(yùn)行不會(huì)有任何問(wèn)題。
          如果你確實(shí)希望測(cè)試方法中對(duì)數(shù)據(jù)庫(kù)的操作持久生效而不是被回滾,Spring也可以滿足你的要求,你僅需要在測(cè)試方法中添加setComplete()方法就可以了。

             1: public void testRegisterUser(){
             2:
             3: User user1 = userService.findUserByUserName("john");
             4: assertEquals(user.getUserId(), user1.getUserId());
             5: setComplete(); ①測(cè)試方法中的事務(wù)性數(shù)據(jù)操作將被提交
             6: } 

          AbstractTransactionalSpringContextTests還擁有幾個(gè)可用于初始化測(cè)試數(shù)據(jù)庫(kù),并在測(cè)試完成后清除測(cè)試數(shù)據(jù)的方法,分別介紹如下:
          ? onSetUpBeforeTransaction()/onTearDownAfterTransaction():子類(lèi)可以覆蓋這兩個(gè)方法,以便在事務(wù)性測(cè)試方法運(yùn)行的前后執(zhí)行一些數(shù)據(jù)庫(kù)初始化的操作并在事務(wù)完成后清除之;
          ? onSetUpInTransaction()/onTearDownInTransaction():這對(duì)方法和前面介紹的方法完成相同的功能,只不過(guò)它們是在測(cè)試方法的相同事務(wù)中執(zhí)行的。
          AbstractTransactionalSpringContextTests另外還提供了一組用于測(cè)試延遲數(shù)據(jù)加載的方法:endTransaction()/startNewTransaction()。我在測(cè)試Hibernate、JPA等允許延遲數(shù)據(jù)加載的應(yīng)用時(shí),如何模擬數(shù)據(jù)在Service層事務(wù)中被部分加載,當(dāng)傳遞到Web層時(shí)重新打開(kāi)事務(wù)完成延遲部分?jǐn)?shù)據(jù)加載的測(cè)試場(chǎng)景呢?這兩個(gè)方法即為此用途而生:你可以在測(cè)試方法中顯式調(diào)用endTransaction()方法以模擬從Service層中獲取部分?jǐn)?shù)據(jù)后返回,爾后,再通過(guò)startNewTransaction()開(kāi)啟一個(gè)和原事務(wù)無(wú)關(guān)新事務(wù)——模擬在Web層中重新打開(kāi)事務(wù),接下來(lái)你就可以訪問(wèn)延遲加載的數(shù)據(jù),看是否一切如期所料了。
          在代碼清單 6的②處,我們通過(guò)UserService#findUserByUserName()方法對(duì)前面registerUser(user)方法數(shù)據(jù)操作的正確性進(jìn)行檢驗(yàn)。應(yīng)該說(shuō),我們非常幸運(yùn),因?yàn)樵赨serService中剛好存在一個(gè)可用于檢測(cè)registerUser(user)數(shù)據(jù)操作正確性的方法。讓我們考慮另外的一種情況:要是 UserService不存在這樣的方法,我們?cè)撊绾螜z測(cè)registerUser(user)數(shù)據(jù)操作結(jié)果的正確性呢?顯然我們不能使用肉眼觀察的方法,那難道為了驗(yàn)證數(shù)據(jù)操作正確性專(zhuān)門(mén)編寫(xiě)一個(gè)配合性的數(shù)據(jù)訪問(wèn)類(lèi)不成?
          通過(guò)JDBC訪問(wèn)數(shù)據(jù)庫(kù),檢測(cè)數(shù)據(jù)操作正確性
          正當(dāng)我們“山重水復(fù)疑無(wú)路”的時(shí)候,讓我們?cè)偻白呱弦怀蹋祷鲗①亢龆?
          ——AbstractTransactionalDataSourceSpringContextTests就是花開(kāi)景明之所。該類(lèi)繼承于AbstractTransactionalSpringContextTests,它添加了一個(gè)JdbcTemplate,你可以借由此道快意直達(dá)數(shù)據(jù)庫(kù)。它自動(dòng)使用Spring容器中的數(shù)據(jù)源(DataSource)創(chuàng)建好一個(gè)JdbcTemplate實(shí)例并開(kāi)放給子類(lèi)使用。值得注意的是,如果你采用byName自動(dòng)裝配機(jī)制,數(shù)據(jù)源Bean的名稱(chēng)必須取名為“dataSource”。
          讓我們對(duì)UserServiceIntegrateTest進(jìn)行改造,以便讓其自動(dòng)擁有訪問(wèn)數(shù)據(jù)庫(kù)的設(shè)施(JdbcTemplate),并用靈活的方法訪問(wèn)數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)操作的檢驗(yàn),其代碼如下所示:

          代碼清單 7 UserServiceIntegrateWithJdbcTest

             1: package com.baobaotao.service;
             2: import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
             3:
             4: public class UserServiceIntegrateWithJdbcTest
             5: extends AbstractTransactionalDataSourceSpringContextTests {①注意:繼承類(lèi)發(fā)生調(diào)整
             6: private UserService userService;
             7: public void setUserService(UserService userService) {
             8: this.userService = userService;
             9: }
            10: @Override
            11: protected String[] getConfigLocations() {
            12: return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
            13: }
            14: public void testRegisterUser(){
            15: User user = new User();
            16: user.setUserId(2);
            17: user.setUserName("john");
            18: user.setPassword("123456");
            19: userService.registerUser(user);
            20: String sqlStr = " SELECT user_id FROM t_user WHERE user_name ='john' ";
            21: int userId = jdbcTemplate.queryForInt(sqlStr); ①可以直接使用JdbcTemplate訪問(wèn)數(shù)據(jù)庫(kù)了
            22: assertEquals(user.getUserId(), userId);
            23: setComplete();
            24: }
            25: }

          jdbcTemplate是AbstractTransactionalDataSourceSpringContextTests類(lèi)中定義的,子類(lèi)可以直接使用它訪問(wèn)數(shù)據(jù)庫(kù)。這樣我們就可以靈活地訪問(wèn)數(shù)據(jù)庫(kù)以檢驗(yàn)?zāi)繕?biāo)測(cè)試方法的數(shù)據(jù)操作正確性。至此,我們終于畢其功于

          一役于AbstractTransactionalDataSourceSpringContextTests,順利解決前面我們中指出的最后問(wèn)題。 只要你通過(guò)擴(kuò)展AbstractTransactionalSpringContextTests及其子類(lèi)創(chuàng)建測(cè)試用例,所有測(cè)試方法都會(huì)工作了

          事務(wù)環(huán)境下。也就是說(shuō),即使某些測(cè)試方法不需要訪問(wèn)數(shù)據(jù)庫(kù),也會(huì)產(chǎn)生額外的事務(wù)管理開(kāi)銷(xiāo),是否可以對(duì)測(cè)試方法啟用事務(wù)管理的行為進(jìn)行控制呢?此外,在一些情況下,除對(duì)目標(biāo)方法邏輯運(yùn)行的正確性進(jìn)行檢驗(yàn)外,我們還希望

          對(duì)目標(biāo)方法的運(yùn)行性能進(jìn)行測(cè)試:如當(dāng)目標(biāo)方法運(yùn)行時(shí)間超過(guò)200毫秒時(shí),則測(cè)試用例視為未通過(guò)。諸如此類(lèi)的問(wèn)題,我們目前學(xué)習(xí)到的知識(shí)還不能很好的應(yīng)付。Spring 2.0新增了注解驅(qū)動(dòng)的測(cè)試工具為我們指明了道路,你僅需要

          通過(guò)簡(jiǎn)單為測(cè)試方法標(biāo)注注解,我們剛才提出的“疑難”問(wèn)題就可以迎刃而解了。
          小結(jié)
          本文我們講述了使用Spring提供的一套測(cè)試工具對(duì)Spring應(yīng)用程序進(jìn)行集成測(cè)試所需的所有知識(shí)。
          Spring建議你不應(yīng)該在單元測(cè)試時(shí)使用到Spring容器,你應(yīng)該在集成測(cè)試時(shí)才使用到Spring容器。手工創(chuàng)建測(cè)試固件或者手工裝配測(cè)試固件的工作都是單調(diào)乏味沒(méi)有創(chuàng)意的工作,通過(guò)使用Spring為集成測(cè)試提供了幫助類(lèi),你就

          可以享受測(cè)試固件自動(dòng)裝配的好處,將精力集中到目標(biāo)類(lèi)邏輯測(cè)試編寫(xiě)的工作上。 應(yīng)該說(shuō)大部分的Java應(yīng)用都是Web應(yīng)用,而大部分的Java Web應(yīng)用都是數(shù)據(jù)庫(kù)相關(guān)的應(yīng)用,對(duì)數(shù)據(jù)庫(kù)應(yīng)用進(jìn)行測(cè)試經(jīng)常要考慮數(shù)據(jù)準(zhǔn)備、數(shù)據(jù)庫(kù)現(xiàn)

          場(chǎng)恢復(fù)、靈活訪問(wèn)數(shù)據(jù)以驗(yàn)證數(shù)據(jù)操作正確性等等的問(wèn)題。這些問(wèn)題如果沒(méi)有一個(gè)

          很好的支持工具,將給編寫(xiě)測(cè)試用例造成挑戰(zhàn),幸好Spring都為我們搭建好滿足這些需求的測(cè)試平臺(tái),你僅需要在此基礎(chǔ)上編寫(xiě)特定的測(cè)試用例就可以了。

          posted on 2011-02-24 10:12 人在江湖 閱讀(1817) 評(píng)論(0)  編輯  收藏 所屬分類(lèi): spring
          主站蜘蛛池模板: 菏泽市| 齐河县| 云浮市| 榆社县| 怀远县| 栾川县| 定陶县| 三河市| 南乐县| 武汉市| 泽库县| 舟山市| 龙泉市| 铁力市| 灵璧县| 新河县| 九龙县| 定陶县| 桐乡市| 灵山县| 利辛县| 武清区| 屯昌县| 丰台区| 伊宁县| 永定县| 天水市| 临高县| 克什克腾旗| 焦作市| 河池市| 广元市| 周宁县| 江门市| 盐边县| 井研县| 石门县| 云南省| 西安市| 合江县| 闽清县|