Java单元测试实践-00.目录(9万多字文档+700多测试示例)

1. 使用spring-test进行单元测试

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#overview-testing 。

spring-test模块支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。它提供Spring ApplicationContext的持续加载,以及这些上下文的缓存,还提供了可用于测试的独立于代码的mock对象。

1.1. 基本配置

1.1.1. SpringJUnit4ClassRunner

参考“Spring JUnit 4 Runner”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-junit4-runner )。

Spring测试上下文框架通过自定义运行器( 在JUnit4.12或更高版本上支持 )提供与JUnit4的完全集成。通过使用@RunWith(SpringJUnit4ClassRunner.class)或简写的@RunWith(SpringRunner.class)变体来注解测试类。开发人员可以实现基于标准JUnit4的单元测试和集成测试,同时获得测试上下文框架提供的便利,例如支持加载应用程序上下文,测试实例的依赖注入,事务测试方法执行等。

1.1.2. 上下文配置注解@ContextConfiguration

参考“@ContextConfiguration”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#__contextconfiguration )。

@ContextConfiguration注解用于定义类级元数据,用于确定如何为集成测试加载和配置Spring的应用上下文ApplicationContext。具体来说,@ContextConfiguration声明应用程序上下文资源位置或用于加载上下文的包含注解的类。

以上所述资源位置通常是位于类路径中的XML配置文件或Groovy脚本;注解类通常是@Configuration类。同时,资源位置也可以引用文件系统中的文件和脚本,包含注解的类可以是组件类等。

使用XML配置文件时,@ContextConfiguration注解的使用示例如下:

@ContextConfiguration(locations = {"classpath:applicationContext.xml"})

使用spring-test可以正常加载Spring上下文,与直接启动Spring应用的效果类似。

可参考示例TestSpringBase类及其子类TestSpringReuseContext1、TestSpringReuseContext2。

1.2. 测试工具类

参考“General testing utilities”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#unit-testing-utilities )。

org.springframework.test.util包中包含几个用于单元测试和集成测试的通用实用程序。

1.2.1. ReflectionTestUtils

ReflectionTestUtils是基于反射的实用程序方法的集合。开发人员在测试可能需要更改常量值,设置非公共字段,调用非公共setter方法,或需要调用非公共配置或生命周期回调方法,在以上场景中可以使用这些方法。

PowerMock的Whitebox类存在类似功能,使用更方便,可以替代ReflectionTestUtils。

1.2.2. AopTestUtils

AopTestUtils是AOP相关实用程序方法的集合。这些方法可用于获取隐藏在一个或多个Spring代理后面的底层目标对象的引用。例如,如果使用像EasyMock或Mockito这样的库将bean配置为动态Mock对象,并且Mock对象包装在Spring代理中,则可能需要直接访问底层Mock对象,以便为其配置期望并执行验证。

在后续内容中,有Whitebox、AopTestUtils的使用示例。

1.3. Spring Context加载次数

1.3.1. Context缓存

参考“Context management and caching”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testing-ctx-management ),“Context caching”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-ctx-management-caching )。

Spring TestContext Framework提供SpringApplicationContexts和WebApplicationContexts的加载以及上下文的缓存。支持缓存加载的上下文非常重要,因为启动时间可能会成为一个问题,不是因为Spring本身的开销,而是因为Spring容器实例化的对象需要时间来实例化。

一旦TestContext框架为测试加载了ApplicationContext( 或WebApplicationContext ),该上下文将被缓存,并在同一测试套件中声明相同唯一上下文配置的所有后续测试重复使用,即上下文会被缓存并重复使用。

从Spring Framework 4.3开始,上下文缓存的大小受限于默认的最大大小32。每当达到最大大小时,会使用最近最少使用(LRU)策略剔除和关闭过时的上下文。通过设置名为spring.test.context.cache.maxSize的JVM系统属性,可以通过命令行或构建脚本配置最大大小。

由于在给定的测试套件中加载大量应用程序上下文会导致套件执行不必要的长时间,因此确切地知道已加载和缓存了多少底层上下文通常是有帮助的。要查看上下文缓存的统计信息,只需将org.springframework.test.context.cache日志记录类别的日志级别设置为DEBUG。

当使用缓存时,涉及到org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate类等。

1.3.2. 禁用Context缓存

参考“Context caching”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-ctx-management-caching )。

在某些情况下,测试会破坏应用程序上下文并需要重新加载,例如修改bean定义或应用程序对象的状态。可以使用@DirtiesContext注解测试类或测试方法,以指示Spring在执行下一个测试之前从缓存中删除上下文并重建应用程序上下文,即可以禁止使用上下文缓存,每次都生成新的上下文。

