使用 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 包列表:
图 1. 工程目录结构
类总体介绍
假设我们现在有一个基于 Spring 的应用程序,除了 MVC 层,还包括业务层和数据访问层,业务层有一个类 AccountService,负责处理账号类的业务,其依赖于数据访问层 AccountDao 类,此类提供了基于 Spring Jdbc Template 实现的数据库访问方法,AccountService 和 AccountDao 以及他们之间的依赖关系都是通过 Spring 配置文件进行管理的。
现在我们要对 AccountService 类进行测试,在不使用 Spring 测试方法之前,我们需要这样做:
清单 1. Account.Java
此类代表账号的基本信息,提供 getter 和 setter 方法。
package domain; public class Account { public static final String SEX_MALE = "male"; public static final String SEX_FEMALE = "female"; private int id; private String name; private int age; private String sex; public String toString() { return String.format("Account[id=%d,name=%s,age:%d,sex:%s]",id,name,age,sex); } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public static Account getAccount(int id,String name,int age,String sex) { Account acct = new Account(); acct.setId(id); acct.setName(name); acct.setAge(age); acct.setSex(sex); return acct; } } |
注意上面的 Account 类有一个 toString() 方法和一个静态的 getAccount 方法,getAccount 方法用于快速获取 Account 测试对象。
清单 2. AccountDao.Java
这个 DAO 我们这里为了简单起见,采用 Spring Jdbc Template 来实现。
package DAO; import Java.sql.ResultSet; import Java.sql.SQLException; import Java.util.HashMap; import Java.util.List; import Java.util.Map; import org.Springframework.context.ApplicationContext; import org.Springframework.context.support.ClassPathXmlApplicationContext; import org.Springframework.jdbc.core.RowMapper; import org.Springframework.jdbc.core.namedparam.NamedParameterJdbcDaoSupport; import org.Springframework.jdbc.core.simple.ParameterizedRowMapper; import domain.Account; public class AccountDao extends NamedParameterJdbcDaoSupport { public void saveAccount(Account account) { String sql = "insert into tbl_account(id,name,age,sex) " + "values(:id,:name,:age,:sex)"; Map paramMap = new HashMap(); paramMap.put("id", account.getId()); paramMap.put("name", account.getName()); paramMap.put("age", account.getAge()); paramMap.put("sex",account.getSex()); getNamedParameterJdbcTemplate().update(sql, paramMap); } public Account getAccountById(int id) { String sql = "select id,name,age,sex from tbl_account where id=:id"; Map paramMap = new HashMap(); paramMap.put("id", id); List<Account> matches = getNamedParameterJdbcTemplate().query(sql, paramMap,new ParameterizedRowMapper<Account>() { @Override public Account mapRow(ResultSet rs, int rowNum) throws SQLException { Account a = new Account(); a.setId(rs.getInt(1)); a.setName(rs.getString(2)); a.setAge(rs.getInt(3)); a.setSex(rs.getString(4)); return a; } }); return matches.size()>0?matches.get(0):null; } } |
AccountDao 定义了几个账号对象的数据库访问方法:
- saveAccount:负责把传入的账号对象入库
- getAccountById:负责根据 Id 查询账号
清单 3. AccountService.Java
package service; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.Springframework.beans.factory.annotation.Autowired; import DAO.AccountDao; import domain.Account; public class AccountService { private static final Log log = LogFactory.getLog(AccountService.class); @Autowired private AccountDao accountDao; public Account getAccountById(int id) { return accountDao.getAccountById(id); } public void insertIfNotExist(Account account) { Account acct = accountDao.getAccountById(account.getId()); if(acct==null) { log.debug("No "+account+" found,would insert it."); accountDao.saveAccount(account); } acct = null; } } |
AccountService 包括下列方法:
- getAccountById:根据 Id 查询账号信息
- insertIfNotExist:根据传入的对象插入数据库
其依赖的 DAO 对象 accountDao 是通过 Spring 注释标签 @Autowired 自动注入的。
清单 4. Spring 配置文件
上述几个类的依赖关系是通过 Spring 进行管理的,配置文件如下:
<beans xmlns="http://www.Springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.Springframework.org/schema/context" xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans-3.0.xsd http://www.Springframework.org/schema/context http://www.Springframework.org/schema/context/Spring-context-3.0.xsd "> <context:annotation-config/> <bean id="datasource" class=" org.Springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver" /> <property name="url" value="jdbc:hsqldb:hsql://localhost" /> <property name="username" value="sa" /> <property name="password" value="" /> </bean> <bean id="initer" init-method="init" class="service.Initializer"> </bean> <bean id="accountDao" depends-on="initer" class="DAO.AccountDao"> <property name="dataSource" ref="datasource" /> </bean> <bean id="accountService" class="service.AccountService"> </bean> </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
package service; import static org.Junit.Assert.assertEquals; import org.Junit.BeforeClass; import org.Junit.Test; import org.Springframework.context.ApplicationContext; import org.Springframework.context.support.ClassPathXmlApplicationContext; import domain.Account; public class AccountServiceOldTest { private static AccountService service; @BeforeClass public static void init() { ApplicationContext context = new ClassPathXmlApplicationContext("config/Spring-db-old.xml"); service = (AccountService)context.getBean("accountService"); } @Test public void testGetAcccountById() { Account acct = Account.getAccount(1, "user01", 18, "M"); Account acct2 = null; try { service.insertIfNotExist(acct); acct2 = service.getAccountById(1); assertEquals(acct, acct2); } catch (Exception ex) { fail(ex.getMessage()); } finally { service.removeAccount(acct); } } } |
注意上面的 Junit4 注释标签,第一个注释标签 @BeforeClass,用来执行整个测试类需要一次性初始化的环境,这里我们用 Spring 的 ClassPathXmlApplicationContext 从 XML 文件中加载了上面定义的 Spring 配置文件,并从中获得了 accountService 的实例。第二个注释标签 @Test 用来进行实际的测试。
测试过程:我们先获取一个 Account 实例对象,然后通过 service bean 插入数据库中,然后通过 getAccountById 方法从数据库再查询这个记录,如果能获取,则判断两者的相等性;如果相同,则表示测试成功。成功后,我们尝试删除这个记录,以利于下一个测试的进行,这里我们用了 try-catch-finally 来保证账号信息会被清除。
相关阅读:
执行测试:(在 Eclipse 中,右键选择 AccountServiceOldTest 类,点击 Run as Junit test 选项),得到的结果如下: