qileilove

blog已经转移至github,大家请访问 http://qaseven.github.io/

使用Spring进行单元测试(上)

 简介:通过本文,您能够在较短的时间内掌握使用 Spring 单元测试框架测试基于 Spring 的应用程序的方法,这套方法主要涵盖如何使用 Spring 测试注释来进行常见的 Junit4 或者 TestNG 的单元测试,同时支持访问 Spring 的 beanFactory 和进行自动化的事务管理。

  概述

  单元测试和集成测试在我们的软件开发整个流程中占有举足轻重的地位,一方面,程序员通过编写单元测试来验证自己程序的有效性,另外一方面,管理者通过持续自动的执行单元测试和分析单元测试的覆盖率等来确保软件本身的质量。这里,我们先不谈单元测试本身的重要性,对于目前大多数的基于 Java 的企业应用软件来说,Spring 已经成为了标准配置,一方面它实现了程序之间的低耦合度,另外也通过一些配置减少了企业软件集成的工作量,例如和 Hibernate、Struts 等的集成。那么,有个问题,在普遍使用 Spring 的应用程序中,我们如何去做单元测试?或者说,我们怎么样能高效的在 Spring 生态系统中实现各种单元测试手段?这就是本文章要告诉大家的事情。

  单元测试目前主要的框架包括 Junit、TestNG,还有些 MOCK 框架,例如 Jmock、Easymock、PowerMock 等,这些都是单元测试的利器,但是当把他们用在 Spring 的开发环境中,还是那么高效么?还好,Spring 提供了单元测试的强大支持,主要特性包括:

  ● 支持主流的测试框架 Junit 和 TestNG

  ● 支持在测试类中使用依赖注入 Denpendency Injection

  ● 支持测试类的自动化事务管理

  ● 支持使用各种注释标签,提高开发效率和代码简洁性

  ● Spring 3.1 更是支持在测试类中使用非 XML 配置方法和基于 Profile 的 bean 配置模式

  通过阅读本文,您能够快速的掌握基于 Spring TestContext 框架的测试方法,并了解基本的实现原理。本文将提供大量测试标签的使用方法,通过这些标签,开发人员能够极大的减少编码工作量。OK,现在让我们开始 Spring 的测试之旅吧!

  原来我们是怎么做的

  这里先展示一个基于 Junit 的单元测试,这个单元测试运行在基于 Spring 的应用程序中,需要使用 Spring 的相关配置文件来进行测试。相关类图如下:

  数据库

  假设有一个员工账号表,保存了员工的基本账号信息,表结构如下:

  ● ID:整数类型,唯一标识

  ● NAME:字符串,登录账号

  ● SEX:字符串,性别

  ● AGE:字符串,年龄

  假设表已经建好,且内容为空。

  测试工程目录结构和依赖 jar 包

  在 Eclipse 中,我们可以展开工程目录结构,看到如下图所示的工程目录结构和依赖的 jar 包列表:

  您需要引入的 jar 包括:

  ● cglib-nodep-2.2.3.jar

  ● commons-logging.jar

  ● hsqldb.jar

  ● Junit-4.5.jar

  ● log4j-1.2.14.jar

  ● Spring-asm-3.2.0.M1.jar

  ● Spring-beans-3.2.0.M1.jar

  ● Spring-context-3.2.0.M1.jar

  ● Spring-core-3.2.0.M1.jar

  ● Spring-expression-3.2.0.M1.jar

  ● Spring-jdbc-3.2.0.M1.jar

  ● Spring-test-3.2.0.M1.jar

  ● Spring-tx-3.2.0.M1.jar

  ● testng-6.8.jar

  其中的 hsqldb 是我们测试用数据库。

  图 1. 工程目录结构




