引述:程序測試對(duì)保障應(yīng)用程序正確性而言,其重要性怎么樣強(qiáng)調(diào)都不為過。JUnit是必須事先掌握的測試框架,大多數(shù)測試框架和測試工具都在此基礎(chǔ)上擴(kuò)展而來,Spring對(duì)測試所提供的幫助類也是在JUnit的基礎(chǔ)上進(jìn)行演化的。直接使用JUnit測試基于Spring的應(yīng)用存在諸多不便,不可避免地需要將大量的精力用于應(yīng)付測試夾具準(zhǔn)備、測試現(xiàn)場恢復(fù)、訪問測試數(shù)據(jù)操作結(jié)果等邊緣性的工作中。Mockito、Unitils、Dbunit等框架的出現(xiàn),這些問題有了很好的解決方案,特別是Unitils結(jié)合Dbunit對(duì)測試DAO層提供了強(qiáng)大的支持,大大提高了編寫測試用例的效率和質(zhì)量。
Unitils海納百川(Junit,dbunit,mockito spring hibernate and so on..),以打造一個(gè)在實(shí)際應(yīng)用開發(fā)中真正實(shí)戰(zhàn)的測試框架,是致力于應(yīng)用實(shí)戰(zhàn)的開發(fā)者不得不學(xué)習(xí)的開源框架。Unitils概述 Unitils測試框架目的是讓單元測試變得更加容易和可維護(hù)。Unitils構(gòu)建在DbUnit與EasyMock項(xiàng)目之上并與JUnit和TestNG相結(jié)合。支持?jǐn)?shù)據(jù)庫測試,支持利用Mock對(duì)象進(jìn)行測試并提供與Spring和Hibernate相集成。Unitils設(shè)計(jì)成以一種高度可配置和松散耦合的方式來添加這些服務(wù)到單元測試中,目前其最新版本是3.1。
Unitils功能特點(diǎn)- 自動(dòng)維護(hù)和強(qiáng)制關(guān)閉單元測試數(shù)據(jù)庫(支持Oracle、Hsqldb、MySQL、DB2)。
- 簡化單元測試數(shù)據(jù)庫連接的設(shè)置。
- 簡化利用DbUnit測試數(shù)據(jù)的插入。
- 簡化Hibernate session管理。
- 自動(dòng)測試與數(shù)據(jù)庫相映射的Hibernate映射對(duì)象。
- 易于把Spring管理的Bean注入到單元測試中,支持在單元測試中使用Spring容器中的Hibernate SessionFactory。
- 簡化EasyMock Mock對(duì)象創(chuàng)建。
- 簡化Mock對(duì)象注入,利用反射等式匹配EasyMock參數(shù)。
Unitils模塊組件 Unitils通過模塊化的方式來組織各個(gè)功能模塊,采用類似于Spring的模塊劃分方式,如unitils-core、unitils-database、unitils-mock等。比以前整合在一個(gè)工程里面顯得更加清晰,目前所有模塊如下所示:
- unitils-core:核心內(nèi)核包。
- unitils-database:維護(hù)測試數(shù)據(jù)庫及連接池。
- unitils-DbUnit:使用DbUnit來管理測試數(shù)據(jù)。
- unitils-easymock:支持創(chuàng)建Mock和寬松的反射參數(shù)匹配。
- unitils-inject:支持在一個(gè)對(duì)象中注入另一個(gè)對(duì)象。
- unitils-mock:整合各種Mock,在Mock的使用語法上進(jìn)行了簡化。
- unitils-orm:支持Hibernate、JPA的配置和自動(dòng)數(shù)據(jù)庫映射檢查。
- unitils-spring:支持加載Spring的上下文配置,并檢索和Spring Bean注入。
Unitils的核心架構(gòu)中包含Moudule和TestListener兩個(gè)概念,類似Spring中黏連其他開源軟件中的FactoryBean概念??梢钥闯傻谌綔y試工具的一個(gè)黏合劑。整體框架如圖16-4所示:
通過TestListener可以在測試運(yùn)行的不同階段注入某些功能。同時(shí)某一個(gè)TestListener又被一個(gè)對(duì)應(yīng)的Module所持有。Unitils也可以看成一個(gè)插件體系結(jié)構(gòu),TestListener在整個(gè)Unitils中又充當(dāng)了插件中擴(kuò)展點(diǎn)的角色,從TestListener這個(gè)接口中我們可以看到,它可以在crateTestObject、before(after)Class、before(after)TestMethod、beforeSetup、afterTeardown的不同切入點(diǎn)添加不同的動(dòng)作。
Unitils配置文件- unitils-defaults.properties:默認(rèn)配置文件,開啟所有功能。
- unitils.properties:項(xiàng)目級(jí)配置文件,用于項(xiàng)目通用屬性配置。
- unitils-local.properties:用戶級(jí)配置文件,用于個(gè)人特殊屬性配置。
Unitils的配置定義了一般配置文件的名字unitils.properties和用戶自定義配置文件unitils-local.properties,并給出了默認(rèn)的模塊及模塊對(duì)應(yīng)的className,便于Unitils加載對(duì)應(yīng)的模塊module。但是如果用戶分別在unitils.properties文件及unitils -local.properties文件中對(duì)相同屬性配置不同值時(shí),將會(huì)以u(píng)nitils-local.properties 的配置內(nèi)容為主。
Unitils斷言 典型的單元測試一般都包含一個(gè)重要的組成部分:對(duì)比實(shí)際產(chǎn)生的結(jié)果和希望的結(jié)果是否一致的方法,即斷言方法(assertEquals)。Unitils 為我們提供了一個(gè)非常實(shí)用的斷言方法,我們以第2章中編寫的用戶領(lǐng)域?qū)ο骍ser為藍(lán)本,比較兩個(gè)User對(duì)象的實(shí)例來開始認(rèn)識(shí)Unitils的斷言之旅。
assertReflectionEquals:反射斷言 在Java世界中,要比較現(xiàn)有兩個(gè)對(duì)象實(shí)例是否相等,如果類沒有重寫equals()方法,用兩個(gè)對(duì)象的引用是否一致作為判斷依據(jù)。有時(shí)候,我們并不需要關(guān)注兩個(gè)對(duì)象是否引用同一個(gè)對(duì)象,只要兩個(gè)對(duì)象的屬性值一樣就可以了。在JUnit單元測試中,有兩種測試方式進(jìn)行這樣的場景測試:一是在比較實(shí)體類中重寫equals()方法,然后進(jìn)行對(duì)象比較;二是把對(duì)象實(shí)例的屬性一個(gè)一個(gè)進(jìn)行比較。不管采用哪種方法,都比較煩鎖,Unitils為我們提供了一種非常簡單的方法,即使用ReflectionAssert.assertReflectionEquals方法, 如代碼清單16-11所示:
- package com.baobaotao.test;
- import java.util.*;
- import org.junit.Test;
- import static org.unitils.reflectionassert.ReflectionAssert.*;
- import static org.unitils.reflectionassert.ReflectionComparatorMode.*;
- import com.baobaotao.domain.User;
-
- public class AssertReflectionEqualsTest {
- @Test
- public void testReflection(){
- User user1 = new User("tom","1234");
- User user2 = new User("tom","1234");
- ReflectionAssert.assertReflectionEquals(user1, user2);
- }
-
- }
ReflectionAssert. AssertReflectionEquals(期望值,實(shí)際值,比較級(jí)別)方法為我們提供了各種級(jí)別的比較斷言。下面我們依次介紹這些級(jí)別的比較斷言。
ReflectionComparatorMode.LENIENT_ORDER:忽略要斷言集合collection 或者array 中元素的順序。
ReflectionComparatorMode.IGNORE_DEFAULTS:忽略Java類型默認(rèn)值,如引用類型為null,整型類型為0,或者布爾類型為false時(shí),那么斷言忽略這些值的比較。
ReflectionComparatorMode.LENIENT_DATES:比較兩個(gè)實(shí)例的Date是不是都被設(shè)置了值或者都為null,而忽略Date的值是否相等。
assertLenientEquals:斷言 ReflectionAssert 類為我們提供了兩種比較斷言:既忽略順序又忽略默認(rèn)值的斷言assertLenientEquals,使用這種斷言就可以進(jìn)行簡單比較。下面通過實(shí)例學(xué)習(xí)其具體的用法,如代碼清單16-12所示。
- package com.baobaotao.test;
- import java.util.*;
- …
- public class AssertReflectionEqualsTest {
- Integer orderList1[] = new Integer[]{1,2,3};
- Integer orderList2[] = new Integer[]{3,2,1};
-
-
- //① 測試兩個(gè)數(shù)組的值是否相等,忽略順序
- //assertReflectionEquals(orderList1, orderList2,LENIENT_ORDER);
- assertLenientEquals(orderList1, orderList2);
-
-
- //② 測試兩個(gè)對(duì)象的值是否相等,忽略時(shí)間值是否相等
- User user1 = new User("tom","1234");
- Calendar cal1 = Calendar.getInstance();
- user1.setLastVisit(cal1.getTime());
- User user2 = new User("tom","1234");
- Calendar cal2 = Calendar.getInstance();
- cal2.set(Calendar.DATE, 15);
- user2.setLastVisit(cal2.getTime());
- //assertReflectionEquals(user1, user2,LENIENT_DATES);
- assertLenientEquals(user1, user2);
- }
assertPropertyXxxEquals:屬性斷言 assertLenientEquals 和assertReflectionEquals 這兩個(gè)方法是把對(duì)象作為整體進(jìn)行比較,ReflectionAssert 類還給我們提供了只比較對(duì)象特定屬性的方法:assertPropertyReflection Equals()和assertPropertyLenientEquals()。下面通過實(shí)例學(xué)習(xí)其具體的用法,如代碼清單16-13所示。
- package com.baobaotao.test;
- import java.util.*;
- …
- public class AssertReflectionEqualsTest {
- User user = new User("tom","1234");
- assertPropertyReflectionEquals("userName", "tom", user);
- assertPropertyLenientEquals("lastVisit", null, user);
- }
assertPropertyReflectionEquals()斷言是默認(rèn)嚴(yán)格比較模式但是可以手動(dòng)設(shè)置比較級(jí)別的斷言,assertPropertyLenientEquals()斷言是具有忽略順序和忽略默認(rèn)值的斷言。
集成Spring Unitils 提供了一些在Spring 框架下進(jìn)行單元測試的特性。Spring 的一個(gè)基本特性就是,類要設(shè)計(jì)成為沒有Spring 容器或者在其他容器下仍然易于進(jìn)行單元測試。但是很多時(shí)候在Spring 容器下進(jìn)行測試還是非常有用的。
Unitils 提供了以下支持 Spring 的特性:
- ApplicationContext 配置的管理;
- 在單元測試代碼中注入Spring 的Beans;
- 使用定義在Spring 配置文件里的Hibernate SessionFactory;
- 引用在Spring 配置中Unitils 數(shù)據(jù)源。
ApplicationContext 配置 可以簡單地在一個(gè)類、方法或者屬性上加上@SpringApplicationContext 注解,并用Spring的配置文件作為參數(shù),來加載Spring應(yīng)用程序上下文。下面我們通過實(shí)例來介紹一下如何創(chuàng)建ApplicationContext。
- import org.junit.Test;
- import org.springframework.context.ApplicationContext;
- import org.unitils.UnitilsJUnit4;
- import org.unitils.spring.annotation.SpringApplicationContext;
- import org.unitils.spring.annotation.SpringBean;
- import com.baobaotao.service.UserService;
- import static org.junit.Assert.*;
- //①用戶服務(wù)測試
- public class UserServiceTest extends UnitilsJUnit4 {
-
- //①-1 加載Spring配置文件
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"})
- private ApplicationContext applicationContext;
-
- //①-1 加載Spring容器中的Bean
- @SpringBean("userService")
- private UserService userService;
-
- //①-3 測試Spring容器中的用戶服務(wù)Bean
- @Test
- public void testUserService (){
- assertNotNull(applicationContext);
- assertNotNull(userService.findUserByUserName("tom"));
- }
- }
- …
在①-1處,通過@SpringApplicationContext 注解加載baobaotao-service.xml和baobaotao- dao.xml兩個(gè)配置文件,生成一個(gè)Spring應(yīng)用上下文,我們就可以在注解的范圍內(nèi)引用applicationContext這個(gè)上下文。在①-2處,通過@SpringBean注解注入當(dāng)前Spring容器中相應(yīng)的Bean,如實(shí)例中加載ID為“userService”的Bean到當(dāng)前測試范圍。在①-3處,通過JUnit斷言驗(yàn)證是否成功加載applicationContext和userService。Unitils加載Spring上下文的過程是:首先掃描父類的@SpringApplicationContext注解,如果找到了就在加載子類的配置文件之前加載父類的配置文件,這樣就可以讓子類重寫配置文件和加載特定配置文件。
細(xì)心的讀者可能會(huì)發(fā)現(xiàn),采用這種方式加載Spring應(yīng)用上下文,每次執(zhí)行測試時(shí),都會(huì)重復(fù)加載Spring應(yīng)用上下文。Unitils為我們提供在類上加載Spring應(yīng)用上下文的能力,以避免重復(fù)加載的問題。
- …
- @SpringApplicationContext({"baobaotao-service.xml", "baobaotao-dao.xml"})
- public class BaseServiceTest extends UnitilsJUnit4 {
-
- //加載Spring上下文
- @SpringApplicationContext
- public ApplicationContext applicationContext;
-
- }
在父類BaseServiceTest里指定了Spring配置文件,Spring應(yīng)用上下文只會(huì)創(chuàng)建一次,然后在子類SimpleUserServiceTest 里會(huì)重用這個(gè)應(yīng)用程序上下文。加載Spring應(yīng)用上下文是一個(gè)非常繁重的操作,如果重用這個(gè)Spring應(yīng)用上下文就會(huì)大大提升測試的性能。
- …
- public class SimpleUserServiceTest extends BaseServiceTest {
-
- //① Spring容器中加載Id為"userService"的Bean
- @SpringBean("userService")
- private UserService userService1;
-
- //② 從Spring容器中加載與UserService相同類型的Bean
- @SpringBeanByType
- private UserService userService2;
-
- //③ 從Spring容器中加載與userService相同名稱的Bean
- @SpringBeanByName
- private UserService userService;
-
- //④ 使用父類的Spring上下文
- @Test
- public void testApplicationContext(){
- assertNotNull(applicationContext);
- }
-
- @Test
- public void testUserService(){
- assertNotNull(userService.findUserByUserName("tom"));
- assertNotNull(userService1.findUserByUserName("tom"));
- assertNotNull(userService2.findUserByUserName("tom"));
- }
- }
- …
在①處,使用@SpringBean 注解從Spring容器中加載一個(gè)ID為userService的Bean。在②處,使用@ SpringBeanByType注解從Spring容器中加載一個(gè)與UserService相同類型的Bean,如果找不到相同類型的Bean,就會(huì)拋出異常。在③處,使用@SpringBeanByName 注解從Spring容器中加載一個(gè)與當(dāng)前屬性名稱相同的Bean。
集成Hibernate Hibernate是一個(gè)優(yōu)秀的O / R開源框架,它極大地簡化了應(yīng)用程序的數(shù)據(jù)訪問層開發(fā)。雖然我們?cè)谑褂靡粋€(gè)優(yōu)秀的O/R框架,但并不意味我們無須對(duì)數(shù)據(jù)訪問層進(jìn)行單元測試。單元測試仍然非常重要。它不僅可以確保Hibernate映射類的映射正確性,也可以很便捷地測試HQL查詢等語句。Unitils為方便測試 Hibernate,提供了許多實(shí)用的工具類,如HibernateUnitils就是其中一個(gè),使用assertMappingWithDatabaseConsistent()方法,就可以方便測試映射文件的正確性。
SessionFactory 配置 可以簡單地在一個(gè)類、方法或者屬性上加上@ HibernateSessionFactory 注解,并用Hibernate的配置文件作為參數(shù),來加載Hibernate上下文。下面我們通過實(shí)例來介紹一下如何創(chuàng)建SessionFactory。
- …
- @HibernateSessionFactory("hibernate.cfg.xml")
- public class BaseDaoTest extends UnitilsJUnit4 {
- @HibernateSessionFactory
- public SessionFactory sessionFactory;
-
-
- @Test
- public void testSessionFactory(){
- assertNotNull(sessionFactory);
- }
- }
在父類BaseDaoTest里指定了Hibernate配置文件,Hibernate應(yīng)用上下文只會(huì)創(chuàng)建一次,然后在子類SimpleUserDaoTest里會(huì)重用這個(gè)應(yīng)用程序上下文。加載Hibernate應(yīng)用上下文是一個(gè)非常繁重的操作,如果重用這個(gè)Hibernate應(yīng)用上下文就會(huì)大大提升測試的性能。
- …
- public class SimpleUserDaoTest extends BaseDaoTest {
- private UserDao userDao;
-
- //① 初始化UserDao
- @Before
- public void init(){
- userDao = new WithoutSpringUserDaoImpl();
- userDao.setSessionFactory(sessionFactory); //使用父類的SessionFactory
- }
-
- //② Hibernate映射測試
- @Test
- public void testMappingToDatabase() {
- HibernateUnitils.assertMappingWithDatabaseConsistent();
- }
-
- //③ 測試UserDao
- @Test
- public void testUserDao(){
- assertNotNull(userDao);
- assertNotNull(userDao.findUserByUserName("tom"));
- assertEquals("tom", userDao.findUserByUserName("tom").getUserName());
- }
- }
- …
為了更好演示如何應(yīng)用Unitils測試基于Hibernate數(shù)據(jù)訪問層,在這個(gè)實(shí)例中不使用Spring框架。所以在執(zhí)行測試時(shí),需要先創(chuàng)建相應(yīng)的數(shù)據(jù)訪問層實(shí)例,如實(shí)例中的userDao。其創(chuàng)建過程如①處所示,先手工實(shí)例化一個(gè)UserDao,然后獲取父類中創(chuàng)建的SessionFactory,并設(shè)置到UserDao中。在②處,使用Unitils提供的工具類HibernateUnitils中的方法測試我們的Hibernate映射文件。在③處,通過JUnit的斷言驗(yàn)證 UserDao相關(guān)方法,看是否與我們預(yù)期的結(jié)果一致。
集成Dbunit Dbunit是一個(gè)基于JUnit擴(kuò)展的數(shù)據(jù)庫測試框架。它提供了大量的類,對(duì)數(shù)據(jù)庫相關(guān)的操作進(jìn)行了抽象和封裝。Dbunit通過使用用戶自定義的數(shù)據(jù)集以及相關(guān)操作使數(shù)據(jù)庫處于一種可知的狀態(tài),從而使得測試自動(dòng)化、可重復(fù)和相對(duì)獨(dú)立。雖然不用Dbunit也可以達(dá)到這種目的,但是我們必須為此付出代價(jià)(編寫大量代碼、測試及維護(hù))。既然有了這么優(yōu)秀的開源框架,我們又何必再造輪子。目前其最新的版本是2.4.8。
隨著Unitils的出現(xiàn),將Spring、Hibernate、DbUnit等整合在一起,使得DAO層的單元測試變得非常容易。Unitils采用模塊化方式來整合第三方框架,通過實(shí)現(xiàn)擴(kuò)展模塊接口org.unitils.core.Module來實(shí)現(xiàn)擴(kuò)展功能。在Unitils中已經(jīng)實(shí)現(xiàn)一個(gè)DbUnitModule,很好整合了DbUnit。通過這個(gè)擴(kuò)展模塊,就可以在Unitils中使用Dbunit強(qiáng)大的數(shù)據(jù)集功能,如用于準(zhǔn)備數(shù)據(jù)的@DataSet注解、用于驗(yàn)證數(shù)據(jù)的@ExpectedDataSet注解。Unitils集成DbUnit流程圖如圖16-5所示。
16.4.5 自定義擴(kuò)展模塊 Unitils通過模塊化的方式來組織各個(gè)功能模塊,對(duì)外提供一個(gè)統(tǒng)一的擴(kuò)展模塊接口org.unitils.core.Module來實(shí)現(xiàn)與第三方框架的集成及自定義擴(kuò)展。在Unitils中已經(jīng)實(shí)現(xiàn)目前一些主流框架的模塊擴(kuò)展,如Spring、Hibernate、DbUnit、Testng等。如果這些內(nèi)置的擴(kuò)展模塊無法滿足需求,我們可以實(shí)現(xiàn)自己的一些擴(kuò)展模塊。擴(kuò)展Unitils模塊很簡單,如代碼清單16-19所示。
- package sample.unitils.module;
- import java.lang.reflect.Method;
- import org.unitils.core.TestListener;
- import org.unitils.core. Module;
- //① 實(shí)現(xiàn)Module接口
- public class CustomExtModule implements Module {
- //② 實(shí)現(xiàn)獲取測試監(jiān)聽的方法
- public TestListener getTestListener() {
- return new CustomExtListener();
- }
-
- //② 新建監(jiān)聽模塊
- protected class CustomExtListener extends TestListener {
- //③ 重寫 TestListener里的相關(guān)方法,完成相關(guān)擴(kuò)展的功能
- @Override
- public void afterTestMethod(Object testObject, Method testMethod,
- Throwable testThrowable) {
- …
- }
-
- @Override
- public void beforeTestMethod(Object testObject, Method testMethod) {
- …
- }
- }
- …
- }
在①處新建自定義擴(kuò)展模塊CustomExtModule,實(shí)現(xiàn)Module接口。在②處新建自定義監(jiān)聽模塊,繼承TestListener。在③處重寫(@Override)TestListener里的相關(guān)方法,完成相關(guān)擴(kuò)展的功能。實(shí)現(xiàn)自定義擴(kuò)展模塊之后,剩下的工作就是在Unitils配置文件unitils.properties中注冊(cè)這個(gè)自定義擴(kuò)展的模塊:
引用
unitils.modules=…,custom
unitils.module. custom.className= sample.unitils.module.CustomExtModule
這些文章摘自于我的
《Spring 3.x企業(yè)應(yīng)用開發(fā)實(shí)戰(zhàn)》的第16章,我將通過連載的方式,陸續(xù)在此發(fā)出。歡迎大家討論。