Spring中進行集成測試

概述

在單元測試時,我們儘量在屏蔽模塊間相互干擾的情況下,重點關注模塊內部邏輯的正確性。而集成測試則是在將模塊整合在一起後進行的測試,它的目的在於發現一些模塊間整合的問題。有些功能很難通過模擬對象進行模擬,相反它們往往只能在真實模塊整合後,才能真正運行起來,如事務管理就是其中比較典型的例子。

按照Spring的推薦(原話:You should not normally use the Spring container for unit tests: simply populate your POJOs in plain JUnit tests!),在單元測試時,你不應該依賴於Spring容器。換言之,你不應該在單元測試時啓動ApplicatonContext並從中獲取Bean,相反你應該通過模擬對象完成單元測試。而集成測試的前提則是事先裝配好模塊和模塊之間的關聯類,如將DAO層真實的UserDao和LoginLogDao裝配到UserServiceImpl再進行測試。具體裝配工作是在Spring配置文件中完成的,因此在一般情況下,集成測試需要啓動Spring容器,你可以在測試類中簡單地從Spring容器中取出目標Bean進行測試。

需要測試的業務接口

假設我們的應用中擁有一個UserService業務層接口,它擁有4個業務方法,其代碼如下所示:

代碼清單1 UserServie接口

package com.baobaotao.service; 
import com.baobaotao.domain.User;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public interface UserService {
boolean hasMatchUser(String userName,String password);
User findUserByUserName(String userName);
void loginSuccess(User user);
void registerUser(User user);
}


我們通過UserServiceImpl對UserService提供了實現:

代碼清單2 UserServiceImpl實現UserService接口

package com.baobaotao.service;

import com.baobaotao.dao.LoginLogDao;
import com.baobaotao.dao.UserDao;
import com.baobaotao.domain.LoginLog;
import com.baobaotao.domain.User;

public class UserServiceImpl implements UserService {
private UserDao userDao;

private LoginLogDao loginLogDao;

public boolean hasMatchUser(String userName, String password) {
int matchCount = userDao.getMatchCount(userName, password);
return matchCount > 0;
}

public User findUserByUserName(String userName) {
return userDao.findUserByUserName(userName);
}

public void loginSuccess(User user) {
user.setCredits(5 + user.getCredits());
LoginLog loginLog = new LoginLog();
loginLog.setUserId(user.getUserId());
loginLog.setIp(user.getLastIp());
loginLog.setLoginDate(user.getLastVisit());
userDao.updateLoginInfo(user);
loginLogDao.insertLoginLog(loginLog);
}

public void setLoginLogDao(LoginLogDao loginLogDao) {
this.loginLogDao = loginLogDao;
}

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}




UserServiceImpl引用了兩個DAO層的類(UserDao和LoginLogDao)共同實現UserService的接口,在UserServiceImpl開放使用之前,我們有必須對其進行集成測試,以保證實現邏輯的正確性。

使用傳統的方式進行集成測試

下面,我們通過傳統的方式爲UserServiceImpl編寫了一個集成測試用例,測試代碼如下所示:

代碼清單 3 TestUserService:UserService集成測試用例

package com.baobaotao.service;

public class TestUserService extends TestCase {
public ApplicationContext ctx = null; ①Spring容器引用
private static String[] CONFIG_FILES = { ②Spring配置文件
"baobaotao-dao.xml",
"baobaotao-service.xml" };
protected void setUp() throws Exception {③啓動Spring容器
ctx = new FileSystemXmlApplicationContext(CONFIG_FILES); }
public void testHasMatchUser() { ④測試方法一 ④-1從容器中獲取Bean UserService userService = (UserService) ctx.getBean("userService"); boolean b1 = userService.hasMatchUser("admin", "123456");
boolean b2 = userService.hasMatchUser("admin", "1111"); assertTrue(b1);
assertTrue(!b2); } public void testAddLoginLog() {⑤測試方法二 ⑤-1從容器中獲取Bean UserService userService = (UserService) ctx.getBean("userService");
User user = userService.findUserByUserName("admin"); user.setUserId(1);
user.setUserName("admin");
user.setLastIp("192.168.12.7");
user.setLastVisit(new Date()); userService.loginSuccess(user); } …//省略其餘的測試方法 }

在這個測試用例中,我們使用了最原始的JUnit的TestCase進行集成測試,乍一看並沒有多大的問題,但仔細分析一下,我們就可以總結出以下四點明顯的不足:

1)導致多次Spring容器初始化問題:根據JUnit測試方法的調用流程(參見錯誤!未找到引用源。小節的描述),每執行一個測試方法都會創建一個TestUserService實例並調用setUp()方法。由於我們在setUp()方法中初始化Spring容器,這意味着TestUserService有多少個測試方法,Spring容器就會被重複初始化多少次。雖然初始化Spring容器的速度並不會太慢,但由於可能會在Sprnig容器初始化時執行加載Hibernate映射文件等耗時的操作,如果每執行一個測試方法都必須重複初始化Spring容器,則對測試性能的影響是不容忽視的;

