JUnit與Spring的整合——JUnit的TestCase如何自動注入Spring容器託管的物件
問題
在Java中,一般使用JUnit作為單元測試框架,測試的物件一般是Service和DAO,也可能是RemoteService和Controller。所有這些測試物件基本都是Spring託管的,不會直接new出來。而每個TestCase類卻是由JUnit建立的。如何在每個TestCase例項中注入這些依賴呢?
預期效果
我們希望能夠達到這樣的效果:
package me.arganzheng.study;importstatic org.junit.Assert.*;import org.junit.Test;import org.springframework.beans.factory .annotation.Autowired;/**
* @author arganzheng
*/publicclassFooServiceTest{@AutowiredprivateFooService fooService;@Testpublicvoid testSaveFoo(){Foo foo =newFoo();// ...long id = fooService.saveFoo(foo);
assertTrue(id >0);}}
解決思路
其實在我前面的文章:Quartz與Spring的整合-Quartz中的job如何自動注入spring容器託管的物件,已經詳細的討論過這個問題了。Quartz是一個框架,Junit同樣是個框架,Spring對於接入外部框架,採用了非常一致的做法。對於依賴注入,不外乎就是這個步驟:
-
首先,找到外部框架建立例項的地方(類或者介面),比如Quartz的jobFactory,預設為
org.quartz.simpl.SimpleJobFactory
,也可以配置為org.quartz.simpl.PropertySettingJobFactory
。這兩個類都是實現了org.quartz.spi.JobFactory
介面。對於JUnit4.5+,則是org.junit.runners.BlockJUnit4ClassRunner
類中的createTest
方法。/** * Returns a new fixture for running a test. Default implementation executes * the test class's no-argument constructor (validation should have ensured * one exists). */
-
繼承或者組合這些框架類,如果需要使用他們封裝的一些方法的話。如果這些類是有實現介面的,那麼也可以直接實現介面,與他們並行。然後對創建出來的物件進行依賴注入。
比如在Quartz中,Spring採用的是直接實現org.quartz.spi.JobFactory
介面的方式:
publicclassSpringBeanJobFactoryextendsAdaptableJobFactoryimplementsSchedulerContextAware{...}publicclassAdaptableJobFactoryimplementsJobFactory{...}
但是Spring提供的org.springframework.scheduling.quartz.SpringBeanJobFactory
並沒有自動依賴注入,它其實也是簡單的根據job類名直接建立類:
/**
* Create an instance of the specified job class.
* <p>Can be overridden to post-process the job instance.
* @param bundle the TriggerFiredBundle from which the JobDetail
* and other info relating to the trigger firing can be obtained
* @return the job instance
* @throws Exception if job instantiation failed
*/protectedObject createJobInstance(TriggerFiredBundle bundle)throwsException{return bundle.getJobDetail().getJobClass().newInstance();}
不過正如它註釋所說的,Can be overridden to post-process the job instance
,我們的做法也正是繼承了org.springframework.scheduling.quartz.SpringBeanJobFactory
,然後覆蓋它的這個方法:
publicclassOurSpringBeanJobFactoryextends org.springframework.scheduling.quartz.SpringBeanJobFactory{@AutowireprivateAutowireCapableBeanFactory beanFactory;/**
* 這裡我們覆蓋了super的createJobInstance方法,對其創建出來的類再進行autowire。
*/@OverrideprotectedObject createJobInstance(TriggerFiredBundle bundle)throwsException{Object jobInstance =super.createJobInstance(bundle);
beanFactory.autowireBean(jobInstance);return jobInstance;}}
由於OurSpringBeanJobFactory
是配置在Spring容器中,預設就具備拿到ApplicationContext的能力。當然就可以做ApplicationContext能夠做的任何事情。
題外話
這裡體現了框架設計一個很重要的原則:開閉原則——針對修改關閉,針對擴充套件開放。除非是bug,否者框架的原始碼不會直接拿來修改,但是對於功能性的個性化需求,框架應該允許使用者進行擴充套件。這也是為什麼所有的框架基本都是面向介面和多型實現的,並且支援應用通過配置項註冊自定義實現類,比如Quartz的`org.quartz.scheduler.jobFactory.class`和`org.quartz.scheduler.instanceIdGenerator.class`配置項。
解決方案
回到JUnit,其實也是如此。
Junit4.5+是通過org.junit.runners.BlockJUnit4ClassRunner
中的createTest
方法來建立單元測試類物件的。
/**
* Returns a new fixture for running a test. Default implementation executes
* the test class's no-argument constructor (validation should have ensured
* one exists).
*/protectedObject createTest()throwsException{return getTestClass().getOnlyConstructor().newInstance();}
那麼根據前面的討論,我們只要extendsorg.junit.runners.BlockJUnit4ClassRunner
類,覆蓋它的createTest
方法就可以了。如果我們的這個類能夠方便的拿到ApplicationContext(這個其實很簡單,比如使用ClassPathXmlApplicationContext
),那麼就可以很方便的實現依賴注入功能了。JUnit沒有專門定義建立UT例項的介面,但是它提供了@RunWith
的註解,可以讓我們指定我們自定義的ClassRunner。於是,解決方案就出來了。
Spring內建的解決方案
Spring3提供了SpringJUnit4ClassRunner
基類讓我們可以很方便的接入JUnit4。
publicclass org.springframework.test.context.junit4.SpringJUnit4ClassRunnerextends org.junit.runners.BlockJUnit4ClassRunner{...}
思路跟我們上面討論的一樣,不過它採用了更靈活的設計:
- 引入Spring TestContext Framework,允許接入不同的UT框架(如JUnit3.8-,JUnit4.5+,TestNG,etc.).
- 相對於ApplicationContextAware介面,它允許指定要載入的配置檔案位置,實現更細粒度的控制,同時快取application context per Test Feature。這個是通過
@ContextConfiguration
註解暴露給使用者的。(其實由於SpringJUnit4ClassRunner
是由JUnit建立而不是Spring建立的,所以這裡ApplicationContextAware should not work。但是筆者發現AbstractJUnit38SpringContextTests
是實現ApplicationContextAware
介面的,但是其ApplicationContext又是通過org.springframework.test.context.support.DependencyInjectionTestExecutionListener
載入的。感覺實在沒有必要實現ApplicationContextAware
介面。) - 基於事件監聽機制(the listener-based test context framework),並且允許使用者自定義事件監聽器,通過
@TestExecutionListeners
註解註冊。預設是org.springframework.test.context.support.DependencyInjectionTestExecutionListener
、org.springframework.test.context.support.DirtiesContextTestExecutionListener
和org.springframework.test.context.transaction.TransactionalTestExecutionListener
這三個事件監聽器。
其中依賴注入就是在org.springframework.test.context.support.DependencyInjectionTestExecutionListener
完成的:
/**
* Performs dependency injection and bean initialization for the supplied
* {@link TestContext} as described in
* {@link #prepareTestInstance(TestContext) prepareTestInstance()}.
* <p>The {@link #REINJECT_DEPENDENCIES_ATTRIBUTE} will be subsequently removed
* from the test context, regardless of its value.
* @param testContext the test context for which dependency injection should
* be performed (never <code>null</code>)
* @throws Exception allows any exception to propagate
* @see #prepareTestInstance(TestContext)
* @see #beforeTestMethod(TestContext)
*/protectedvoid injectDependencies(finalTestContext testContext)throwsException{Object bean = testContext.getTestInstance();AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
beanFactory.autowireBeanProperties(bean,AutowireCapableBeanFactory.AUTOWIRE_NO,false);
beanFactory.initializeBean(bean, testContext.getTestClass().getName());
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);}
這裡面ApplicationContext在Test類建立的時候就已經根據@ContextLocation標註的位置載入存放到TestContext中了:
/**
* TestContext encapsulates the context in which a test is executed, agnostic of
* the actual testing framework in use.
*
* @author Sam Brannen
* @author Juergen Hoeller
* @since 2.5
*/publicclassTestContextextendsAttributeAccessorSupport{TestContext(Class<?> testClass,ContextCache contextCache,String defaultContextLoaderClassName){...if(!StringUtils.hasText(defaultContextLoaderClassName)){
defaultContextLoaderClassName = STANDARD_DEFAULT_CONTEXT_LOADER_CLASS_NAME;}ContextConfiguration contextConfiguration = testClass.getAnnotation(ContextConfiguration.class);String[] locations =null;ContextLoader contextLoader =null;...Class<?extendsContextLoader> contextLoaderClass = retrieveContextLoaderClass(testClass,
defaultContextLoaderClassName);
contextLoader =(ContextLoader)BeanUtils.instantiateClass(contextLoaderClass);
locations = retrieveContextLocations(contextLoader, testClass);this.testClass = testClass;this.contextCache = contextCache;this.contextLoader = contextLoader;this.locations = locations;}}
說明 :
- JUnit3.8:package
org.springframework.test.context.junit38
AbstractJUnit38SpringContextTests
- applicationContext
AbstractTransactionalJUnit38SpringContextTests
- applicationContext
- simpleJdbcTemplate
- JUnit4.5:package
org.springframework.test.context.junit4
AbstractJUnit4SpringContextTests
- applicationContext
AbstractTransactionalJUnit4SpringContextTests
- applicationContext
- simpleJdbcTemplate
- Custom JUnit 4.5 Runner:
SpringJUnit4ClassRunner
- @Runwith
- @ContextConfiguration
- @TestExecutionListeners
- TestNG: package
org.springframework.test.context.testng
AbstractTestNGSpringContextTests
- applicationContext
AbstractTransactionalTestNGSpringContextTests
- applicationContext
- simpleJdbcTemplate
補充:對於JUnit3,Spring2.x原來提供了三種接入方式:
- AbstractDependencyInjectionSpringContextTests
- AbstractTransactionalSpringContextTests
- AbstractTransactionalDataSourceSpringContextTests
不過從Spring3.0開始,這些了類都被org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests
和AbstractTransactionalJUnit38SpringContextTests
取代了:
@deprecated as of Spring 3.0, in favor of using the listener-based test context framework(不過由於JUnit3.x不支援
beforeTestClass
和afterTestClass
,所以這兩個事件是無法監聽的。)({@link org.springframework.test.context.junit38.AbstractJUnit38SpringContextTests})
採用Spring3.x提供的SpringJUnit4ClassRunner接入方式,我們可以這樣寫我們的UT:
package me.arganzheng.study;importstatic org.junit.Assert.*;import org.junit.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/**
* @author arganzheng
*/@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath:conf-spring/spring-dao.xml","classpath:conf-spring/spring-service.xml","classpath:conf-spring/spring-controller.xml"})publicclassFooServiceTest{@AutowiredprivateFooService fooService;@Testpublicvoid testSaveFoo(){Foo foo =newFoo();// ...long id = fooService.saveFoo(foo);
assertTrue(id >0);}}
當然,每個UT類都要配置這麼多anotation配置是很不方便的,搞成一個基類會好很多:
ackage me.arganzheng.study;import org.junit.runner.RunWith;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import org.springframework.transaction.annotation.Transactional;/**
* @author arganzheng
*/@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath:conf-spring/spring-dao.xml","classpath:conf-spring/spring-service.xml","classpath:conf-spring/spring-controller.xml"})@TransactionalpublicclassBaseSpringTestCase{}
然後我們的FooServiceTest就可以簡化為:
package me.arganzheng.study;importstatic org.junit.Assert.*;import org.junit.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.annotation.Rollback;/**
* @author arganzheng
*/publicclassFooServiceTestextendsBaseSpringTestCase{@AutowiredprivateFooService fooService;@Test// @Rollback(true) 預設就是truepublicvoid testSaveFoo(){Foo foo =newFoo();// ...long id = fooService.saveFoo(foo);
assertTrue(id >0);}}
單元測試的其他問題
上面只是簡單解決了依賴注入問題,其實單元測試還有很多。如
- 事務管理
- Mock掉外界依賴
- web層測試
- 介面測試
- 靜態和私有方法測試
- 測試資料準備和結果驗證
等等。
--EOF--
原文地址:http://ju.outofmemory.cn/entry/75778