Spring测试 其实很简单

在过去的职业生涯里,我经常发现有些人不写测试代码,而他们声称不写的理由是无法轻易地写出覆盖多个不同模块的测试用例。好吧,我相信他们中的大部分要么是缺乏一些比较易掌握的技术手段,要么就是没时间来把它搞清楚,毕竟工作中总会有进度之类的各种压力。因为不知道该如何测试,所以就经常忽略集成测试,由此带来的问题就是越来越糟糕的软件、越来越多的BUG和更加失望的客户。所以我想分享一些个人的经验,揭开集成测试神秘的面纱。

如何对基于Spring的工程更好地进行集成测试
使用工具: Spring, JUnit, Mockito
想象有这样一个Spring工程,它集成了一些外部服务,例如,一些银行的web服务。那么,为这个工程写测试用例以及在持续集成系统中完成这些测试时所遇到的问题基本都差不多:
 1.每次测试都会有交易进行,每次交易都需要付出金钱成本,这些成本最终由客户承担;
 2.测试时发出的过多的请求有可能被认为是恶意请求,可能造成在银行的账户被封,后果是测试失败;
 3.当使用非生产环境进行测试时,测试结果并不十分可靠,同样,后果是测试失败。
通常情况下,你对单个类进行测试的时候,问题很容易解决,因为你可以虚拟一些外部服务来供调用。但是当对整个巨大的业务流程进行测试的时候,意味你需要对多个部件进行测试,这时,需要你将这些部件都纳入到Spring容器中进行管理。所幸,Spring包含了非常优秀的测试框架,允许你将来自生产环境配置文件中的bean注入到测试环境中,但是对那些被调用的外部服务,需要我们自己去写模拟实现。一般人第一反应可能是在测试的setUp阶段对由Spring注入的bean进行重新注入(修改),但是这种方法需要再仔细考虑一下。

警告:通过这种方式,你的测试代码打破了容器自身的行为,所以没法保证在真实的环境中也如你测试的结果一样。
事实上,我们无需先实现模拟类然后再把它重新注入到所需的bean中,我们可以让Spring帮助我们一开始就注入模拟类。让我们用代码演示一下。
示例工程包含一个名为BankService的类,代表调用的外部服务,一个名为UserBalanceService的类,它会调用BankService。UserBalanceService实现的非常简单,仅仅完成将余额从String向Double类型的转换。
BankService.java的源码:

 public interface BankService {
 String getBalanceByEmail(String email);
}

BankServiceImpl.java的源码:

 public class BankServiceImpl implements BankService {
 @Override
 public String getBalanceByEmail(String email) {
  throw new UnsupportedOperationException("Operation failed due to external exception");
 }
}

UserBalanceService.java的源码:

 interface UserBalanceService {
 Double getAccountBalance(String email);
}

UserBalanceServiceImpl.java的源码:

 public class UserBalanceServiceImpl implements UserBalanceService {
 @Autowired
 private BankService bankService;
 @Override
 public Double getAccountBalance(String email) {
  return Double.valueOf(bankService.getBalanceByEmail(email));
 }
}

然后是Spring的XML配置文件,添加所需要的bean声明。
applicationContext.xml的源代码:

 <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans.xsd">
 <bean id="bankService" class="ua.eshepelyuk.blog.springtest.springockito.BankServiceImpl"/>
 <bean id="userBalanceService" class="ua.eshepelyuk.blog.springtest.springockito.UserBalanceServiceImpl"/>
</beans>

下面是测试类UserBalanceServiceImplTest.java的源代码:

 @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/springtest/springockito/applicationContext.xml")
public class UserBalanceServiceImplProfileTest {
 @Autowired
 private UserBalanceService userBalanceService;
 @Autowired
 private BankService bankService;
 @Test
 public void shouldReturnMockedBalance() {
  Double balance = userBalanceService.getAccountBalance("user@bank.com");
  assertEquals(balance, Double.valueOf(123.45D));
 }
}

如我们预料的一样,测试方法报UnsupportedOperationException异常。我们现在的目的是把BankService换成我们的模拟实现。直接使用Mockito来生成factory bean的方法是没问题的,但是有更好的选择,使用Springockito框架。继续之前可以先大概了解一下。