2)需要使用硬編碼方式手工獲取Bean:在④-1和⑤-1處,我們通過ctx.getBean()方法從Spring容器中獲取需要測試的目標Bean,並且還要進行強制類型轉換的造型操作。這種乏味的操作迷漫在測試用例的代碼中,讓人覺得繁瑣不堪;

3)數據庫現場容易遭受破壞:⑤處的測試方法會對數據庫記錄進行插入操作,雖然是針對開發數據庫進行操作,但如果數據操作的影響是持久的,可能會影響到後面的測試行爲。舉個例子,你在測試方法中插入一條ID爲1的User記錄,第一次運行不會有問題,第二次運行時,就會因爲主鍵衝突而導致測試用例失敗。所以應該既能夠完成功能邏輯檢查,又能夠在測試完成後恢復現場,不會留下“後遺症”;

4)沒有對數據操作正確性進行檢查:⑤處我們向登錄日誌表插入了一條成功登錄日誌,可是我們卻沒有對t_login_log表中是否確實添加了一條記錄進行檢查。原來我們的方式是打開數據庫,肉眼觀察是否插入了相應的記錄,但這嚴重違背了自動測試的原則。試想,你在測試包括成千上萬個數據操作行爲的程序時,如何用肉眼進行檢查?

既然使用傳統方式對Spring應用進行集成測試存在這麼多不足,Spring責無旁貸地擔當起革新之任。它通過擴展JUnit框架提供了一套專門測試Spring應用的有力工具。藉助Spring集成測試工具的幫助,以上所羅列的種種問題將冰消雪融、雲開霧散。

Spring提供的測試幫助類

Spring在org.springframework.test包中爲測試提供了幾個有用的類,它們都是JUnit TestCase的子類。通過層層擴展,不斷豐富測試的功能,我們可以通過下圖瞭解這些類的繼承關係:

[img]http://dl.iteye.com/upload/attachment/511563/da157fc5-bdf7-3f57-8925-882db9ea6d05.gif[/img]

圖 1 Spring測試工具類

下面,我們來逐個瞭解這棵承繼類樹中每個節點測試類的功用,第一個要認識的是直接擴展於TestCase的ConditionalTestCase測試類。

ConditionalTestCase

如果你直接通過擴展TestCase創建測試用例,則所有帶test前綴的測試方法都會被毫無例外地執行。而ConditionalTestCase可以讓你在某些情況下,有選擇地關閉掉一些測試方法,不讓他們在測試用例中執行。這給開發者帶來了很大的靈活性,因爲他們可以在某次測試中關閉掉一些測試方法,而僅運行當前特別關注的測試方法,將問題域聚集到一定範圍內。

如果你要關閉某個測試方法行,僅需實現ConditionalTestCase的 isDisabledInThisEnvironment(String testMethodName)方法就可以了,ConditionalTestCase在運行每一個測試方法前會根據isDisabledInThisEnvironment()方法判斷是簡單放棄目標方法的運行,還是按正常方式執行之。該方法默認情況下對所有的測試方法都返回false,也即執行所有的測試方法。讓我們來看一個具體例子:

代碼清單 4 ConditionalTest1:有條件執行測試方法