参考“@DirtiesContext”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#__dirtiescontext )。

@DirtiesContext注解指示在执行测试期间基础Spring ApplicationContext已被弄脏( 即被以某种方式修改或损坏,例如,通过更改单例bean的状态 )并且应该关闭。当应用程序上下文标记为脏时,将会从测试框架的缓存中删除并关闭。因此,对于需要具有相同配置元数据的上下文的任何后续测试,将重建基础Spring容器。

@DirtiesContext可以用作同一类或类层次结构中的类级别和方法级别注释。通过该注解配置的methodMode和classMode,可以决定ApplicationContext被标记为脏的时机,是在带有此类注解的方法执行之前或之后,还是在当前测试类执行之前或之后。

@DirtiesContext包含属性classMode与methodMode,默认的classMode是AFTER_CLASS,默认的methodMode是AFTER_METHOD。

classMode的取值范围及说明如下:

classMode

含义

BEFORE_CLASS

在测试类之前,关联的ApplicationContext将标记为脏

BEFORE_EACH_TEST_METHOD

在测试类的每个测试方法之前,关联的ApplicationContext将标记为脏

AFTER_EACH_TEST_METHOD

在测试类的每个测试方法之后,关联的ApplicationContext将标记为脏

AFTER_CLASS

在测试类之后,关联的ApplicationContext将标记为脏

methodMode的取值范围及说明如下:

methodMode

含义

BEFORE_METHOD

在对应的测试方法之前,关联的ApplicationContext将标记为脏

AFTER_METHOD

在对应的测试方法之后,关联的ApplicationContext将标记为脏

@DirtiesContext的简单使用可以查看示例TestSpringRefreshContextBeforeClass、TestSpringRefreshContextBeforeMethod类。

例如当需要使Spring Context在执行类之前重新加载时,可以使用“@DirtiesContext(classMode = BEFORE_CLASS)”;当需要使Spring Context在执行每个@Test方法前重新加载时,可以使用“@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD)”。

当Spring Context不使用缓存时,执行每个测试类都需要重新加载上下文,会增加测试执行时间。

1.3.3. Spring Context加载次数验证

默认情况下,Spring Context会使用缓存,通过以下现象证明:

  • 基类TestSpringBase的initTestMockBase方法使用了@Before注解,在其中打印了TestApplicationListener类保存的最近一次ContextRefreshedEvent事件对象的hashCode,若观察不同的测试类中打印的hashCode相同,则说明Context使用缓存,没有重新加载;
  • 子类TestSpringReuseContext1、TestSpringReuseContext2、TestSpringRefreshContextBeforeClass、TestSpringRefreshContextBeforeMethod类的@Before方法中打印了注入的TestService2对象的hashCode,也可以用于观察;
  • 当org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate类的loadContext方法打印的日志为“Retrieved”时,代表当前类使用的Context使用缓存;当日志为“Storing”时,代表使用的Context第一次加载或重新加载;
  • 当DefaultContextCache类的logStatistics方法打印的日志中的hitCount与前一次相比加1时,代表Context使用缓存;当missCount与前一次相比加1时,代表使用的Context第一次加载或重新加载。

DefaultCacheAwareContextLoaderDelegate类的日志示例如下:

