之前在做单元测试时采用注解方式进行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. 测试数据准备和验证

项目中Spring注入报错小结的更多相关文章

  1. 解决Maven项目中的无故报错的方法

    解决Eclipse+maven中的无故报错 错误: One or more constraints have not been satisfied. Deployment Assembly跟java版 ...

  2. 项目中访问controller报错:HTTP Status 500 - Servlet.init() for servlet spring threw exception

    直接访问controller路径http://localhost:8080/index报错: HTTP Status 500 - Servlet.init() for servlet spring t ...

  3. RN项目中使用react-native-elements报错: Unrecognized font family 'Material Icons'

    查询了一些方案,但各自的环境不尽相同,最后在google中找到了答案.主要问题在于 (1)版本问题 (2)Xcode配置问题 报错如下 解决步骤: 1 . 首先需要正确安装 npm i -S reac ...

  4. Springboot项目中Pom.xml报错

    摘要:使用idea,两次在maven上浪费时间,第一次不知道怎么就解决了,第二次记录一下解决办法 参考博客地址: https://blog.csdn.net/u013129944/article/de ...

  5. Struts2项目中使用Ajax报错

    在Struts2项目中使用Ajax向后台请求数据,当添加了json-lib-2.3-jdk15.jar和struts2-json-plugin-2.3.4.1.jar两个包时,在result中配置ty ...

  6. 在maven项目中引用ueditor报错问题

    遇到的问题:将pom.xml中引入 <dependency> <groupId>com.baidu</groupId> <artifactId>uedi ...

  7. 新建vue项目中遇到的报错信息

    在npm install的时候会报错,经过上网查阅资料之后,解决方法如下: 0.先升级npm版本:npm install -g npm   有可能是npm版本过低报错 1.然后清理缓存: npm ca ...

  8. 项目中使用mybatis报错:对实体 "serverTimezone" 的引用必须以 ';' 分隔符结尾。

    报错信息如下: Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: ### ...

  9. node升级后,项目中的node-sass报错的问题

    之前可能因为电脑不知道哪抽风了,在npm build的时候进入就卡在入口的地方,启动express的时候也会,所以就重装了一下node 重装node 其实也不是重装,就是使用 where node 查 ...

随机推荐

  1. smarty模板执行原理

    为了实现程序的业务逻辑和内容表现页面的分离从而提高开发速度,php 引入了模板引擎的概念,php 模板引擎里面最流行的可以说是smarty了,smarty因其功能强大而且速度快而被广大php web开 ...

  2. Linux usb子系统(一):子系统架构

    一.USB协议基础知识   前序:USB概念概述 USB1.0版本速度1.5Mbps(低速USB) USB1.1版本速度12Mbps(全速USB)  USB2.0版本速度480Mbps(高速USB). ...

  3. java BingInteger生成2进制String循环移位时长度自动缩减

    最近在做文本处理,使用MD5 生成一段文字的MD5哈希长度为32位也即128个0-1序列. 由于需要对这个MD5值进行循环移位,显然普通的  int 是不行的,所以使用 BigInteger.但是在使 ...

  4. WEB服务器1--开篇

    WEB服务器 web服务器一般指网站服务器,是指驻留于因特网上某种类型计算机的程序,可以向浏览器等Web客户端提供文档,也可以放置网站文件,让全世界浏览:可以放置数据文件,让全世界下载.目前最主流的三 ...

  5. 自由缩放属性-resize(禁止textarea的自由缩放尺寸功能)

    resize:none|both|horizontal|vertical|inherit 可以用来改变元素的尺寸大小 用上这个属性后元素的有下脚会有一个小图标 拖它就可以了 但是我把resize:bo ...

  6. 2个或多个datable类似于sql inner join 合并查询

    public static DataTable DatableInnerJoin(DataTable FirstTB, DataTable SecondTB, DataColumn[] FJC, Da ...

  7. ServiceAccount 枚举

    指定服务的安全上下文,安全上下文定义其登录类型. 命名空间:  System.ServiceProcess程序集:  System.ServiceProcess(在 System.ServicePro ...

  8. 如何安装,卸载eclipse中的ADT,或者更新ADT

    昨天手贱把Android的SDK升级了,然后搞到ADT的版本不对应,然后从网上搜了一些办法~效果还可以,重新安装了一次ADT. 卸载ADT的方法,方法如下(我的Eclipse版本为3.5): 1.选择 ...

  9. Android ---------- 清单文件中Activity常规设置

    <activity android:name="xxxxx" android:alwaysRetainTaskState="true" android:c ...

  10. Windows7 无法打开ASA SSL VPN和ASDM首页

    原文地址:Windows7 无法打开ASA SSL VPN 首页和无法打开 ASDM GUI 页面作者:futhy              windows 7 无法打开ASA SSL VPN 和AS ...