package com.baobaotao.test; 
import org.springframework.test.ConditionalTestCase;
public class ConditionalTest1 extends ConditionalTestCase {
①被忽略不執行的測試方法
private static String[] IGNORED_METHODS = {"testMethod1","testMethod3"};
@Override
protected boolean isDisabledInThisEnvironment(String testMethodName) {②所有在
for (String method : IGNORED_METHODS) { IGNORED_METHODS數組中
if (method.equals(testMethodName)) { 的方法都忽略執行。
return true;
}
}
return false;
}
public void testMethod1(){ ③不執行
System.out.println("method1");
}
public void testMethod2(){ ④執行
System.out.println("method2");
}
public void testMethod3(){ ⑤不執行
System.out.println("method3");
}
}

如果我們直接承繼JUnit的TestCase,③、④及⑤處的三個測試方法都會被執行,但現在我們通過繼承ConditionalTestCase編寫測試類,並覆蓋了isDisabledInThisEnvironment()方法,當測試方法名位於IGNORED_METHODS數組中時,測試方法就被旁路掉了。因此當運行ConditionalTest1時,你會發現只有④處的testMethod2()測試方法得到了執行,其它兩個測試方法看起來也被成功執行,只不過會程序日誌會給出報告,告訴你哪些測試方法是真正被執行,而哪些方法被“僞執行”的。

ConditionalTestCase其實可用於任何程序的單元測試中,它本身並沒有和Spring容器有任何關聯,它僅添加了一個按條件執行測試方法的功能。

AbstractSpringContextTests

AbstractSpringContextTests擴展於ConditionalTestCase,它維護了一個static類型的緩存器(HashMap),它使用鍵保存Spring ApplicationContext實例,這意味着Spring ApplicationContext是JVM級的,不同測試用例、不同測試方法都可以共享這個實例。也就是說,在運行多個測試用例和測試方法時,Spring容器僅需要實例化一次就可以了,極大地提高了基於Spring容器測試程序的運行效率。Spring通過這個測試幫助類解決了前面我們所指出的第1)個問題。

AbstractSingleSpringContextTests

AbstractSingleSpringContextTests繼承於AbstractSpringContextTests,它通過一些方法讓你方便地指定Spring配置文件所在位置:

String[] getConfigLocations():該方法允許你在指定Spring配置文件時使用資源類型前綴,這些資源類型前綴包括:classpath:、file:。以類似於“com/baobaotao/beans.xml”形式指定的資源被當成類路徑資源處理;
String[] getConfigPaths():以“/”開頭的地址被當成類路徑處理,如“/com/baobaotao/beans.xml”,而未以“/”開頭的地址被當成相對於測試類所在包的文件路徑,如“beans.xml”表示配置文件在測試類所在類包的目錄下;
String getConfigPath():和getConfigPaths()類似,在僅需指定一個配置文件中使用。

以上三個方法,它們的優先級和我們介紹的先後順序對應,也就是說,當你在子類中覆蓋了getConfigLocations()方法後,其它兩個方法就沒有意義了。所以你僅需選擇三者當中適合的方法進行覆蓋,而沒有必要同時覆蓋多個方法。

AbstractSingleSpringContextTests將根據這些方法指定的Spring配置文件初始化Spring容器,然後將Spring容器引用添加到static緩存中。並通過getApplicationContext()向子類開放ApplicationContext的引用。

一般情況下,所有的測試類和測試方法都可以共享這個Spring容器直到測試完結,不過在某些極端情況下,測試方法可能會對Spring容器進行改動(比如通過程序改變Bean的配置定義),如果這種改變對於其它測試方法來說是有干擾的,這就相當於“弄髒”了作爲測試現場的Spring容器,因此在下一個測試方法執行前必須“抹除”這個改變。你可以簡單地在會“弄髒”Spring容器的測試方法中添加setDirty()方法向AbstractSingleSpringContextTests報告這一行爲,這樣在下一個測試方法執行前,AbstractSingleSpringContextTests就會重新加載Spring容器以修補被“弄髒”的部分。

雖然你可以直接繼承AbstractSpringContextTests或AbstractSingleSpringContextTests創建自己的集成測試用例,不過你大可不必如此着急。Spring已經提供了幾個功能齊全、實踐性更強的子類,讓我們繼續探索Spring集成測試工具類的精彩篇章吧。