DEBUG DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:129)  - Retrieved ApplicationContext from cache with key [[MergedContextConfiguration@590c73d3 testClass = TestSpringReuseContext2, locations = '{classpath:applicationContext.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]
DEBUG DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:118)  - Storing ApplicationContext in cache under key [[MergedContextConfiguration@58e6d4b8 testClass = TestSpringRefreshContextBeforeClass, locations = '{classpath:applicationContext.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]

DefaultContextCache类的日志示例如下:

DEBUG cache.logStatistics(DefaultContextCache.java:290)  - Spring test ApplicationContext cache statistics: [DefaultContextCache@60d1a32f size = 1, maxSize = 32, parentContextCount = 0, hitCount = 2, missCount = 2]
DEBUG cache.logStatistics(DefaultContextCache.java:290)  - Spring test ApplicationContext cache statistics: [DefaultContextCache@60d1a32f size = 1, maxSize = 32, parentContextCount = 0, hitCount = 3, missCount = 2]
DEBUG cache.logStatistics(DefaultContextCache.java:290)  - Spring test ApplicationContext cache statistics: [DefaultContextCache@60d1a32f size = 1, maxSize = 32, parentContextCount = 0, hitCount = 3, missCount = 3]

可参考示例TestSpringReuseContext1、TestSpringReuseContext2类。

通过以下方式可以执行上述示例,结果相同:

  • 使用JUnit的Suite执行

执行使用Suite的示例TestSuiteFrameworkSpring类,可以执行TestSpringReuseContext1、TestSpringReuseContext2、TestSpringRefreshContextBeforeClass、TestSpringRefreshContextBeforeMethod类;

  • 使用IDE执行目录或多个类

使用IDE( IntelliJ IDEA、Eclipse等 ),选中示例TestSpringReuseContext1类所在的test目录,或其中的类,启动测试,可以执行TestSpringReuseContext1、TestSpringReuseContext2、TestSpringRefreshContextBeforeClass、TestSpringRefreshContextBeforeMethod类。

1.3.4. 使用PowerMock时Context缓存失效

当使用PowerMock时,Spring Context缓存会失效,执行每个类时Context都会加载一次,在后续内容进行说明。

1.4. 测试执行监听器TestExecutionListener

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#_testexecutionlistener ,测试执行监听器TestExecutionListener定义了API,可以对监听器已注册的测试上下文管理器TestContextManager发布的测试执行事件进行响应。

1.4.1. Spring默认提供的TestExecutionListener实现

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-tel-config 。

Spring默认提供了以下TestExecutionListener实现:

  • ServletTestExecutionListener

为WebApplicationContext配置Servlet API模拟;

  • DirtiesContextBeforeModesTestExecutionListener

处理before模式的@DirtiesContext注解;

  • DependencyInjectionTestExecutionListener

为测试实例提供依赖注入;

  • DirtiesContextTestExecutionListener

处理after模式的@DirtiesContext注解;

  • TransactionalTestExecutionListener

提供具有默认回滚语义的事务测试执行;

  • SqlScriptsTestExecutionListener

执行通过@Sql注解配置的SQL脚本。

1.4.2. TestExecutionListener提供的方法

TestExecutionListener提供了一些方法,这些方法的参数类型均为TestContext。

为了获取当前执行的测试类与测试方法相关的信息,可以调用TestContext的方法,getTestClass()方法可返回当前测试类类型testClass,getTestInstance()方法可返回当前测试类实例testInstance,getTestMethod()方法可返回当前测试方法testMethod,getTestException()方法可以返回当前测试方法执行期间出现的异常(未出现异常时为空)。

TestExecutionListener提供的方法,除prepareTestInstance()以外,其余可以与JUnit的@Before等注解对应(执行时机相同,效果类似),如下所示:

  • beforeTestClass()

在测试类执行前执行,与@BeforeClass注解对应。

beforeTestClass()方法执行时,TestContext中的testClass非null,testMethod、testInstance为null。

  • prepareTestInstance()

在测试类完成实例化时执行。

prepareTestInstance()方法执行时,TestContext中的testMethod为null,testClass、testInstance非null,且testInstance中的成员变量已完成依赖注入。

若需要获取测试实例中的成员变量,可通过反射获取,例如对TestContext中的testInstance执行Whitebox.getInternalState()操作。

在执行每个测试类时,会使用新的TestExecutionListener实例,可以观察示例TestCommonExecutionListener类的prepareTestInstance()方法输出的当前TestExecutionListener类的hashcode,在执行不同的测试类时显示值不同。

  • beforeTestMethod()

在测试方法执行前执行,与@Before注解对应。

beforeTestMethod()方法执行时,TestContext中的testMethod、testClass、testInstance均非null。

  • afterTestMethod()

在测试方法执行后执行,与@After注解对应。

  • afterTestClass()

在测试类执行后执行,与@AfterClass注解对应。

与在测试基类中通过@Before等注解指定测试方法执行前后的公共处理相比,使用TestExecutionListener的优势在于,可以减少测试基类中的代码,且公共代码更容易复用。

1.4.3. 注册自定义TestExecutionListener

参考 https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-tel-config-registering-tels 。

使用@TestExecutionListeners注解可将自定义TestExecutionListener实现类注册到测试类及其子类。

需要将DirtiesContextBeforeModesTestExecutionListener、DependencyInjectionTestExecutionListener、DirtiesContextTestExecutionListener也进行注册,若不对DependencyInjectionTestExecutionListener进行注册,测试类的依赖注入无法完成 ,示例如下:

@TestExecutionListeners({DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class, TestCommonExecutionListener.class})

可参考示例TestMockBase、TestCommonExecutionListener类。

@TestExecutionListeners注解可以添加在测试类,或测试类的超类中,子类与超类中的配置不会相互覆盖。

例如在子类中通过@TestExecutionListeners注解指定了A.class,在超类中指定了B.class,则A.class与B.class均会被@TestExecutionListeners注解处理。

1.5. 加快Spring应用单元测试启动速度

对于基于Spring的应用,若有太多Bean被设置为非懒加载,可能增加应用启动耗时;在以上情况下,可以在进行单元测试时将所有的Bean设置为非懒加载,避免单元测试时不需要使用的Bean提前实例化,可加快Spring应用启动速度。

在示例代码中,BeanFactoryPostProcessor的实现类TestBeanFactoryPostProcessor保存在test模块中,仅当进行单元测试时生效,在postProcessBeanFactory方法中根据TestFlag类的标志,将全部Bean设置为懒加载(或非懒加载)。

TestBeanFactoryPostProcessor类示例如下:

@Component
public class TestBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        boolean setLazy;

        if (Boolean.TRUE.equals(TestFlag.isAllLazy())) {
            setLazy = true;
        } else if (Boolean.FALSE.equals(TestFlag.isAllLazy())) {
            setLazy = false;
        } else {
            return;
        }

        String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);

            beanDefinition.setLazyInit(setLazy);

            logger.info("beanClassName: {} lazy: {} setLazy: {}", beanDefinition.getBeanClassName(), beanDefinition.isLazyInit(), setLazy);
        }
    }
}