剩下的问题就简单了:如何让Spring注入模拟的bean而不是真实的bean,在Spring 3.1版之前除了新建一个XML配置文件之外没有其他的方法。但是自从Spring引入了bean的profile定义之后,我们有了更加优雅的解决方式,虽然这种方式也需要一个额外的专门用作测试的XML配置文件。下面是这个用来测试的配置文件testApplicationContext.xml的代码:

 <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:mockito="http://www.mockito.org/spring/mockito"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.mockito.org/spring/mockito https://bitbucket.org/kubek2k/springockito/raw/tip/springockito/src/main/resources/spring/mockito.xsd">
 <import resource="classpath:/springtest/springockito/applicationContext.xml"/>
 <beans profile="springTest">
  <mockito:mock id="bankService" class="ua.eshepelyuk.blog.springtest.springockito.BankService"/>
 </beans>
</beans>

做相应修改过之后的测试类UserBalanceServiceImplProfileTest.java的源代码:

 @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/springtest/springockito/testApplicationContext.xml")
@ActiveProfiles(profiles = {"springTest"})
public class UserBalanceServiceImplProfileTest {
 @Autowired
 private UserBalanceService userBalanceService;
 @Autowired
 private BankService bankService;
 @Before
 public void setUp() throws Exception {
  Mockito.when(bankService.getBalanceByEmail("user@bank.com")).thenReturn(String.valueOf(123.45D));
 }
 @Test
 public void shouldReturnMockedBalance() {
  Double balance = userBalanceService.getAccountBalance("user@bank.com");
  assertEquals(balance, Double.valueOf(123.45D));
 }
}

你可能注意到了,在setUp方法里,我们定义了模拟的行为,并且在类上面加了@Profile的注解。这个注解激活了名为springTest的profile,因此使用Springockito模拟的bean就可以自动注入到任何它所需要的地方了。这个测试的运行结果会成功,因为Spring注入了Springockito 所模拟的版本,而不是applicationContext.xml里所声明的版本。

继续优化我们的测试
 如果我们能将解决这个问题的方法更加推进一步的话,这篇文章看起来才没有缺憾。Springockito提供了另外一个名字叫作
 Springockito Annotation的框架,它允许我们在测试类中使用注解来注入模拟类。继续看下去之前,您最好先去网站上大概瞧瞧。好了,下面是经过修改后的测试代码。

 UserBalanceServiceImplAnnotationTest.java的源代码:
 @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = SpringockitoContextLoader.class,
 locations = "classpath:/springtest/springockito/applicationContext.xml")
public class UserBalanceServiceImplAnnotationTest {
 @Autowired
 private UserBalanceService userBalanceService;
 @Autowired
 @ReplaceWithMock
 private BankService bankService;
 @Before
 public void setUp() throws Exception {
  Mockito.when(bankService.getBalanceByEmail("user@bank.com")).thenReturn(String.valueOf(valueOf(123.45D)));
 }
 @Test
 public void shouldReturnMockedBalance() {
  Double balance = userBalanceService.getAccountBalance("user@bank.com");
  assertEquals(balance, valueOf(123.45D));
 }
}

请注意,这里并没有新引入的XML配置文件,而是直接使用了正式环境的applicationContext.xml。我们使用@ReplaceWithMock这个注解标记了类型为BankService的bean,而后在setUp方法中对模拟类的行为进行了定义。

后记
Springockito-annotations项目有个巨大的优点,那就是,它使我们的测试代码建立在依赖覆盖的基础之上,通过这样,我们既不需要定义额外的XML配置文件,也不需要为了测试而去改动生产环境的配置文件。如果不使用Springockito-annotations的话,我们除了定义额外的XML配置文件别无他选了。因此,我强烈建议您在集成测试中使用Springockito-annotations,这样你可以最大限度减少测试用例对生产代码的影响,也能消除维护额外XML配置文件的负担。
附言
为Spring工程写集成测试真是简单多了吧,文章中的代码参考自我的GitHub