一般集成測試

應該說,Spring通過AbstractSpringContextTests或AbstractSingleSpringContextTests準備好了集成測試的一些基礎設施,在建築學上,這叫夯實地基,而AbstractDependencyInjectionSpringContextTests是在此地基之上建起的第一幢樓房。

AbstractDependencyInjectionSpringContextTests所新添的主要功能是其子類的屬性能被Spring容器中的Bean自動裝配,你無需手工通過ApplicationContext#getBean()從容器中獲取目標Bean自行裝配。它很好回答了前面我們所指出第2)問題,下面我們通過實例進行學習:

代碼清單 5 DependencyInjectionCtxTest

package com.baobaotao.test;
import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
import com.baobaotao.service.UserService;
public class DependencyInjectionCtxTest
extends AbstractDependencyInjectionSpringContextTests {
private UserService userService;
public void setUserService(UserService userService) {①該屬性設置方法會被自動調動
this.userService = userService;
}
@Override
protected String[] getConfigLocations() { ②指定Spring配置文件所在位置
return new String[]{"baobaotao-service.xml","baobaotao-dao.xml"};
}
public void testHasMatchUser(){ ③測試方法
boolean match = userService.hasMatchUser("tom","123456");
assertEquals(true,match);
}

}


在②處,我們指定了Spring配置文件所在的位置,AbstractDependencyInjectionSpringContextTests將使用這些配置文件初始化好Spring容器,並將它們保存於static的緩存中。然後馬上着手根據類型匹配機制(byType),自動將Spring容器中匹配測試類屬性的Bean通過Setter注入到測試類中。爲了方便說明這一重要的特性,我們先看一下baobaotao-service.xml的內容:

<beans> <tx:annotation-driven/> ①按類型匹配於DependencyInjectionCtxTest的userService屬性 <bean id="userService" class="com.baobaotao.service.UserServiceImpl"> <property name="userDao" ref="userDao"/> <property name="loginLogDao" ref="loginLogDao"/> </bean> … </beans>


根據baobaotao-service.xml配置文件的內容,我們知道Spring容器中有一個UserService Bean,AbstractDependencyInjectionSpringContextTests探測到Spring容器中存在一個匹配於userService屬性的Bean後,就將其注入到DependencyInjectionCtxTest的userService屬性中。userService是這個集成測試類的測試固件,因此我們說AbstractDependencyInjectionSpringContextTests可以自己裝配測試固件。

解決自動裝配問題

如果Spring容器中擁有多個匹配UserService類型的Bean,由於Spring沒有足夠的信息做出取捨決策,因此會拋出UnsatisfiedDependencyException異常。假設我們採用以下傳統的事務管理的配置方式對UserService進行配置,按類型匹配的自動裝配機制就會引發問題:

①用於被代理的目標Bean,按類型匹配於UserService

<bean id="userServiceTarget" class="com.baobaotao.service.UserServiceImpl">
<property name="userDao" ref="userDao" />
<property name="loginLogDao" ref="loginLogDao"></property>
</bean>

②通過事務代理工廠爲UserServiceImpl創建的代理Bean,也按匹配於UserService

<bean id="userService"
class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="transactionManager" ref="transactionManager" />
<property name="target" ref="userServiceTarget" />
<property name="transactionAttributes">

</property>
</bean>

由於①處和②處的Bean都按類型匹配於UserService,在對DependencyInjectionCtxTest的userService屬性進行自動裝配將會引發問題。有兩種針對該問題的解決辦法:

調整配置文件,使按類型匹配於UserService的Bean僅有一個,具體有以下兩個方法:
將①處的Bean作爲②處的內部Bean進行裝配;
使用基於註解驅動的事務管理配置機制,這樣就無需在配置文件中定義兩個UserService的Bean了。關於註解驅動事務管理配置的詳細信息,請參見9.6小節的內容。
改變DependencyInjectionCtxTest的自動裝配機制:Spring默認使用byType類型的自動裝配機制,但它允許你通過setAutowireMode()的方法改變默認自動裝配的機制,比如你可以調用setAutowireMode(AUTOWIRE_BY_NAME)方法啓用按名稱匹配的自動裝配機制。

AbstractDependencyInjectionSpringContextTests定義了三個代表自動裝配機制類型的常量,分別說明如下:

AUTOWIRE_BY_TYPE:按類型匹配的方式進行自動裝配,這個默認的機制;
AUTOWIRE_BY_NAME:按名字匹配的方式進行自動裝配
AUTOWIRE_NO:不使用自動裝配機制,這意味着你需要手工調用getBean()進行裝配。

現在我們解決了在自動裝配時,因Spring容器中存在多個匹配Bean而導致的問題,接下來讓我們考察另一個自動裝配的問題。

依賴檢查

假設我們在DependencyInjectionCtxTest添加一個User類型的屬性並提供Setter方法,而Spring容器中沒有匹配該屬性的Bean:

package com.baobaotao.test;

import com.baobaotao.domain.User;
public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
private User user;
public void setUser(User user) {
this.user = user;
}

}


猜想一下重新運行DependencyInjectionCtxTest將會發生什麼情況呢?答案可能讓你失望:UnsatisfiedDependencyException再次象黑幕一樣降臨。在默認情況下, AbstractDependencyInjectionSpringContextTests要求所有屬性都能在Spring容器中找到對應Bean,否則拋出異常。

仔細思考一下,這種運行機制並非沒有道理,因爲既然你已經提供了Setter方法,就相當於給出了這樣的暗示信息:“這個屬性測試類自身創建不了,必須由外部提供”。而在使用自動裝配機制的情況下,測試類屬性自動從Spring容器中注入匹配的屬性,一般情況下不會手工去調用Setter方法準備屬性。

如果你出於一些特殊的理由,希望在採用自動裝配的情況下,如果有屬性未得到裝配也不在乎,那麼你可以在測試類構造函數中調用setDependencyCheck(false)方法達到目的:

package com.baobaotao.test;

public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
public DependencyInjectionCtxTest(){
setDependencyCheck(false); ①告知不進行屬性依賴性檢查
}

}


這個AbstractDependencyInjectionSpringContextTests就不會對測試類有些屬性找不到匹配Bean而拋出異常了。

在不提供Setter方法的情況下自動注入

大多數IDE都提供了爲屬性變量自動生成Setter方法的操作,因此客觀地說,爲屬性編寫一個Setter方法的工作根本不值一提。如果你覺得衆多的Setter方法影響了視覺感觀,但又希望享受測試類屬性自動裝配的好處,Spring也不會讓你失望的。你需要做的是以下兩步的工作:

1) 將需要自動裝配的屬性變量聲明爲protected;

2) 在測試類構造函數中調用setPopulateProtectedVariables(true)方法。

package com.baobaotao.test;

public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
protected UserService userService; ①將屬性聲明爲protected
// public void setUserService(UserService userService) { ②大膽將Setter方法移除掉
// this.userService = userService;
// }
public DependencyInjectionCtxTest(){
setDependencyCheck(false);
setPopulateProtectedVariables(true); ③啓用直接對屬性變量進行註釋的機制
}

}


將屬性聲明爲protected後並通過setPopulateProtectedVariables(true)啓用對屬性變量直接注入的機制(啓用反射機制注入),你就可以避免爲屬性變量編寫對應的Setter方法了。

提示 屬性如果聲明爲public,雖然你也調用了setPopulateProtectedVariables(true)方法,屬性變量依然不會被自動注入。所以這種機制僅限於protected的屬性變量。

方便地恢復測試數據庫現場

我們現在已經可以通過AbstractDependencyInjectionSpringContextTests的屬性自動裝配機制方便地建立起測試固件,省卻手工調用getBean()自行準備測試固件的煩惱。當我們對UserService的hasMatchUser()和findUserByUserName()方法進行測試時,不會有任何問題,因爲這兩個方法僅對數據庫執行讀操作。但UserService以下兩個接口方法會對數據庫執行更改操作:

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


當我們對這兩個接口方法進行測試時,它們將會在數據庫中產生持久化數據。考慮對registerUser(User user)方法進行測試時,我們可能編寫如下所示的測試方法:

public void testRegisterUser(){
User user = new User();
user.setUserId(2);
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
}

當第一次成功運行testRegisterUser()測試方法時,將在數據庫中產生一條主鍵爲2的記錄,如何第二次重新運行testRegisterUser()測試方法其結果將不言自明:因主鍵衝突導致測試方法執行失敗,最終報告測試用例沒有通過。在這種情況下,測試用例未通過並不是因爲UserServiceImpl#registerUser(User user)存在邏輯錯誤,而是因爲測試方法的積累效應導致外在設施的現場發生變化而引起的問題。

爲了防止這種問題,測試用例必須在保證不對數據庫狀態產生持久化變化的情況下,對目標類的數據操作邏輯正確性進行檢測。乍一聽這一要求有點貌似於“既想馬兒跑,又想馬兒不吃草”一樣充滿悖論,實則不然。只要我們讓測試方法不提交事務,在測試完後自動回滾事務,就皆大歡喜了。

讓測試方法自動擁有回滾能力

AbstractTransactionalSpringContextTests專爲解決以上問題而生,也就是說前面我們所提及的第3)個問題在此得到了回答。只要繼承該類創建測試用例,在默認情況下,測試方法中所包含的事務性數據操作都會在測試方法返回前被回滾。由於事務回滾操作發生在測試方法返回前的點上,所以你可以象往常一樣在測試方法體中對數據操作的正確性進行校驗。

代碼清單 6 UserServiceIntegrateTest:

package com.baobaotao.service;
import org.springframework.test.AbstractTransactionalSpringContextTests;
import com.baobaotao.domain.User;
public class UserServiceIntegrateTest extends AbstractTransactionalSpringContextTests {
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
protected String[] getConfigLocations() {
return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
}
public void testRegisterUser(){ ①測試方法中的數據操作將在方法返回前被回滾,不會對數據庫
User user = new User(); 產生永久性數據操作,第二次運行該測試方法時,依舊可以
user.setUserId(2); 成功運行。
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
User user1 = userService.findUserByUserName("john"); ②對數據操作進行
assertEquals(user.getUserId(), user1.getUserId()); 正確性檢驗
}
}


如果testRegisterUser()是直接繼承於AbstractDependencyInjectionSpringContextTests類的測試方法,則重複運行該測試方法就會發生數據衝突問題。但因爲它位於繼承於AbstractTransactionalSpringContextTests的測試用例類中,測試方法中對數據庫的操作會被正確回滾,所以重複運行不會有任何問題。

如果你確實希望測試方法中對數據庫的操作持久生效而不是被回滾,Spring也可以滿足你的要求,你僅需要在測試方法中添加setComplete()方法就可以了。

 public void testRegisterUser(){

User user1 = userService.findUserByUserName("john");
assertEquals(user.getUserId(), user1.getUserId());
setComplete(); ①測試方法中的事務性數據操作將被提交
}


AbstractTransactionalSpringContextTests還擁有幾個可用於初始化測試數據庫,並在測試完成後清除測試數據的方法,分別介紹如下:

onSetUpBeforeTransaction()/onTearDownAfterTransaction():子類可以覆蓋這兩個方法,以便在事務性測試方法運行的前後執行一些數據庫初始化的操作並在事務完成後清除之;
onSetUpInTransaction()/onTearDownInTransaction():這對方法和前面介紹的方法完成相同的功能,只不過它們是在測試方法的相同事務中執行的。

AbstractTransactionalSpringContextTests另外還提供了一組用於測試延遲數據加載的方法:endTransaction()/startNewTransaction()。我在測試Hibernate、JPA等允許延遲數據加載的應用時,如何模擬數據在Service層事務中被部分加載,當傳遞到Web層時重新打開事務完成延遲部分數據加載的測試場景呢?這兩個方法即爲此用途而生:你可以在測試方法中顯式調用endTransaction()方法以模擬從Service層中獲取部分數據後返回,爾後,再通過startNewTransaction()開啓一個和原事務無關新事務——模擬在Web層中重新打開事務,接下來你就可以訪問延遲加載的數據,看是否一切如期所料了。

