之前在做单元测试时采用注解方式进行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. java设计模式--结构型模式--代理模式

    代理模式 概述 为其他对象提供一种代理以控制对这个对象的访问. 适用性 1.远程代理(RemoteProxy)为一个对象在不同的地址空间提供局部代表. 2.虚代理(VirtualProxy)根据需要创 ...

  2. 如何允许外网可以连接mysql数据库

    1.首先检查mysql所在服务器的防火墙,如果限制了外网对3306端口的连接,那么放开限制Linux服务器中执行 iptables -L   可以查看当前的防火墙规则iptables -F   可以清 ...

  3. Linux如何生成列表

    如何生成列表: 方法一:{1..100} 方法二:`seq [起始数 [步进长度]] 结束数` 1,...,100 declare -i SUM=0    integer    -x

  4. BackgroundWorker用法

    BackgroundWorker主要用来提供后台运算服务(防止用户前台无响应等待),并提供服务进度的类: 代码如下: BackgroundWorker bgw = new BackgroundWork ...

  5. 怎样实现IOS开发中的数据存储方式

    iOS 开发中,一般有如下几种数据存储方式.需要根据具体的业务场景,选择 合适的数据存储方式. (1)  用户默认设置 – 这种情况通常不需要用户干预,如游戏通关信息,Video 播放记录,或者 Ap ...

  6. Oracle 学习笔记 19 -- 触发器和包浅析(PL/SQL)

    触发器是存放在数据库中的一种特殊类型的子程序.不能被用户直接调用,而是当特定事件或操作发生时由系统自己主动 调用执行.触发器不能接受參数.所以执行触发器就叫做触发或点火.Oracle事件指的是数据库的 ...

  7. [Cycle.js] Hyperscript as our alternative to template languages

    Usually we use template languages like Handlebars, JSX, and Jade to create. One simple way we can cr ...

  8. Codeforces 466 E. Information Graph

    并查集.... E. Information Graph time limit per test 1 second memory limit per test 512 megabytes input ...

  9. iOS经常使用的加密算法

    在iOS开发中,为了数据的安全经常对内容进行加密,在这儿我们对经常使用的加密算法进行了总结: 1.MD5 <span style="font-size:18px;">+ ...

  10. Chrome 开发者工具详解(2):Network 面板

    面板上包含了Elements面板.Console面板.Sources面板.Network面板. Timeline面板.Profiles面板.Application面板.Security面板.Audit ...