JUnit与Spring的整合——JUnit的TestCase如何自动注入Spring容器托管的对象

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: JUnit与Spring的整合——JUnit的TestCase如何自动注入Spring容器托管的对象 问题 在Java中,一般使用JUnit作为单元测试框架,测试的对象一般是Service和DAO,也可能是RemoteService和Controller。

JUnit与Spring的整合——JUnit的TestCase如何自动注入Spring容器托管的对象

问题

在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. 测试数据准备和结果验证

等等。

--EOF--

原文地址http://www.bieryun.com/2033.html

相关文章
|
27天前
|
Java Spring
在使用Spring的`@Value`注解注入属性值时,有一些特殊字符需要注意
【10月更文挑战第9天】在使用Spring的`@Value`注解注入属性值时,需注意一些特殊字符的正确处理方法,包括空格、引号、反斜杠、新行、制表符、逗号、大括号、$、百分号及其他特殊字符。通过适当包裹或转义,确保这些字符能被正确解析和注入。
|
1月前
|
Java 测试技术 程序员
为什么Spring不推荐@Autowired用于字段注入?
作为Java程序员,Spring框架在日常开发中使用频繁,其依赖注入机制带来了极大的便利。然而,尽管@Autowired注解简化了依赖注入,Spring官方却不推荐在字段上使用它。本文将探讨字段注入的现状及其存在的问题,如难以进行单元测试、违反单一职责原则及易引发NPE等,并介绍为何Spring推荐构造器注入,包括增强代码可读性和维护性、方便单元测试以及避免NPE等问题。通过示例代码展示如何将字段注入重构为构造器注入,提高代码质量。
|
30天前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
1月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
31 1
|
1月前
|
XML Java 数据格式
Spring IOC容器的深度解析及实战应用
【10月更文挑战第14天】在软件工程中,随着系统规模的扩大,对象间的依赖关系变得越来越复杂,这导致了系统的高耦合度,增加了开发和维护的难度。为解决这一问题,Michael Mattson在1996年提出了IOC(Inversion of Control,控制反转)理论,旨在降低对象间的耦合度,提高系统的灵活性和可维护性。Spring框架正是基于这一理论,通过IOC容器实现了对象间的依赖注入和生命周期管理。
65 0
|
2月前
|
XML Java 开发者
经典面试---spring IOC容器的核心实现原理
作为一名拥有十年研发经验的工程师,对Spring框架尤其是其IOC(Inversion of Control,控制反转)容器的核心实现原理有着深入的理解。
120 3
|
3月前
|
XML Java 数据格式
Spring5入门到实战------8、IOC容器-Bean管理注解方式
这篇文章详细介绍了Spring5框架中使用注解进行Bean管理的方法,包括创建Bean的注解、自动装配和属性注入的注解,以及如何用配置类替代XML配置文件实现完全注解开发。
Spring5入门到实战------8、IOC容器-Bean管理注解方式
|
3月前
|
缓存 Java 数据库连接
Spring Boot 资源文件属性配置,紧跟技术热点,为你的应用注入灵动活力!
【8月更文挑战第29天】在Spring Boot开发中,资源文件属性配置至关重要,它让开发者能灵活定制应用行为而不改动代码,极大提升了可维护性和扩展性。Spring Boot支持多种配置文件类型,如`application.properties`和`application.yml`,分别位于项目的resources目录下。`.properties`文件采用键值对形式,而`yml`文件则具有更清晰的层次结构,适合复杂配置。此外,Spring Boot还支持占位符引用和其他外部来源的属性值,便于不同环境下覆盖默认配置。通过合理配置,应用能快速适应各种环境与需求变化。
43 0
|
3月前
|
安全 Java 开发者
开发者必看!@Resource与private final的较量,Spring Boot注入技巧大揭秘,你不可不知的细节!
【8月更文挑战第29天】Spring Boot作为热门Java框架,其依赖注入机制备受关注。本文通过对比@Resource(JSR-250规范)和@Autowired(Spring特有),并结合private final声明的字段注入,详细探讨了两者的区别与应用场景。通过示例代码展示了@Resource按名称注入及@Autowired按类型注入的特点,并分析了它们在注入时机、依赖性、线程安全性和单一职责原则方面的差异,帮助开发者根据具体需求选择最合适的注入策略。
139 0
|
3月前
|
XML Java 测试技术
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架
这篇文章介绍了Spring5框架的三个新特性:支持@Nullable注解以明确方法返回、参数和属性值可以为空;引入函数式风格的GenericApplicationContext进行对象注册和管理;以及如何整合JUnit5进行单元测试,同时讨论了JUnit4与JUnit5的整合方法,并提出了关于配置文件加载的疑问。
Spring5入门到实战------17、Spring5新功能 --Nullable注解和函数式注册对象。整合JUnit5单元测试框架