在代碼清單 6的②處,我們通過UserService#findUserByUserName()方法對前面registerUser(user)方法數據操作的正確性進行檢驗。應該說,我們非常幸運,因爲在UserService中剛好存在一個可用於檢測registerUser(user)數據操作正確性的方法。讓我們考慮另外的一種情況:要是 UserService不存在這樣的方法,我們該如何檢測registerUser(user)數據操作結果的正確性呢?顯然我們不能使用肉眼觀察的方法,那難道爲了驗證數據操作正確性專門編寫一個配合性的數據訪問類不成?

通過JDBC訪問數據庫,檢測數據操作正確性

正當我們“山重水複疑無路”的時候,讓我們再往前走上一程,柳暗花明將倏忽而至
——AbstractTransactionalDataSourceSpringContextTests就是花開景明之所。該類繼承於AbstractTransactionalSpringContextTests,它添加了一個JdbcTemplate,你可以藉由此道快意直達數據庫。它自動使用Spring容器中的數據源(DataSource)創建好一個JdbcTemplate實例並開放給子類使用。值得注意的是,如果你採用byName自動裝配機制,數據源Bean的名稱必須取名爲“dataSource”。

讓我們對UserServiceIntegrateTest進行改造,以便讓其自動擁有訪問數據庫的設施(JdbcTemplate),並用靈活的方法訪問數據庫進行數據操作的檢驗,其代碼如下所示:

代碼清單 7 UserServiceIntegrateWithJdbcTest

package com.baobaotao.service;
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;

public class UserServiceIntegrateWithJdbcTest
extends AbstractTransactionalDataSourceSpringContextTests {①注意:繼承類發生調整
private UserService userService;
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
protected String[] getConfigLocations() {
return new String[]{"baobaotao-service.xml", "baobaotao-dao.xml"};
}
public void testRegisterUser(){
User user = new User();
user.setUserId(2);
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
String sqlStr = " SELECT user_id FROM t_user WHERE user_name ='john' ";
int userId = jdbcTemplate.queryForInt(sqlStr); ①可以直接使用JdbcTemplate訪問數據庫了
assertEquals(user.getUserId(), userId);
setComplete();
}
}


jdbcTemplate是AbstractTransactionalDataSourceSpringContextTests類中定義的,子類可以直接使用它訪問數據庫。這樣我們就可以靈活地訪問數據庫以檢驗目標測試方法的數據操作正確性。至此,我們終於畢其功於一役於AbstractTransactionalDataSourceSpringContextTests,順利解決前面我們中指出的最後問題。

只要你通過擴展AbstractTransactionalSpringContextTests及其子類創建測試用例,所有測試方法都會工作了事務環境下。也就是說,即使某些測試方法不需要訪問數據庫,也會產生額外的事務管理開銷,是否可以對測試方法啓用事務管理的行爲進行控制呢?此外,在一些情況下,除對目標方法邏輯運行的正確性進行檢驗外,我們還希望對目標方法的運行性能進行測試:如當目標方法運行時間超過200毫秒時,則測試用例視爲未通過。諸如此類的問題,我們目前學習到的知識還不能很好的應付。Spring 2.0新增了註解驅動的測試工具爲我們指明瞭道路,你僅需要通過簡單爲測試方法標註註解,我們剛纔提出的“疑難”問題就可以迎刃而解了。

小結

本文我們講述了使用Spring提供的一套測試工具對Spring應用程序進行集成測試所需的所有知識。
Spring建議你不應該在單元測試時使用到Spring容器,你應該在集成測試時才使用到Spring容器。手工創建測試固件或者手工裝配測試固件的工作都是單調乏味沒有創意的工作,通過使用Spring爲集成測試提供了幫助類,你就可以享受測試固件自動裝配的好處,將精力集中到目標類邏輯測試編寫的工作上。

應該說大部分的Java應用都是Web應用,而大部分的Java Web應用都是數據庫相關的應用,對數據庫應用進行測試經常要考慮數據準備、數據庫現場恢復、靈活訪問數據以驗證數據操作正確性等等的問題。這些問題如果沒有一個很好的支持工具,將給編寫測試用例造成挑戰,幸好Spring都爲我們搭建好滿足這些需求的測試平臺,你僅需要在此基礎上編寫特定的測試用例就可以了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章