译文链接:http://www.codeceo.com/article/spring-test-is-easy.html
英文原文:Test Me If You Can #1 (Spring Framework)
翻译作者:码农网 – Sandbox Wang

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • spring快速入门实例教程

    本文实例讲述了spring的基本配置与控制反转.分享给大家供大家参考.具体如下: 这里我们采用了maven构建java项目,没有采用maven的同样适用. 1. 创建maven项目,我创建的项目名称为springdemo. 2. 添加依赖包,我是通过maven添加的包,maven相关配置如下: 复制代码 代码如下: <dependency>     <groupId>org.springframework</groupId>     <artifactId>

  • SpringMVC文件上传 多文件上传实例

    必须明确告诉DispatcherServlet如何处理MultipartRequest.SpringMVC中提供了文件上传使用方式如下配置xxx-servlet.xml,添加如下代码: 复制代码 代码如下: <bean id="multipartResolver"  class="org.springframework.web.multipart.commons.CommonsMultipartResolver">          <!-- 设置

  • 读取spring配置文件的方法(spring读取资源文件)

    1.spring配置文件 复制代码 代码如下: <bean id="configproperties"          class="org.springframework.beans.factory.config.PropertiesFactoryBean">          <property name="location" value="classpath:jdbc.properties"/>

  • Spring MVC 框架搭建配置方法及详解

    现在主流的Web MVC框架除了Struts这个主力 外,其次就是Spring MVC了,因此这也是作为一名程序员需要掌握的主流框架,框架选择多了,应对多变的需求和业务时,可实行的方案自然就多了.不过要想灵活运用Spring MVC来应对大多数的Web开发,就必须要掌握它的配置及原理. 一.Spring MVC环境搭建:(Spring 2.5.6 + Hibernate 3.2.0) 1. jar包引入 Spring 2.5.6:spring.jar.spring-webmvc.jar.comm

  • 在Spring中用select last_insert_id()时遇到问题

    今天在使用MySQL时却不知如何处理,插入记录后不知怎样获得刚刚插入的id,查过文档后发现了select last_insert_id(),在插入之后执行此查询,即可获得自增id,喜出望外.可用到自己的程序中之后却得不到想要的结果,于是就怀疑到了Spring头上,因为通过基本JDBC测试是没有任何问题的,所以就去跟踪Spring JDBC, 看过源码之后才豁然开朗,原来Spring中如此获得数据库Connection的:Connection con = DataSourceUtils.getCo

  • Spring中的事务管理实例详解

    本文实例讲述了Spring中的事务管理.分享给大家供大家参考.具体分析如下: 事务简介: 事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性 事务就是一系列的动作,它们被当作一个单独的工作单元.这些动作要么全部完成,要么全部不起作用 事务的四个关键属性(ACID) ① 原子性(atomicity):事务室一个原子操作,有一系列动作组成.事务的原子性确保动作要么全部完成,要么完全不起作用 ② 一致性(consistency):一旦所有事务动作完成,事务就被提交.数据和资源就

  • struts2+spring+hibernate分页代码[比较多]第1/7页

    dao层接口: Java代码 复制代码 代码如下: package com.last999.im.news.dao; import java.util.*; import com.last999.im.news.entity.KindEntity; import com.last999.im.news.web.PageTool; public interface KindEntityDao{ public KindEntity get(String uuid); public void save

  • Spring事务管理只对出现运行期异常进行回滚

    一.结论 Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚. 如果一个方法抛出Exception或者Checked异常,Spring事务管理默认不进行回滚. 关于异常的分类一下详细介绍: 1.基本概念 看java的异常结构图  Throwable是所有异常的根,java.lang.Throwable Error是错误,java.lang.Error Exception是异常,java.lang.Exception 2.Excep

  • Spring实现文件上传(示例代码)

    在实际开发中,经常遇到要实现文件上传到服务器端的功能.Spring可以继承commons-fileupload插件来实现文件上传的功能.分为前端JSP编写和后台Controller的编写. 前期准备工作,首先要引入commons-fileupload这个jar包,pom.xml中的配置如下: 复制代码 代码如下: <!-- 实现文件上传,spring集成了这个功能 --><dependency> <groupId>commons-fileupload</group

  • java Spring整合Freemarker的详细步骤

    我的開發環境框架:springmvc開發工具:springsource-tool-suite-2.9.0版本:1.6.0_29tomcat版本:apache-tomcat-7.0.26前言:FreeMarker是一个用Java语言编写的模板引擎,它基于模板来生成文本输出.FreeMarker与Web容器无关,即在Web运行时,它并不知道Servlet或HTTP.它不仅可以用作表现层的实现技术,而且还可以用于生成XML,JSP或Java 等.簡而言之,Freemarker就是在Jave Web開發

随机推荐