簡介: 通過本文,您能夠在較短的時間內掌握使用 Spring 單元測試框架測試基於 Spring 的應用程序的方法,這套方法主要涵蓋如何使用 Spring 測試注釋來進行常見的 Junit4 或者 TestNG 的單元測試,同時支持訪問 Spring 的 beanFactory 和進行自動化的事務管理。
概述
單元測試和集成測試在我們的軟件開發整個流程中占有舉足輕重的地位,一方面,程序員通過編寫單元測試來驗證自己程序的有效性,另外一方面,管理者通過持續自動的執行單元測試和分析單元測試的覆蓋率等來確保軟件本身的質量。這裡,我們先不談單元測試本身的重要性,對於目前大多數的基於 Java 的企業應用軟件來說,Spring 已經成為了標准配置,一方面它實現了程序之間的低耦合度,另外也通過一些配置減少了企業軟件集成的工作量,例如和 Hibernate、Struts 等的集成。那麼,有個問題,在普遍使用 Spring 的應用程序中,我們如何去做單元測試?或者說,我們怎麼樣能高效的在 Spring 生態系統中實現各種單元測試手段?這就是本文章要告訴大家的事情。
單元測試目前主要的框架包括 Junit、TestNG,還有些 MOCK 框架,例如 Jmock、Easymock、PowerMock 等,這些都是單元測試的利器,但是當把他們用在 Spring 的開發環境中,還是那麼高效麼?還好,Spring 提供了單元測試的強大支持,主要特性包括:
通過閱讀本文,您能夠快速的掌握基於 Spring TestContext 框架的測試方法,並了解基本的實現原理。本文將提供大量測試標簽的使用方法,通過這些標簽,開發人員能夠極大的減少編碼工作量。OK,現在讓我們開始 Spring 的測試之旅吧!
回頁首
原來我們是怎麼做的
這裡先展示一個基於 Junit 的單元測試,這個單元測試運行在基於 Spring 的應用程序中,需要使用 Spring 的相關配置文件來進行測試。相關類圖如下:
數據庫表
假設有一個員工賬號表,保存了員工的基本賬號信息,表結構如下:
假設表已經建好,且內容為空。
測試工程目錄結構和依賴 jar 包
在 Eclipse 中,我們可以展開工程目錄結構,看到如下圖所示的工程目錄結構和依賴的 jar 包列表:
其中的 hsqldb 是我們測試用數據庫。
圖 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 定義了幾個賬號對象的數據庫訪問方法:
清單 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 包括下列方法:
其依賴的 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 來保證賬號信息會被清除。