掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
簡介: 通過本文,您能夠在較短的時間內(nèi)掌握使用 Spring 單元測試框架測試基于 Spring 的應(yīng)用程序的方法,這套方法主要涵蓋如何使用 Spring 測試注釋來進(jìn)行常見的 Junit4 或者 TestNG 的單元測試,同時支持訪問 Spring 的 beanFactory 和進(jìn)行自動化的事務(wù)管理。

概述
單元測試和集成測試在我們的軟件開發(fā)整個流程中占有舉足輕重的地位,一方面,程序員通過編寫單元測試來驗(yàn)證自己程序的有效性,另外一方面,管理者通過持續(xù)自動的執(zhí)行單元測試和分析單元測試的覆蓋率等來確保軟件本身的質(zhì)量。這里,我們先不談單元測試本身的重要性,對于目前大多數(shù)的基于 Java 的企業(yè)應(yīng)用軟件來說,Spring 已經(jīng)成為了標(biāo)準(zhǔn)配置,一方面它實(shí)現(xiàn)了程序之間的低耦合度,另外也通過一些配置減少了企業(yè)軟件集成的工作量,例如和 Hibernate、Struts 等的集成。那么,有個問題,在普遍使用 Spring 的應(yīng)用程序中,我們?nèi)绾稳プ鰡卧獪y試?或者說,我們怎么樣能高效的在 Spring 生態(tài)系統(tǒng)中實(shí)現(xiàn)各種單元測試手段?這就是本文章要告訴大家的事情。
單元測試目前主要的框架包括 Junit、TestNG,還有些 MOCK 框架,例如 Jmock、Easymock、PowerMock 等,這些都是單元測試的利器,但是當(dāng)把他們用在 Spring 的開發(fā)環(huán)境中,還是那么高效么?還好,Spring 提供了單元測試的強(qiáng)大支持,主要特性包括:
通過閱讀本文,您能夠快速的掌握基于 Spring TestContext 框架的測試方法,并了解基本的實(shí)現(xiàn)原理。本文將提供大量測試標(biāo)簽的使用方法,通過這些標(biāo)簽,開發(fā)人員能夠極大的減少編碼工作量。OK,現(xiàn)在讓我們開始 Spring 的測試之旅吧!
原來我們是怎么做的
這里先展示一個基于 Junit 的單元測試,這個單元測試運(yùn)行在基于 Spring 的應(yīng)用程序中,需要使用 Spring 的相關(guān)配置文件來進(jìn)行測試。相關(guān)類圖如下:
數(shù)據(jù)庫表
假設(shè)有一個員工賬號表,保存了員工的基本賬號信息,表結(jié)構(gòu)如下:
假設(shè)表已經(jīng)建好,且內(nèi)容為空。
測試工程目錄結(jié)構(gòu)和依賴 jar 包
在 Eclipse 中,我們可以展開工程目錄結(jié)構(gòu),看到如下圖所示的工程目錄結(jié)構(gòu)和依賴的 jar 包列表:
您需要引入的 jar 包括:
其中的 hsqldb 是我們測試用數(shù)據(jù)庫。
圖 1. 工程目錄結(jié)構(gòu)
類總體介紹
假設(shè)我們現(xiàn)在有一個基于 Spring 的應(yīng)用程序,除了 MVC 層,還包括業(yè)務(wù)層和數(shù)據(jù)訪問層,業(yè)務(wù)層有一個類 AccountService,負(fù)責(zé)處理賬號類的業(yè)務(wù),其依賴于數(shù)據(jù)訪問層 AccountDao 類,此類提供了基于 Spring Jdbc Template 實(shí)現(xiàn)的數(shù)據(jù)庫訪問方法,AccountService 和 AccountDao 以及他們之間的依賴關(guān)系都是通過 Spring 配置文件進(jìn)行管理的。
現(xiàn)在我們要對 AccountService 類進(jìn)行測試,在不使用 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() 方法和一個靜態(tài)的 getAccount 方法,getAccount 方法用于快速獲取 Account 測試對象。
清單 2. AccountDao.Java
這個 DAO 我們這里為了簡單起見,采用 Spring Jdbc Template 來實(shí)現(xiàn)。
- 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
matches = getNamedParameterJdbcTemplate().query(sql, - paramMap,new ParameterizedRowMapper
() { - @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 定義了幾個賬號對象的數(shù)據(jù)庫訪問方法:
#p#
清單 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 注釋標(biāo)簽 @Autowired 自動注入的。
清單 4. Spring 配置文件
上述幾個類的依賴關(guān)系是通過 Spring 進(jìn)行管理的,配置文件如下:
- 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 ">
注意其中的“
另外還有一個要注意的地方,就是 datasource 的定義,由于我們使用的是 Spring Jdbc Template,所以只要定義一個 org.Springframework.jdbc.datasource.DriverManagerDataSource 類型的 datasource 即可。這里我們使用了簡單的數(shù)據(jù)庫 HSQL、Single Server 運(yùn)行模式,通過 JDBC 進(jìn)行訪問。實(shí)際測試中,大家可以選擇 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 注釋標(biāo)簽,***個注釋標(biāo)簽 @BeforeClass,用來執(zhí)行整個測試類需要一次性初始化的環(huán)境,這里我們用 Spring 的 ClassPathXmlApplicationContext 從 XML 文件中加載了上面定義的 Spring 配置文件,并從中獲得了 accountService 的實(shí)例。第二個注釋標(biāo)簽 @Test 用來進(jìn)行實(shí)際的測試。
測試過程:我們先獲取一個 Account 實(shí)例對象,然后通過 service bean 插入數(shù)據(jù)庫中,然后通過 getAccountById 方法從數(shù)據(jù)庫再查詢這個記錄,如果能獲取,則判斷兩者的相等性;如果相同,則表示測試成功。成功后,我們嘗試刪除這個記錄,以利于下一個測試的進(jìn)行,這里我們用了 try-catch-finally 來保證賬號信息會被清除。
執(zhí)行測試:(在 Eclipse 中,右鍵選擇 AccountServiceOldTest 類,點(diǎn)擊 Run as Junit test 選項(xiàng)),得到的結(jié)果如下:
執(zhí)行測試的結(jié)果
在 Eclipse 的 Junit 視圖中,我們可以看到如下的結(jié)果:
圖 2. 測試的結(jié)果
對于這種不使用 Spring test 框架進(jìn)行的單元測試,我們注意到,需要做這些工作:
另外,在這個測試類中,我們還不能使用 Spring 的依賴注入特性。一切都靠手工編碼實(shí)現(xiàn)。好,那么我們看看 Spring test 框架能做到什么。
首先我們修改一下 Spring 的 XML 配置文件,刪除
清單 6. Spring-db1.xml
- 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-3.2.xsd">
- >
- >
其中的 transactionManager 是 Spring test 框架用來做事務(wù)管理的管理器。
清單 7. AccountServiceTest1.Java
- package service;
- import static org.Junit.Assert.assertEquals;
- import org.Junit.Test;
- import org.Junit.runner.RunWith;
- import org.Springframework.beans.factory.annotation.Autowired;
- import org.Springframework.test.context.ContextConfiguration;
- import org.Springframework.test.context.Junit4.SpringJUnit4ClassRunner;
- import org.Springframework.transaction.annotation.Transactional;
- import domain.Account;
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration("/config/Spring-db1.xml")
- @Transactional
- public class AccountServiceTest1 {
- @Autowired
- private AccountService service;
- @Test
- public void testGetAcccountById() {
- Account acct = Account.getAccount(1, "user01", 18, "M");
- service.insertIfNotExist(acct);
- Account acct2 = service.getAccountById(1);
- assertEquals(acct,acct2);
- }
- }
對這個類解釋一下:
執(zhí)行結(jié)果
在 Eclipse 的 Junit 視圖中,我們可以看到如下的結(jié)果:
圖 3. 執(zhí)行結(jié)果
小結(jié)
如果您希望在 Spring 環(huán)境中進(jìn)行單元測試,那么可以做如下配置:
另外您不再需要:
Spring 測試注釋標(biāo)簽
我們已經(jīng)看到利用 Spring test framework 來進(jìn)行基于 Junit4 的單元測試是多么的簡單,下面我們來看一下前面遇到的各種注釋標(biāo)簽的一些可選用法。
@ContextConfiguration 和 @Configuration 的使用
剛才已經(jīng)介紹過,可以輸入 Spring xml 文件的位置,Spring test framework 會自動加載 XML 文件,得到 application context,當(dāng)然也可以使用 Spring 3.0 新提供的特性 @Configuration,這個注釋標(biāo)簽允許您用 Java 語言來定義 bean 實(shí)例,舉個例子:
現(xiàn)在我們將前面定義的 Spring-db1.xml 進(jìn)行修改,我們希望其中的三個 bean:initer、accountDao、accountService 通過配置類來定義,而不是 XML,則我們需要定義如下配置類:
注意:如果您想使用 @Configuration,請?jiān)?classpath 中加入 cglib 的 jar 包(cglib-nodep-2.2.3.jar),否則會報(bào)錯。
#p#
清單 8. SpringDb2Config.Java
- package config;
- import org.Springframework.beans.factory.annotation.Autowired;
- import org.Springframework.context.annotation.Bean;
- import org.Springframework.context.annotation.Configuration;
- import org.Springframework.jdbc.datasource.DriverManagerDataSource;
- import service.AccountService;
- import service.Initializer;
- import DAO.AccountDao;
- @Configuration
- public class SpringDb2Config {
- private @Autowired DriverManagerDataSource datasource;
- @Bean
- public Initializer initer() {
- return new Initializer();
- }
- @Bean
- public AccountDao accountDao() {
- AccountDao DAO = new AccountDao();
- DAO.setDataSource(datasource);
- return DAO;
- }
- @Bean
- public AccountService accountService() {
- return new AccountService();
- }
- }
注意上面的注釋標(biāo)簽:
注意,我們采用的是 XML+config bean 的方式進(jìn)行配置,這種方式比較符合實(shí)際項(xiàng)目的情況。相關(guān)的 Spring 配置文件也要做變化,如下清單所示:
清單 9. Spring-db2.xml
- 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 命名空間的定義,如代碼中黑體字所示。另外還必須有
現(xiàn)在有了這些配置,我們的測試類只要稍稍修改一下,即可實(shí)現(xiàn)加載配置類的效果,如下:
- @ContextConfiguration("/config/Spring-db2.xml")
通過上面的配置,測試用例就可以實(shí)現(xiàn)加載 Spring 配置類,運(yùn)行結(jié)果也是成功的 green bar。
@DirtiesContext
缺省情況下,Spring 測試框架一旦加載 applicationContext 后,將一直緩存,不會改變,但是,
由于 Spring 允許在運(yùn)行期修改 applicationContext 的定義,例如在運(yùn)行期獲取 applicationContext,然后調(diào)用 registerSingleton 方法來動態(tài)的注冊新的 bean,這樣的情況下,如果我們還使用 Spring 測試框架的被修改過 applicationContext,則會帶來測試問題,我們必須能夠在運(yùn)行期重新加載 applicationContext,這個時候,我們可以在測試類或者方法上注釋:@DirtiesContext,作用如下:
@TransactionConfiguration 和 @Rollback
缺省情況下,Spring 測試框架將事務(wù)管理委托到名為 transactionManager 的 bean 上,如果您的事務(wù)管理器不是這個名字,那需要指定 transactionManager 屬性名稱,還可以指定 defaultRollback 屬性,缺省為 true,即所有的方法都 rollback,您可以指定為 false,這樣,在一些需要 rollback 的方法,指定注釋標(biāo)簽 @Rollback(true)即可。
對 Junit4 的注釋標(biāo)簽支持
看了上面 Spring 測試框架的注釋標(biāo)簽,我們來看看一些常見的基于 Junit4 的注釋標(biāo)簽在 Spring 測試環(huán)境中的使用方法。
@Test(expected=…)
此注釋標(biāo)簽的含義是,這是一個測試,期待一個異常的發(fā)生,期待的異常通過 xxx.class 標(biāo)識。例如,我們修改 AccountService.Java 的 insertIfNotExist 方法,對于傳入的參數(shù)如果為空,則拋出 IllegalArgumentException,如下:
- public void insertIfNotExist(Account account) {
- if(account==null)
- throw new IllegalArgumentException("account is null");
- Account acct = accountDao.getAccountById(account.getId());
- if(acct==null) {
- log.debug("No "+account+" found,would insert it.");
- accountDao.saveAccount(account);
- }
- acct = null;
- }
然后,在測試類中增加一個測試異常的方法,如下:
- @Test(expected=IllegalArgumentException.class)
- public void testInsertException() {
- service.insertIfNotExist(null);
- }
運(yùn)行結(jié)果是 green bar。
@Test(timeout=…)
可以給測試方法指定超時時間(毫秒級別),當(dāng)測試方法的執(zhí)行時間超過此值,則失敗。
比如在 AccountService 中增加如下方法:
- public void doSomeHugeJob() {
- try {
- Thread.sleep(2*1000);
- } catch (InterruptedException e) {
- }
- }
上述方法模擬任務(wù)執(zhí)行時間 2 秒,則測試方法如下:
- @Test(timeout=3000)
- public void testHugeJob() {
- service.doSomeHugeJob();
- }
上述測試方法期待 service.doSomeHugeJob 方法能在 3 秒內(nèi)結(jié)束,執(zhí)行測試結(jié)果是 green bar。
@Repeat
通過 @Repeat,您可以輕松的多次執(zhí)行測試用例,而不用自己寫 for 循環(huán),使用方法:
- @Repeat(3)
- @Test(expected=IllegalArgumentException.class)
- public void testInsertException() {
- service.insertIfNotExist(null);
- }
這樣,testInsertException 就能被執(zhí)行 3 次。
在測試類中基于 profile 加載測試 bean
從 Spring 3.2 以后,Spring 開始支持使用 @ActiveProfiles 來指定測試類加載的配置包,比如您的配置文件只有一個,但是需要兼容生產(chǎn)環(huán)境的配置和單元測試的配置,那么您可以使用 profile 的方式來定義 beans,如下:
清單 10. Spring-db.xml
- 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-3.2.xsd">
- >
- >
- >
本文題目:使用Spring進(jìn)行單元測試
新聞來源:http://uogjgqi.cn/article/dpjddpg.html

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流