測試的時(shí)候會(huì )破壞數據庫現場(chǎng),如果兩次都執行添加的測試用例,那么會(huì )拋出主鍵重復的異常,另外測試方法中都要手動(dòng)去獲得Service對象,硬編碼;使用Spring提供的測試機制:
1. 保護數據庫現場(chǎng)
2. 2.通過(guò)IOC將Service注入來(lái)獲取其對象,而不是硬編碼
3. 查看數據庫中的實(shí)際值
Spring的測試包為spring-test.jar文件(某些版本的測試包可能在spring-mock.jar下,具體應用時(shí)請檢查對應的class文件是否存在),

ConditionalTestCase
它本身和Spring沒(méi)有任何關(guān)聯(lián),可用于任何單元測試中,他僅僅是添加了一個(gè)按條件執行測試方法的功能。在單元測試中,所有帶test前綴的方法都會(huì )被毫無(wú)例外的執行,現在的junit4X版本可以通過(guò)加一個(gè)@Test注解,而不用遵守這個(gè)約定。而繼承ConditionalTestCase,可以讓你在某些情況下,有選擇的關(guān)閉掉某些測試方法。
只需要實(shí)現ConditionalTestCase的isDisabledInThisEnvironment方法,ConditionalTestCase在運行每個(gè)方法千,會(huì )根據isDisabledInThisEnvironment判斷是否為要執行的方法,某人情況下,返回false,也就是執行所有方法。
public void runBare() throws Throwable {
// getName will return the name of the method being run
if (isDisabledInThisEnvironment(getName())) {
recordDisabled();
logger.info("**** " + getClass().getName() + "." +
getName() + " disabled in this environment: " +
"Total disabled tests=" + getDisabledTestCount());
return;
}
// Let JUnit handle execution
super.runBare();
}
/**
* Should this test run?
* @param testMethodName name of the test method
* @return whether the test should execute in the current envionment
*/
protected boolean isDisabledInThisEnvironment(String testMethodName) {
return false;
}
AbstractSpringContextTests
擴展自ConditionalTestCase,它維護了一個(gè)static類(lèi)型的緩存器,使用鍵保存了Spring ApplicationContext實(shí)例,這意味著(zhù),不同的用例,不同測試方法都可以共享這個(gè)實(shí)例,也就是說(shuō),在運行多個(gè)測試用例和測試方法時(shí),Spring容器只需要實(shí)例化一次就可以了。
private static Map contextKeyToContextMap = new HashMap();
AbstractSingleSpringContextTests
繼承自AbstractSpringContextTests,她通過(guò)一些方法讓你更方便地制定Spring配置文件所在位置:
protected final void setUp() throws Exception {
this.applicationContext = getContext(contextKey());
prepareTestInstance();
onSetUp();
}
protected final void tearDown() throws Exception {
onTearDown();
}
我可以看到,該類(lèi)將steUp和tearDown覆蓋為final的,所以我們在子類(lèi)中就不能自己再去覆蓋這些類(lèi)了。setUp的作用就是確保創(chuàng )建了配置文件所執行的容器。
this.applicationContext = getContext(contextKey());
該方法建立了applicationContext,而contextKLey方法方法:
protected Object contextKey() {
return getConfigLocations();
}
protected String[] getConfigLocations() {
String[] paths = getConfigPaths();
String[] locations = new String[paths.length];
for (int i = 0; i < paths.length; i++) {
String path = paths[i];
if (path.startsWith("/")) {
locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX +
path;
}
else {
locations[i] = ResourceUtils.CLASSPATH_URL_PREFIX +
StringUtils.cleanPath(
ClassUtils.classPackageAsResourcePath(
getClass()) + "/" + path);
}
}
return locations;
}
protected String[] getConfigPaths() {
String path = getConfigPath();
return (path != null ? new String[] {path} : new String[0]);
}
protected String getConfigPath() {
return null;
}
我們從這里可以看出他們的優(yōu)先級的原因,另外再看一下getContext()方法,他是以final的形式寫(xiě)在父類(lèi)AbstractSpringContextTests中的:
/**
* Obtain an ApplicationContext for the given key, potentially cached.
* @param key the context key
* @return the corresponding ApplicationContext instance (potentially cached)
*/
protected final ConfigurableApplicationContext getContext(Object key) throws Exception {
String keyString = contextKeyString(key);
ConfigurableApplicationContext ctx =
(ConfigurableApplicationContext) contextKeyToContextMap.get(keyString);
if (ctx == null) {
ctx = loadContext(key);
ctx.registerShutdownHook();
contextKeyToContextMap.put(keyString, ctx);
}
return ctx;
}
可以看出這個(gè)方法的意思是先到緩存中查找context,如果沒(méi)找到context則創(chuàng )建。而作為hash的key,則是以路徑的某種形式作為key的。
另外,loadContext在這里類(lèi)中并沒(méi)有做任何事,而是交給之類(lèi)去實(shí)現的:
protected ConfigurableApplicationContext loadContextLocations(String[] locations) throws Exception {
++this.loadCount;
if (logger.isInfoEnabled()) {
logger.info("Loading context for locations: " +
StringUtils.arrayToCommaDelimitedString(locations));
}
return createApplicationContext(locations);
}
protected ConfigurableApplicationContext createApplicationContext(String[] locations) {
GenericApplicationContext context = new
GenericApplicationContext();
customizeBeanFactory(
context.getDefaultListableBeanFactory());
new XmlBeanDefinitionReader(context).
loadBeanDefinitions(locations);
context.refresh();
return context;
}
AbstractDependencyInjectionSpringContextTests
在前面的類(lèi)都準備好一些基礎方法之后,AbstractDependencyInjectionSpringContextTests提供了自動(dòng)依賴(lài)注入的功能,無(wú)需手工從容器中獲取目標bean進(jìn)行裝配。類(lèi)似于ByType的方式:
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) {①該屬性設置方法會(huì )被自動(dòng)調動(dòng)
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);
}
}
<beans> <tx:annotation-driven/> ①按類(lèi)型匹配于DependencyInjectionCtxTest的userService屬性
<bean id="userService" class="com.baobaotao.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="loginLogDao" ref="loginLogDao"/>
</bean> … </beans>指定了Spring配置文件所在的位置,AbstractDependencyInjectionSpringContextTests將使用這些配置文件初始化好Spring容器,并將它們保存于static的緩存中。然后馬上著(zhù)手根據類(lèi)型匹配機制(byType),自動(dòng)將Spring容器中匹配測試類(lèi)屬性的Bean通過(guò)Setter注入到測試類(lèi)中。
他覆蓋了父類(lèi)的prepareTestInstance方法,在準備好容器之后,馬上注入依賴(lài):
protected void prepareTestInstance() throws Exception {
injectDependencies();
}
protected void injectDependencies() throws Exception {
if (isPopulateProtectedVariables()) {
if (this.managedVariableNames == null) {
initManagedVariableNames();
}
populateProtectedVariables();
}
else if (getAutowireMode() != AUTOWIRE_NO) {
getApplicationContext().getBeanFactory()
.autowireBeanProperties( this,
getAutowireMode(), isDependencyCheck());
}
}
自動(dòng)裝配的問(wèn)題
采用這種byType的自動(dòng)裝配方式,有時(shí)候會(huì )帶來(lái)一些問(wèn)題,當容器中擁有多個(gè)匹配類(lèi)型的Bean的時(shí)候,就會(huì )拋出UnsatisfiedDependencyException異常。假設我們采用以下傳統的事務(wù)管理的配置方式對UserService進(jìn)行配置,按類(lèi)型匹配的自動(dòng)裝配機制就會(huì )引發(fā)問(wèn)題:
①用于被代理的目標Bean,按類(lèi)型匹配于UserService
<bean id="userServiceTarget" class="com.baobaotao.service.UserServiceImpl">
<property name="userDao" ref="userDao" />
<property name="loginLogDao" ref="loginLogDao"></property>
</bean>
②通過(guò)事務(wù)代理工廠(chǎng)為UserServiceImpl創(chuàng )建的代理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都按類(lèi)型匹配于UserService,在對DependencyInjectionCtxTest的userService屬性進(jìn)行自動(dòng)裝配將會(huì )引發(fā)問(wèn)題。有兩種針對該問(wèn)題的解決辦法:
1.調整配置文件,使按類(lèi)型匹配于UserService的Bean僅有一個(gè),具體有以下兩個(gè)方法:
2.改變DependencyInjectionCtxTest的自動(dòng)裝配機制:Spring默認使用byType類(lèi)型的自動(dòng)裝配機制,但它允許你通過(guò)setAutowireMode()的方法改變默認自動(dòng)裝配的機制,比如你可以調用setAutowireMode(AUTOWIRE_BY_NAME)方法啟用按名稱(chēng)匹配的自動(dòng)裝配機制。
AbstractDependencyInjectionSpringContextTests定義了三個(gè)代表自動(dòng)裝配機制類(lèi)型的常量,分別說(shuō)明如下:
依賴(lài)檢查
假設我們在DependencyInjectionCtxTest添加一個(gè)User類(lèi)型的屬性并提供Setter方法,而Spring容器中沒(méi)有匹配該屬性的Bean。在默認情況下, AbstractDependencyInjectionSpringContextTests要求所有屬性都能在Spring容器中找到對應Bean,否則拋出異常。
仔細思考一下,這種運行機制并非沒(méi)有道理,因為既然你已經(jīng)提供了Setter方法,就相當于給出了這樣的暗示信息:“這個(gè)屬性測試類(lèi)自身創(chuàng )建不了,必須由外部提供”。而在使用自動(dòng)裝配機制的情況下,測試類(lèi)屬性自動(dòng)從Spring容器中注入匹配的屬性,一般情況下不會(huì )手工去調用Setter方法準備屬性。
如果你出于一些特殊的理由,希望在采用自動(dòng)裝配的情況下,如果有屬性未得到裝配也不在乎,那么你可以在測試類(lèi)構造函數中調用setDependencyCheck(false)方法達到目的:
package com.baobaotao.test;
…
public class DependencyInjectionCtxTest extends AbstractDependencyInjectionSpringContextTests {
public DependencyInjectionCtxTest(){
setDependencyCheck(false); ①告知不進(jìn)行屬性依賴(lài)性檢查
}
…
}
不寫(xiě)setter方法的注入
大部分IDE都有了為字段生成setter的方式,所以寫(xiě)setter方法其實(shí)沒(méi)什么必要,Spring提供了這種注入方式,本質(zhì)上是通過(guò)反射去獲得Field的。只需要在構造函數中調用:
將需要自動(dòng)裝配的屬性變量聲明為protected;
setPopulateProtectedVariables(true)方法(聯(lián)系上面的源碼)。
可能大家對第一點(diǎn)比較奇怪,看一下里面調用的一個(gè)方法就知道了:
private void initManagedVariableNames() throws IllegalAccessException {
LinkedList managedVarNames = new LinkedList();
Class clazz = getClass();
do {
Field[] fields = clazz.getDeclaredFields();
……….
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
….
if (isProtectedInstanceField(field)) {
…..
}
}
clazz = clazz.getSuperclass();
}
while (!clazz.equals(AbstractDependencyInjectionSpringContextTests.class));
this.managedVariableNames = (String[]) managedVarNames.toArray(new String[managedVarNames.size()]);
}
private boolean isProtectedInstanceField(Field field) {
int modifiers = field.getModifiers();
return !Modifier.isStatic(modifiers) &&
Modifier.isProtected(modifiers);
}
AbstractTransactionalSpringContextTests
繼承了上面一個(gè)類(lèi),他的作用就是能夠讓事務(wù)回滾。UserService以下兩個(gè)接口方法會(huì )對數據庫執行更改操作:
void loginSuccess(User user);
void registerUser(User user);
當我們對這兩個(gè)接口方法進(jìn)行測試時(shí),它們將會(huì )在數據庫中產(chǎn)生持久化數據??紤]對registerUser(User user)方法進(jìn)行測試時(shí),我們可能編寫(xiě)如下所示的測試方法:
public void testRegisterUser(){
User user = new User();
user.setUserId(2);
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
}
當第一次成功運行testRegisterUser()測試方法時(shí),將在數據庫中產(chǎn)生一條主鍵為2的記錄,如何第二次重新運行testRegisterUser()測試方法其結果將不言自明:因主鍵沖突導致測試方法執行失敗,最終報告測試用例沒(méi)有通過(guò)。在這種情況下,測試用例未通過(guò)并不是因為UserServiceImpl#registerUser(User user)存在邏輯錯誤,而是因為測試方法的積累效應導致外在設施的現場(chǎng)發(fā)生變化而引起的問(wèn)題。
AbstractTransactionalSpringContextTests專(zhuān)為解決以上問(wèn)題而生,也就是說(shuō)前面我們所提個(gè)問(wèn)題在此得到了回答。只要繼承該類(lèi)創(chuàng )建測試用例,在默認情況下,測試方法中所包含的事務(wù)性數據操作都會(huì )在測試方法返回前被回滾。由于事務(wù)回滾操作發(fā)生在測試方法返回前的點(diǎn)上,所以你可以象往常一樣在測試方法體中對數據操作的正確性進(jìn)行校驗。
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(){
① 測試方法中的數據操作將在方法返回前被回滾,不會(huì )對數據庫
User user = new User(); 產(chǎn)生永久性數據操作,第二次運行該測試方法時(shí),依舊可以
user.setUserId(2); 成功運行。
user.setUserName("john");
user.setPassword("123456");
userService.registerUser(user);
User user1 = userService.findUserByUserName("john"); ②對數據操作進(jìn)行
assertEquals(user.getUserId(), user1.getUserId()); 正確性檢驗
}
}
如果testRegisterUser()是直接繼承于AbstractDependencyInjectionSpringContextTests類(lèi)的測試方法,則重復運行該測試方法就會(huì )發(fā)生數據沖突問(wèn)題。但因為它位于繼承于AbstractTransactionalSpringContextTests的測試用例類(lèi)中,測試方法中對數據庫的操作會(huì )被正確回滾,所以重復運行不會(huì )有任何問(wèn)題。
如果你確實(shí)希望測試方法中對數據庫的操作持久生效而不是被回滾,Spring也可以滿(mǎn)足你的要求,你僅需要在測試方法中添加setComplete()方法就可以了。
public void testRegisterUser(){
…
User user1 = userService.findUserByUserName("john");
assertEquals(user.getUserId(), user1.getUserId());
setComplete(); ①測試方法中的事務(wù)性數據操作將被提交
}
AbstractTransactionalSpringContextTests還擁有幾個(gè)可用于初始化測試數據庫,并在測試完成后清除測試數據的方法,分別介紹如下:
AbstractTransactionalSpringContextTests另外還提供了一組用于測試延遲數據加載的方法:endTransaction()/startNewTransaction()。我在測試Hibernate、JPA等允許延遲數據加載的應用時(shí),如何模擬數據在Service層事務(wù)中被部分加載,當傳遞到Web層時(shí)重新打開(kāi)事務(wù)完成延遲部分數據加載的測試場(chǎng)景呢?這兩個(gè)方法即為此用途而生:你可以在測試方法中顯式調用endTransaction()方法以模擬從Service層中獲取部分數據后返回,爾后,再通過(guò)startNewTransaction()開(kāi)啟一個(gè)和原事務(wù)無(wú)關(guān)新事務(wù)——模擬在Web層中重新打開(kāi)事務(wù),接下來(lái)你就可以訪(fǎng)問(wèn)延遲加載的數據,看是否一切如期所料了。
在代碼清單 6的②處,我們通過(guò)UserService#findUserByUserName()方法對前面registerUser(user)方法數據操作的正確性進(jìn)行檢驗。應該說(shuō),我們非常幸運,因為在UserService中剛好存在一個(gè)可用于檢測registerUser(user)數據操作正確性的方法。讓我們考慮另外的一種情況:要是 UserService不存在這樣的方法,我們該如何檢測registerUser(user)數據操作結果的正確性呢?顯然我們不能使用肉眼觀(guān)察的方法,那難道為了驗證數據操作正確性專(zhuān)門(mén)編寫(xiě)一個(gè)配合性的數據訪(fǎng)問(wèn)類(lèi)不成?
效驗結果正確性AbstractTransactionalSpringContextTests
它添加了一個(gè)JdbcTemplate,你可以借由此道快意直達數據庫。它自動(dòng)使用Spring容器中的數據源(DataSource)創(chuàng )建好一個(gè)JdbcTemplate實(shí)例并開(kāi)放給子類(lèi)使用。值得注意的是,如果你采用byName自動(dòng)裝配機制,數據源Bean的名稱(chēng)必須取名為“dataSource”。
import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests;
…
public class UserServiceIntegrateWithJdbcTest
extends AbstractTransactionalDataSourceSpringContextTests {
① 注意:繼承類(lèi)發(fā)生調整
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訪(fǎng)問(wèn)數據庫了
assertEquals(user.getUserId(), userId);
setComplete();
}
}
jdbcTemplate是AbstractTransactionalDataSourceSpringContextTests類(lèi)中定義的,子類(lèi)可以直接使用它訪(fǎng)問(wèn)數據庫。這樣我們就可以靈活地訪(fǎng)問(wèn)數據庫以檢驗目標測試方法的數據操作正確性。至此,我們終于畢其功于一役于AbstractTransactionalDataSourceSpringContextTests,順利解決前面我們中指出的最后題。
只要你通過(guò)擴展AbstractTransactionalSpringContextTests及其子類(lèi)創(chuàng )建測試用例,所有測試方法都會(huì )工作了事務(wù)環(huán)境下。也就是說(shuō),即使某些測試方法不需要訪(fǎng)問(wèn)數據庫,也會(huì )產(chǎn)生額外的事務(wù)管理開(kāi)銷(xiāo),是否可以對測試方法啟用事務(wù)管理的行為進(jìn)行控制呢?此外,在一些情況下,除對目標方法邏輯運行的正確性進(jìn)行檢驗外,我們還希望對目標方法的運行性能進(jìn)行測試:如當目標方法運行時(shí)間超過(guò)200毫秒時(shí),則測試用例視為未通過(guò)。諸如此類(lèi)的問(wèn)題,我們目前學(xué)習到的知識還不能很好的應付。Spring 2.0新增了注解驅動(dòng)的測試工具為我們指明了道路,你僅需要通過(guò)簡(jiǎn)單為測試方法標注注解,我們剛才提出的“疑難”問(wèn)題就可以迎刃而解了。
@Test(timeout = 1000)
http://www.uml.org.cn/j2ee/200905074.asp
詳細講解在Spring中進(jìn)行集成測試
聯(lián)系客服