1. 程式人生 > >JUnit與Spring的整合——JUnit的TestCase如何自動注入Spring容器託管的物件

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對於接入外部框架,採用了非常一致的做法。對於依賴注入,不外乎就是這個步驟:

  1. 首先,找到外部框架建立例項的地方(類或者介面),比如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).
      */
    protectedObject createTest()throwsException{return getTestClass().getOnlyConstructor().newInstance();}
  2. 繼承或者組合這些框架類,如果需要使用他們封裝的一些方法的話。如果這些類是有實現介面的,那麼也可以直接實現介面,與他們並行。然後對創建出來的物件進行依賴注入。

比如在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{...}

思路跟我們上面討論的一樣,不過它採用了更靈活的設計:

  1. 引入Spring TestContext Framework,允許接入不同的UT框架(如JUnit3.8-,JUnit4.5+,TestNG,etc.).
  2. 相對於ApplicationContextAware介面,它允許指定要載入的配置檔案位置,實現更細粒度的控制,同時快取application context per Test Feature。這個是通過@ContextConfiguration註解暴露給使用者的。(其實由於SpringJUnit4ClassRunner是由JUnit建立而不是Spring建立的,所以這裡ApplicationContextAware should not work。但是筆者發現AbstractJUnit38SpringContextTests是實現ApplicationContextAware介面的,但是其ApplicationContext又是通過org.springframework.test.context.support.DependencyInjectionTestExecutionListener載入的。感覺實在沒有必要實現ApplicationContextAware介面。)
  3. 基於事件監聽機制(the listener-based test context framework),並且允許使用者自定義事件監聽器,通過@TestExecutionListeners註解註冊。預設是org.springframework.test.context.support.DependencyInjectionTestExecutionListenerorg.springframework.test.context.support.DirtiesContextTestExecutionListenerorg.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.AbstractJUnit38SpringContextTestsAbstractTransactionalJUnit38SpringContextTests取代了:

@deprecated as of Spring 3.0, in favor of using the listener-based test context framework(不過由於JUnit3.x不支援beforeTestClassafterTestClass,所以這兩個事件是無法監聽的。)

({@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);}}

單元測試的其他問題

上面只是簡單解決了依賴注入問題,其實單元測試還有很多。如

  1. 事務管理
  2. Mock掉外界依賴
  3. web層測試
  4. 介面測試
  5. 靜態和私有方法測試
  6. 測試資料準備和結果驗證

等等。

--EOF--

原文地址:http://ju.outofmemory.cn/entry/75778