类总体介绍

  假设我们现在有一个基于 Spring 的应用程序,除了 MVC 层,还包括业务层和数据访问层,业务层有一个类 AccountService,负责处理账号类的业务,其依赖于数据访问层 AccountDao 类,此类提供了基于 Spring Jdbc Template 实现的数据库访问方法,AccountService 和 AccountDao 以及他们之间的依赖关系都是通过 Spring 配置文件进行管理的。

  现在我们要对 AccountService 类进行测试,在不使用 Spring 测试方法之前,我们需要这样做:

  清单 1. Account.Java

  此类代表账号的基本信息,提供 getter 和 setter 方法。

  1. package domain;  
  2.    
  3. public class Account {  
  4.     public static final String SEX_MALE = "male";  
  5.     public static final String SEX_FEMALE = "female";  
  6.    
  7.     private int id;  
  8.     private String name;  
  9.     private int age;  
  10.     private String sex;  
  11.     public String toString() {  
  12.        return String.format("Account[id=%d,name=%s,age:%d,sex:%s]",id,name,age,sex);  
  13.     }  
  14.     public int getId() {  
  15.         return id;  
  16.     }  
  17.     public void setId(int id) {  
  18.         this.id = id;  
  19.     }  
  20.     public String getName() {  
  21.         return name;  
  22.     }  
  23.     public void setName(String name) {  
  24.         this.name = name;  
  25.     }  
  26.     public int getAge() {  
  27.         return age;  
  28.     }  
  29.     public void setAge(int age) {  
  30.         this.age = age;  
  31.     }  
  32.     public String getSex() {  
  33.         return sex;  
  34.     }  
  35.     public void setSex(String sex) {  
  36.         this.sex = sex;  
  37.     }  
  38.    
  39.     public static Account getAccount(int id,String name,int age,String sex) {  
  40.         Account acct = new Account();  
  41.         acct.setId(id);  
  42.         acct.setName(name);  
  43.         acct.setAge(age);  
  44.         acct.setSex(sex);  
  45.         return acct;  
  46.     }  
  47. }

  注意上面的 Account 类有一个 toString() 方法和一个静态的 getAccount 方法,getAccount 方法用于快速获取 Account 测试对象。

  清单 2. AccountDao.Java

  这个 DAO 我们这里为了简单起见,采用 Spring Jdbc Template 来实现。

  1. package DAO;  
  2.    
  3. import Java.sql.ResultSet;  
  4. import Java.sql.SQLException;  
  5. import Java.util.HashMap;  
  6. import Java.util.List;  
  7. import Java.util.Map;  
  8.    
  9. import org.Springframework.context.ApplicationContext;  
  10. import org.Springframework.context.support.ClassPathXmlApplicationContext;  
  11. import org.Springframework.jdbc.core.RowMapper;  
  12. import org.Springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport;  
  13. import org.Springframework.jdbc.core.simple.ParameterizedRowMapper;  
  14.    
  15. import domain.Account;  
  16.    
  17. public class AccountDao extends NamedParameterJdbcDaoSupport {  
  18.     public void saveAccount(Account account) {  
  19.         String sql = "insert into tbl_account(id,name,age,sex) " +  
  20.                "values(:id,:name,:age,:sex)";  
  21.         Map paramMap = new HashMap();  
  22.         paramMap.put("id", account.getId());  
  23.         paramMap.put("name", account.getName());  
  24.         paramMap.put("age", account.getAge());  
  25.         paramMap.put("sex",account.getSex());  
  26.         getNamedParameterJdbcTemplate().update(sql, paramMap);  
  27.     }  
  28.    
  29.     public Account getAccountById(int id) {  
  30.         String sql = "select id,name,age,sex from tbl_account where id=:id";  
  31.         Map paramMap = new HashMap();  
  32.         paramMap.put("id", id);  
  33.         List<Account> matches = getNamedParameterJdbcTemplate().query(sql,  
  34.         paramMap,new ParameterizedRowMapper<Account>() {  
  35.                     @Override 
  36.                     public Account mapRow(ResultSet rs, int rowNum)  
  37.                             throws SQLException {  
  38.                         Account a = new Account();  
  39.                         a.setId(rs.getInt(1));  
  40.                         a.setName(rs.getString(2));  
  41.                         a.setAge(rs.getInt(3));  
  42.                         a.setSex(rs.getString(4));  
  43.                         return a;  
  44.                     }  
  45.    
  46.         });  
  47.         return matches.size()>0?matches.get(0):null;  
  48.     }  
  49.    
  50. }

  AccountDao 定义了几个账号对象的数据库访问方法:

  ● saveAccount:负责把传入的账号对象入库
  ● getAccountById:负责根据 Id 查询账号



 清单 3. AccountService.Java

  1. package service;  
  2.    
  3. import org.apache.commons.logging.Log;  
  4. import org.apache.commons.logging.LogFactory;  
  5. import org.Springframework.beans.factory.annotation.Autowired;  
  6.    
  7. import DAO.AccountDao;  
  8. import domain.Account;  
  9.    
  10. public class AccountService {  
  11.     private static final Log log = LogFactory.getLog(AccountService.class);  
  12.    
  13.     @Autowired 
  14.     private AccountDao accountDao;  
  15.    
  16.     public Account getAccountById(int id) {  
  17.         return accountDao.getAccountById(id);  
  18.     }  
  19.    
  20.     public void insertIfNotExist(Account account) {  
  21.         Account acct = accountDao.getAccountById(account.getId());  
  22.         if(acct==null) {  
  23.             log.debug("No "+account+" found,would insert it.");  
  24.             accountDao.saveAccount(account);  
  25.         }  
  26.         acct = null;  
  27.     }  
  28.    
  29. }

  AccountService 包括下列方法:

  ● getAccountById:根据 Id 查询账号信息
  ● insertIfNotExist:根据传入的对象插入数据库

  其依赖的 DAO 对象 accountDao 是通过 Spring 注释标签 @Autowired 自动注入的。

  清单 4. Spring 配置文件

  上述几个类的依赖关系是通过 Spring 进行管理的,配置文件如下:

  1. <beans xmlns="http://www.Springframework.org/schema/beans" 
  2.      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  3.  xmlns:context="http://www.Springframework.org/schema/context" 
  4.  xsi:schemaLocation="http://www.Springframework.org/schema/beans  
  5.    
  6. http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd  
  7.    
  8.    
  9. http://www.Springframework.org/schema/context  
  10.    
  11.  http://www.Springframework.org/schema/context/Spring-context-3.0.xsd "> 
  12.    
  13.  <context:annotation-config/> 
  14.  <bean id="datasource"> 
  15.          <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
  16.          <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
  17.          <property name="username" value="sa" /> 
  18.          <property name="password" value="" /> 
  19.      </bean> 
  20.      <bean id="initer" init-method="init"> 
  21.      </bean> 
  22.  <bean id="accountDao" depends-on="initer"> 
  23.          <property name="dataSource" ref="datasource" /> 
  24.      </bean> 
  25.  <bean id="accountService"> 
  26.      </bean> 
  27.  </beans>

  注意其中的“<context:annotation-config/>”的作用,这个配置启用了 Spring 对 Annotation 的支持,这样在我们的测试类中 @Autowired 注释才会起作用(如果用了 Spring 测试框架,则不需要这样的配置项,稍后会演示)。另外还有一个 accountDao 依赖的 initer bean, 这个 bean 的作用是加载 log4j 日志环境,不是必须的。

  另外还有一个要注意的地方,就是 datasource 的定义,由于我们使用的是 Spring Jdbc Template,所以只要定义一个 org.Springframework.jdbc.datasource.DriverManagerDataSource 类型的 datasource 即可。这里我们使用了简单的数据库 HSQL、Single Server 运行模式,通过 JDBC 进行访问。实际测试中,大家可以选择 Oracle 或者 DB2、Mysql 等。

  好,万事具备,下面我们来用 Junit4 框架测试 accountService 类。代码如下:

  清单 5. AccountServiceOldTest.Java

  1. package service;  
  2.    
  3. import static org.Junit.Assert.assertEquals;  
  4.    
  5. import org.Junit.BeforeClass;  
  6. import org.Junit.Test;  
  7. import org.Springframework.context.ApplicationContext;  
  8. import org.Springframework.context.support.ClassPathXmlApplicationContext;  
  9.    
  10. import domain.Account;  
  11.    
  12. public class AccountServiceOldTest {  
  13.     private static AccountService service;  
  14.    
  15.     @BeforeClass 
  16.     public static void init() {  
  17.         ApplicationContext  
  18. context = new ClassPathXmlApplicationContext("config/Spring-db-old.xml");  
  19.         service = (AccountService)context.getBean("accountService");  
  20.     }   
  21.    
  22.     @Test 
  23.     public void testGetAcccountById() {  
  24. Account acct = Account.getAccount(1"user01"18"M");  
  25.         Account acct2 = null;  
  26.         try {  
  27. service.insertIfNotExist(acct);  
  28.             acct2 = service.getAccountById(1);  
  29.             assertEquals(acct, acct2);  
  30.         } catch (Exception ex) {  
  31.             fail(ex.getMessage());  
  32.         } finally {  
  33.             service.removeAccount(acct);  
  34.         }  
  35. }  
  36. }

  注意上面的 Junit4 注释标签,第一个注释标签 @BeforeClass,用来执行整个测试类需要一次性初始化的环境,这里我们用 Spring 的 ClassPathXmlApplicationContext 从 XML 文件中加载了上面定义的 Spring 配置文件,并从中获得了 accountService 的实例。第二个注释标签 @Test 用来进行实际的测试。

  测试过程:我们先获取一个 Account 实例对象,然后通过 service bean 插入数据库中,然后通过 getAccountById 方法从数据库再查询这个记录,如果能获取,则判断两者的相等性;如果相同,则表示测试成功。成功后,我们尝试删除这个记录,以利于下一个测试的进行,这里我们用了 try-catch-finally 来保证账号信息会被清除。

  执行测试:(在 Eclipse 中,右键选择 AccountServiceOldTest 类,点击 Run as Junit test 选项),得到的结果如下:

  执行测试的结果

  在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

  图 2. 测试的结果

  对于这种不使用 Spring test 框架进行的单元测试,我们注意到,需要做这些工作:

  ● 在测试开始之前,需要手工加载 Spring 的配置文件,并获取需要的 bean 实例

  ● 在测试结束的时候,需要手工清空搭建的数据库环境,比如清除您插入或者更新的数据,以保证对下一个测试没有影响

  另外,在这个测试类中,我们还不能使用 Spring 的依赖注入特性。一切都靠手工编码实现。好,那么我们看看 Spring test 框架能做到什么。

  首先我们修改一下 Spring 的 XML 配置文件,删除 <context:annotation-config/> 行,其他不变。

  清单 6. Spring-db1.xml

  1. <beans xmlns="http://www.Springframework.org/schema/beans" 
  2.      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  3.  xsi:schemaLocation="http://www.Springframework.org/schema/beans  
  4.    
  5. http://www.Springframework.org/schema/beans/Spring-beans-3.2.xsd"> 
  6.    
  7.  <bean id="datasource" 
  8. > 
  9.          <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> 
  10.          <property name="url" value="jdbc:hsqldb:hsql://localhost" /> 
  11.          <property name="username" value="sa"/> 
  12.          <property name="password" value=""/> 
  13.      </bean> 
  14.  <bean id="transactionManager" 
  15. > 
  16.          <property name="dataSource" ref="datasource"></property> 
  17.      </bean> 
  18.      <bean id="initer" init-method="init"> 
  19.      </bean> 
  20.  <bean id="accountDao" depends-on="initer"> 
  21.          <property name="dataSource" ref="datasource"/> 
  22.      </bean> 
  23.      <bean id="accountService"> 
  24.      </bean> 
  25.  </beans>

  其中的 transactionManager 是 Spring test 框架用来做事务管理的管理器。

  清单 7. AccountServiceTest1.Java

  1. package service;  
  2. import static org.Junit.Assert.assertEquals;  
  3.    
  4. import org.Junit.Test;  
  5. import org.Junit.runner.RunWith;  
  6. import org.Springframework.beans.factory.annotation.Autowired;  
  7. import org.Springframework.test.context.ContextConfiguration;  
  8. import org.Springframework.test.context.Junit4.SpringJUnit4ClassRunner;  
  9. import org.Springframework.transaction.annotation.Transactional;  
  10.    
  11. import domain.Account;  
  12.    
  13. @RunWith(SpringJUnit4ClassRunner.class)  
  14. @ContextConfiguration("/config/Spring-db1.xml")  
  15. @Transactional 
  16. public class AccountServiceTest1 {  
  17.     @Autowired 
  18.     private AccountService service;  
  19.    
  20.     @Test 
  21.     public void testGetAcccountById() {  
  22. Account acct = Account.getAccount(1"user01"18"M");  
  23.         service.insertIfNotExist(acct);  
  24.         Account acct2 = service.getAccountById(1);  
  25.         assertEquals(acct,acct2);  
  26.     }  
  27. }

  对这个类解释一下:

  ● @RunWith 注释标签是 Junit 提供的,用来说明此测试类的运行者,这里用了 SpringJUnit4ClassRunner,这个类是一个针对 Junit 运行环境的自定义扩展,用来标准化在 Spring 环境中 Junit4.5 的测试用例,例如支持的注释标签的标准化

  ● @ContextConfiguration 注释标签是 Spring test context 提供的,用来指定 Spring 配置信息的来源,支持指定 XML 文件位置或者 Spring 配置类名,这里我们指定 classpath 下的 /config/Spring-db1.xml 为配置文件的位置

  ● @Transactional 注释标签是表明此测试类的事务启用,这样所有的测试方案都会自动的 rollback,即您不用自己清除自己所做的任何对数据库的变更了

  ● @Autowired 体现了我们的测试类也是在 Spring 的容器中管理的,他可以获取容器的 bean 的注入,您不用自己手工获取要测试的 bean 实例了

  ● testGetAccountById 是我们的测试用例:注意和上面的 AccountServiceOldTest 中相同的测试方法的对比,这里我们不用再 try-catch-finally 了,事务管理自动运行,当我们执行完成后,所有相关变更会被自动清除

  执行结果

  在 Eclipse 的 Junit 视图中,我们可以看到如下的结果:

  图 3. 执行结果

  小结

  如果您希望在 Spring 环境中进行单元测试,那么可以做如下配置:

  ● 继续使用 Junit4 测试框架,包括其 @Test 注释标签和相关的类和方法的定义,这些都不用变

  ● 您需要通过 @RunWith(SpringJUnit4ClassRunner.class) 来启动 Spring 对测试类的支持

  ● 您需要通过 @ContextConfiguration 注释标签来指定 Spring 配置文件或者配置类的位置

  ● 您需要通过 @Transactional 来启用自动的事务管理

  ● 您可以使用 @Autowired 自动织入 Spring 的 bean 用来测试

  另外您不再需要:

  ● 手工加载 Spring 的配置文件

  ● 手工清理数据库的每次变更

  ● 手工获取 application context 然后获取 bean 实例

posted on 2013-06-06 10:46 顺其自然EVO 阅读(3090) 评论(0)  编辑  收藏 所属分类: 测试学习专栏


只有注册用户登录后才能发表评论。


网站导航:
 
<2013年6月>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456

导航

统计

常用链接

留言簿(55)

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