BeanPostProcessor的实现类TestBeanPostProcessor保存在test模块中,仅当进行单元测试时生效,当有Bean被实例化时,记录将已完成实例化的Bean数量加1,用于记录已完成实例化的Bean数量。

TestBeanPostProcessor类示例如下:

@Component
public class TestBeanPostProcessor implements BeanPostProcessor {

    private static AtomicInteger inited = new AtomicInteger(0);

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (TestFlag.isAllLazy() == null) {
            return bean;
        }

        inited.incrementAndGet();

        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public static int getInitedNum() {
        return inited.get();
    }
}

在示例类TestSpringLazyTrue中,设置TestFlag标志,使所有的Bean设置为懒加载,执行后可确认指定的自定义类未完成实例化,已完成实例化的Bean总数约6,执行时启动耗时约2.08秒。

在示例类TestSpringLazyFalse中,设置TestFlag标志,使所有的Bean设置为非懒加载,执行后可确认指定的自定义类已完成实例化,已完成实例化的Bean总数约44,执行时启动耗时约3.122秒。

可以看出,将不需要使用的Bean由非懒加载设置为懒加载,可以加快Spring应用单元测试启动速度(需要注意应不影响功能)。

1.6. Spring JUnit4支持类

参考“JUnit 4 support classes”( https://docs.spring.io/spring/docs/4.3.26.RELEASE/spring-framework-reference/htmlsingle/#testcontext-support-classes-junit4 )。

org.springframework.test.context.junit4包提供了基于JUnit4测试的支持类( 支持JUnit 4.12或更高版本 ),包括AbstractJUnit4SpringContextTests、AbstractTransactionalJUnit4SpringContextTests等。

  • AbstractJUnit4SpringContextTests

AbstractJUnit4SpringContextTests是一个抽象基础测试类,它将Spring TestContext Framework与JUnit 4环境中的显式ApplicationContext测试支持集成在一起。扩展AbstractJUnit4SpringContextTests时,可以访问protected applicationContext实例变量,该变量可用于执行显式bean查找或测试整个上下文的状态。

  • AbstractTransactionalJUnit4SpringContextTests

AbstractTransactionalJUnit4SpringContextTests是AbstractJUnit4SpringContextTests的抽象事务扩展,它为JDBC访问添加了一些便利功能。该类要求在ApplicationContext中定义javax.sql.DataSource bean和PlatformTransactionManager bean。扩展AbstractTransactionalJUnit4SpringContextTests时,可以访问受保护的jdbcTemplate实例变量,该变量可用于执行SQL语句以查询数据库,可用于在执行与数据库相关的应用程序代码之前和之后确认数据库状态,Spring确保此类查询在与应用程序代码相同的事务范围内运行。

AbstractTransactionalJUnit4SpringContextTests使用了@Transactional注解,并通过@TestExecutionListeners注解指定了TransactionalTestExecutionListener类。

当测试类继承自该AbstractTransactionalJUnit4SpringContextTests类时,测试结束后默认会自动对数据库操作进行回滚。在后续内容“数据库操作自动回滚处理”中有详细说明。

1.7. spring-test示例工程

Spring提供了关于spring-test的示例工程,地址为( https://github.com/spring-projects/spring-framework/tree/master/spring-test/ )。