本文共 11565 字,大约阅读时间需要 38 分钟。
针对Spring某个类及方法的单元测试,结合单元测试框架(比如JUnit)和Mock的框架(比如EasyMock)就足以完成,当前Bean需要的依赖对象通过Mock创建,隔离所有的依赖,不需要使用到Spring IoC容器。但实际开发中所进行的Spring测试并不是完全意义上的单元测试,而是依赖于容器的测试。
Spring提供的测试框架严格意义上是侧重集成测试的框架,这在Spring官方文档中有明确的区分。但Spring针对单元测试提供了一些支持,包括用于单元测试的一些共用方法和模拟类,这些方法和模拟类,可以使用在一般应用和Web应用中,让测试变得非常方便。
结合单元测试框架和Mock框架基本可以对Spring进行单元测试,比如JUnit+EasyMock的组合。在JUnit的框架下,使用EasyMock模拟依赖对象对某个组件类或服务类进行脱离容器的单元测试。
如果要结合容器进行集成测试,在测试类中的固件初始化时(比如@Before注解方法)初始化容器后获取需要的Bean实例进行测试,以JUnit4中初始化容器进行测试的代码示例如下:
public class JunitSpringTest { //单元测试类private ApplicationContext context; //定义上下文变量 @Before //测试环境初始化注解 public void initSpring() { //环境初始化方法 context = new ClassPathXmlApplicationContext("applicationContext.xml");//初始上下文 } @Test //注解测试方法 public void testMethod1() { //测试方法 HelloBean bean1 = (HelloBean)context.getBean("bean1"); //从容器获取bean实例}}
以上在initSpring()方法中,通过ClassPathXmlApplicationContext初始化容器后就可以从容器中获取Bean进行测试。上面的代码段是较为常见的Spring测试示例,虽然可以达成测试效果,但是存在着如下问题:
在JUnit4中,@Before注解在每个标准@Test方法之前都会执行,这就意味着这个测试类中有多少个@Test标识的方法,Spring容器就会被重复化初始多少次。如果容器在初始化时要加载ORM映射和数据源,这笔开销还是很可观的。这个问题虽然可以通过使用@BeforeClass注解修改成类层级的初始化,但是在整个项目使用Maven进行批量测试时,对应每个测试类,都会初始化一个新的容器。
这种效果可以在Eclipse使用Maven批量测试进行验证。新建两个测试类,打印ApplicationContext对象的id,可以看到每个测试类的应用上下文都不一样的,验证的代码如下:
public class JUnitSpring1Test { //测试类 private static ApplicationContext context; //静态应用上下文 @BeforeClass //类层级环境初始化注解 public static void initSpring() { //类层级环境初始化 context = new ClassPathXmlApplicationContext("applicationContext.xml");//上下文 } @Test //注解测试方法 public void testMethod() { //测试方法 System.out.println("JUnitSpring1Test, applicationContext=" + context.toString()); }}
对每个Bean的测试都要先通过getBean()方法从容器中先获取对应的实例,之后做强制类型转换之后方可使用和测试。在涉及多个Bean的集成测试的时候,这样重复额外的代码会极大的影响测试效率。
在开发时候的单元测试,或是使用工具批量的单元测试,很多状况希望测试方法不要将数据操作持久化到数据库中。虽然可以在方法层级上添加事务的处理,但是针对测试的特别改动有可能出现忘记回退而影响实际的功能。
针对上面那些问题,Spring提供了专门的测试模块来简化测试工作和加快测试的效率。
Spring提供了通用的、注解驱动的测试模块(spring-test)辅助进行Spring相关的测试。spring-test支持结合JUnit或TestNG框架来进行单元测试和集成测试。spring-test的测试模块需要导入,Maven的导入方式直接在pom.xml中加入依赖项,类似:
org.springframework spring-test 5.0.8.RELEASE
此外,Spring是结合JUnit或TestNG进行测试,所以需要确保对应的测试框架导入。
spring-test测试模块的代码目录结构如图1所示。
图1 Spring测试模块的目录结构
测试模块分为mock和test两大部分,mock包里面提供了四种类型的模拟对象,可以单独使用于单元测试。context和web分别对应核心框架和Web框架的测试支持;annotation是用于测试的注解定义;jdbc包提供了数据库测试的支持;util包提供了一些公用方法可以用于脱离Spring测试框架进行独立的单元测试。
Spring测试模块可以结合单元测试框架,对一般桌面应用和Web应用进行测试。针对两种不同类型应用的测试,Spring对应的有通用测试框架和MVC Web的测试框架。使用Spring测试框架,应用上下文可以一次性加载并进行缓存,大大提高了测试的速度。更方便的是其提供了很多便捷的注解辅助测试。
Spring对单元测试的支持主要有两个方面:提供了多种类型的模拟对象和提供了用于单元测试的一些共用方法。
对于简单的POJO和服务端的类和方法,可以使用Mock框架模拟创建,但对于复杂的状况,Mock框架不易处理。Spring提供了更高层级的类的模拟,包含:
1、环境的模拟
Java中的Property是以键值对方式存储的数据类型,PropertyResolver是Spring提供的属性解析器,可以通过key查找对应的值,Environment继承自PropertyResolver,但是额外提供了Profile特性,即可以根据不同环境(比如:开发、测试和正式环境)得到相应数据。MockEnvironment就是对Environment的模拟,可以在测试时设置需要的属性键和值。使用的示例如下:
//代码方式的bean配置和注册DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); //bean工厂GenericBeanDefinition beanDefinition = new GenericBeanDefinition();//Bean定义beanDefinition.setBeanClass(User.class); //设置bean定义类beanDefinition.getPropertyValues().add("name", "${name}") //设置bean属性;使用占位符bf.registerBeanDefinition("user", beanDefinition); //注册bean//模拟环境并设置属性后,对bean中的占位符进行替换PropertySourcesPlaceholderConfigurer pc = new PropertySourcesPlaceholderConfigurer();pc.setEnvironment(new MockEnvironment().withProperty("name", "Oscar"));//设置环境属性pc.postProcessBeanFactory(bf); //替换占位符
上面的示例代码中,使用GenericBeanDefinition类进行Bean的配置和注册,对应的Bean类是User,使用占位符${name}设置该类的属性值。接着创建MockEnvironment和设置了属性name的值,最后设置PropertySourcesPlaceholderConfigurer的环境为创建的模拟环境并替换占位符为模拟环境中设置的属性值。
2、JNDI的模拟
JNDI(Java Naming and Directory Interface),Java命名与目录接口。直观点理解就是给资源(比如数据库资源)一个通用的名字,通过这个名字就可以查找这个资源了。
在开发和测试时,数据源可以通过配置url、name和password在XML中配置,但在正式环境中,为了保障安全,数据源更多的是使用JNDI的方式配置在应用服务器上,而且除了数据源以外的其他资源,类似EJB等就必须使用JNDI的方式了。Spring提供了根据JNDI名称查找资源对象的类JndiObjectFactoryBean,以JNDI:“java:comp/env/jdbc/mydatasource”为例,通过XML配置数据源bean的方式如下:
java:comp/env/jdbc/mydatasource
现在问题是:测试的时候不希望开启应用服务器,怎么找到JNDI对应的资源呢?Spring提供了使用SimpleNamingContextBuilder来构造JNDI资源的模拟。以上面配置的数据源的JNDI模拟为例:
public void initForTest() throws IllegalStateException, NamingException { //模拟JNDI方法 DriverManagerDataSource ds = new DriverManagerDataSource(); //创建数据源 ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); //驱动类设置 ds.setUrl("jdbc:mysql://localhost:3306/ssmi?serverTimezone=UTC");//数据源url ds.setUsername("root"); //用户名 ds.setPassword("123456"); //密码 SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder(); builder.bind("java:comp/env/jdbc/mydatasource", ds); //绑定数据源 builder.activate();//激活 }
以上通过DriverManagerDataSource创建一个数据源,使用SimpleNamingContextBuilder给该资源绑定一个JNDI的名字。以上方法可以使用@Before注解后,读取包含以上JNDI的配置文件就可以使用模拟的数据源了。DB及数据源相关的部分会在后面章节深入介绍。
3、HTTP和Web相关的模拟
Spring提供了Http和Servlet的模拟对象类用于Web测试,相关部分的会在后面Spring Web测试中介绍,另外,还提供了Spring Web Reactive的响应式 Web测试的模拟对象,本书不做探讨。
org.springframework.test.util包中的ReflectionTestUtils类,提供了基于反射机制的方法,可以修改变量、非公有的属性和访问非公有的方法,还可以调用生命周期回调方法。在基于Spring框架开发中,经常使用注解(包括@Autowired、@Inject和@Resource等)对私有的方法或属性进行依赖注入,这些私有的变量和方法不能直接获取和调用。举例来看,组件类Foo定义如下:
public class Foo { //组件类 @Autowired //自动装载注解 private String name; //字符串变量 @PostConstruct //组件初始化注解 private void onInit(){ //组件初始化回调方法 System.out.println("onInit... " + name); } @PreDestroy //组件销毁注解 private void onDestroy(){ //组件销毁方法回调 System.out.println("onDestroy... " + name);}}
该组件类使用@Autowired注解了一个私有的属性name,@PostConstruct、@PreDestroy分别注解了两个私有的回调方法。这种代码风格是实际开发中常见的风格,可是在单元测试中不启动Spring容器,无法得到依赖对象也无法执行注解的生命周期回调方法下如何测试呢?答案就是通过ReflectionTestUtils,测试代码如下:
@Test //测试方法注解public void test () { //测试方法 Foo foo = new Foo(); //组件创建 ReflectionTestUtils.setField(foo, "name", "Oscar"); //设置私有变量的值 ReflectionTestUtils.invokeMethod(foo, "onInit"); //调用组件初始化方法 ReflectionTestUtils.invokeMethod(foo, "onDestroy"); //调用组件销毁方法}
Spring测试框架在JUnit和TestNG框架之上进行扩展。在测试类似使用JUnit的@RunWith(JUnit4)或@ExtendWith(JUnit5)指定Spring对应的运行器扩展,就可以在测试类中很容易进行上下文的初始化和缓存。通过测试执行监听器,可以在测试类中使用依赖注入和事务管理等注解,也可以自定义注解,简化测试。
使用Spring框架开发的应用,更常使用的是对容器初始化之后的测试,也就是严格意义上的集成测试。Spring测试框架主要位于org.springframework.test.context包中,提供了通用的、注解驱动和集成测试支持。其无缝集成了JUnit和TestNG的测试框架。
以JUnit4为例,在Spring测试框架下编写测试类的步骤如下:
完整的代码实例如下:
@RunWith(SpringJUnit4ClassRunner.class) //运行器注解@ContextConfiguration(locations="classpath:cn/osxm/ssmi/chp6/applicationContext.xml")//配置 public class SpringTest { //测试类@Autowired //自动装载注解private HelloService helloService;@Test //测试方法注解public void hello() { //测试方法 helloService.sayHello();}}
@RunWith是JUnit的注解,用于标注测试运行器类。Spring扩展了JUnit4的BlockJUnit4ClassRunner类, 额外实现了容器的测试上下文的初始化和维护(JUnit5使用的是@ExtendWith)。初始化容器依据的配置通过注解@ContextConfiguration指定。在测试类中也可以使用Spring容器依赖注入等(类似@Autowired)注解。这些类在使用Maven批量执行时,应用上下文(容器)会缓存,不需要重复创建,节省测试开销,加快了测试效率。
Spring提供的测试运行器SpringJUnit4ClassRunner会创建测试上下文管理类TestContextManager,TestContextManager主要维护测试上下文TestContext和测试执行的监听TestExecutionListener,TestContext维护了根据配置初始化的容器应用上下文ApplicationContext和测试类、方法、和异常等信息,TestExecutionListener则是测试类和方法执行前后的一些操作,这几个类的关系如图2所示。
图2 Spring测试框架的核心类及关系
对上面的测试框架的核心类说明如下:
除了上面的核心类之外,还有以下一些重要的类:
@TestExecutionListeners注解使用在测试类上,用于指定需要的测试执行监听器。测试执行监听器用于实现类执行之前被执行或实现类的测试方法之前被执行测试执行之前进行一些额外的处理。Spring默认提供了TestExecutionListener的三个实现,包括:事务管理的TransactionalTestExecutionListener、依赖注入的DependencyInjectionTestExecutionListener和上下文检查的DirtiesContextTestExecutionListener。
除了以上监听器外,也可以实现自己的监听器类完成需要的操作,自定义的监听器需要继承类org.springframework.test.context.support.AbstractTestExecutionListener,具体可以参照DependencyInjectionTestExecutionListener的实现
默认DependencyInjectionTestExecutionListener和TransactionalTestExecutionListener是开启的,也就是在测试类上不添加@TestExecutionListeners注解配置,就可以在测试类上直接使用@Autowried和@Transaction等注解。但是如果有自定义的新增注解的话,则需要把DependencyInjectionTestExecutionListener或是TransactionalTestExecutionListener加上,TransactionalTestExecutionListener可以不加。接下来自定义一个注解@MyLogTestAnno,如果在测试方法中使用了此注解,则添加测试日志的功能为例,通过添加一个测试执行监听的实现MyLogTestExecutionListener来达成此功能。
注解定义类MyLogTestAnno:
@Retention(RetentionPolicy.RUNTIME) //运行时注解@Target(ElementType.METHOD) //注解使用在方法上@Inherited //允许继承@Documented //注解包含在Java Doc中public @interface MyLogTestAnno { //注解定义 public String logFileName(); }
日志测试执行监听实现类:
public class MyLogTestExecutionListener implements TestExecutionListener {public void prepareTestInstance(TestContext testContext) throws Exception {//准备测试实例}public void beforeTestClass(TestContext testContext) throws Exception {//测试类之前执行 System.out.println("测试类之前执行");}public void afterTestClass(TestContext testContext) throws Exception { //测试类之后执行 System.out.println("测试类之后执行");}public void beforeTestMethod(TestContext testContext) throws Exception {//测试方法之前执行System.out.println("测试方法之前执行");MyLogTestAnno myLogTest = testContext.getTestMethod().getAnnotation(MyLogTestAnno.class); //获取注解 if (myLogTest == null) { return; } String logfile = myLogTest.logFileName(); System.out.println("注解参入的参数"+logfile);}public void afterTestMethod(TestContext testContext) throws Exception {//测试方法之后执行 System.out.println("测试方法之后执行");}}
在完成beforeTestMethod()的接口方法,获取该方法上标注的注解,如果有找到@MyLogTestAnno,则执行日志相关的逻辑代码。
测试类中配置自定义测试执行监听类:
@RunWith(SpringJUnit4ClassRunner.class) //测试运行器注解@ContextConfiguration(locations = { "classpath:cn/osxm/ssmi/chp6/applicationContext.xml" })@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class, MyLogTestExecutionListener.class,}) //测试执行监听器注解public class MyExecutionListenerTest{ //测试类 @Autowired //自动装载注解 private UserController userController; @Test //测试方法注解 @MyLogTestAnno(logFileName = "testLog.txt") //自定义注解使用,logFileName赋值 public void test() { }}
在@TestExecutionListeners注解中,指定自定义的监听器,需要注意将框架用于依赖和事务处理的一并加上。
从测试执行监听实现类的代码可以看到,可以在测试类或测试方法之前和之后进行预置或是善后操作。这个和 JUnit的@Before,@BeforeClass很类似,只是这里处理的是自定义的注解。
Spring测试框架在不需要依赖应用服务器和其他的部署环境的状况下,可以进行基于容器的集成测试。Spring测试框架与主流的测试框架JUnit或TestNG的整合使用,学习成本较低。Spring测试框架还支持:
除此,Spring测试框架还可以管理测试之间的Spring IoC容器缓存。特别是在Web应用中可以大大提高测试的方便性和效率性。
本文节选自《Spring+SpringMVC+MyBatis整合开发实战》,由兴趣的读者朋友可以关注后私信我获取这本书的pdf
转载地址:http://migii.baihongyu.com/