項目中Spring注入報錯小結


之前在做單元測試時采用注解方式進行service對象的注入,但運行測試用例時對象要注入的service對象總是空的,檢查下spring配置文件,我要配置的bean類xml文件已經包含到spring要加載的配置文件中,並且相關寫法跟同事另一個方法完全相同,但就是運行運行注入不成功,一時很郁悶上網搜了下spring注入不成功可能的問題或調試定位的方法,沒找到頭緒,后來問同事才知道因為我寫的單元測試類沒有繼承一個BaseTestSuit類,我按他說的繼承下果然就成功注入了,一方面為自己的不小心自責,另一個方面也意識到自己之所以被該問題難住主要還是對Spring的注入知識,Junit單元測試的原理知識不熟悉,自己之前處於不知道自己不知道的初級階段。看了下面的文章我終於知道原因了,大意時通過類繼承或組合的方式實現兩個JUnit和Spring 兩個框架的整合。

下文轉自:http://blog.arganzheng.me/posts/junit-and-spring-integration-ioc-autowire.html

問題

在Java中,一般使用JUnit作為單元測試框架,測試的對象一般是Service和DAO,也可能是RemoteService和Controller。所有這些測試對象基本都是Spring托管的,不會直接new出來。而每個TestCase類卻是由JUnit創建的。如何在每個TestCase實例中注入這些依賴呢?

預期效果

我們希望能夠達到這樣的效果:

package me.arganzheng.study;  import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired;  /**  * @author arganzheng  */ public class FooServiceTest{      @Autowired     private FooService fooService;      @Test     public void testSaveFoo() {         Foo foo = new Foo();         // ...         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).   */  protected Object createTest() throws Exception {      return getTestClass().getOnlyConstructor().newInstance();  } 
  2. 繼承或者組合這些框架類,如果需要使用他們封裝的一些方法的話。如果這些類是有實現接口的,那么也可以直接實現接口,與他們並行。然后對創建出來的對象進行依賴注入。

比如在Quartz中,Spring采用的是直接實現org.quartz.spi.JobFactory接口的方式:

    public class SpringBeanJobFactory extends AdaptableJobFactory implements SchedulerContextAware {         ...     }      public class AdaptableJobFactory implements JobFactory {         ...     } 

但是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      */     protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {         return bundle.getJobDetail().getJobClass().newInstance();     } 

不過正如它注釋所說的,Can be overridden to post-process the job instance,我們的做法也正是繼承了org.springframework.scheduling.quartz.SpringBeanJobFactory,然后覆蓋它的這個方法:

public class OurSpringBeanJobFactory extends org.springframework.scheduling.quartz.SpringBeanJobFactory{     @Autowire     private AutowireCapableBeanFactory beanFactory;      /**      * 這里我們覆蓋了super的createJobInstance方法,對其創建出來的類再進行autowire。      */     @Override     protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {         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).  */ protected Object createTest() throws Exception {    return getTestClass().getOnlyConstructor().newInstance(); } 

那么根據前面的討論,我們只要extendsorg.junit.runners.BlockJUnit4ClassRunner類,覆蓋它的createTest方法就可以了。如果我們的這個類能夠方便的拿到ApplicationContext(這個其實很簡單,比如使用ClassPathXmlApplicationContext),那么就可以很方便的實現依賴注入功能了。JUnit沒有專門定義創建UT實例的接口,但是它提供了@RunWith的注解,可以讓我們指定我們自定義的ClassRunner。於是,解決方案就出來了。

Spring內建的解決方案

Spring3提供了SpringJUnit4ClassRunner基類讓我們可以很方便的接入JUnit4。

public class org.springframework.test.context.junit4.SpringJUnit4ClassRunner extends 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)  */ protected void injectDependencies(final TestContext testContext) throws Exception {     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  */ public class TestContext extends AttributeAccessorSupport {      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<? extends ContextLoader> 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;     } } 

說明 :

Spring3使用了Spring TestContext Framework框架,支持多種接入方式:10.3.5.5 TestContext support classes。非常不錯的官方文檔,強烈推薦閱讀。簡單概括如下:

  • 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;  import static 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" }) public class FooServiceTest{      @Autowired     private FooService fooService;      @Test     public void testSaveFoo() {         Foo foo = new Foo();         // ...         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" }) @Transactional public class BaseSpringTestCase{  } 

然后我們的FooServiceTest就可以簡化為:

package me.arganzheng.study;  import static org.junit.Assert.*; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.Rollback;  /**   * @author arganzheng  */ public class FooServiceTest extends BaseSpringTestCase{      @Autowired     private FooService fooService;      @Test     // @Rollback(true) 默認就是true     public void testSaveFoo() {         Foo foo = new Foo();         // ...         long id = fooService.saveFoo(foo);          assertTrue(id > 0);     } } 

單元測試的其他問題

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

  1. 事務管理
  2. Mock掉外界依賴
  3. web層測試
  4. 接口測試
  5. 靜態和私有方法測試
  6. 測試數據准備和驗證

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM