使用 Spring 進行單元測試

發表於2013-06-03

趙 才文, 技術經理
簡介: 通過本文,您能夠在較短的時間內掌握使用 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 進行單元測試

類總體介紹

假設我們現在有一個基於 Spring 的應用程式,除了 MVC 層,還包括業務層和資料訪問層,業務層有一個類 AccountService,負責處理賬號類的業務,其依賴於資料訪問層 AccountDao 類,此類提供了基於 Spring Jdbc Template 實現的資料庫訪問方法,AccountService 和 AccountDao 以及他們之間的依賴關係都是通過 Spring 配置檔案進行管理的。

現在我們要對 AccountService 類進行測試,在不使用 Spring 測試方法之前,我們需要這樣做:

清單 1. Account.Java

此類代表賬號的基本資訊,提供 getter 和 setter 方法。

注意上面的 Account 類有一個 toString() 方法和一個靜態的 getAccount 方法,getAccount 方法用於快速獲取 Account 測試物件。

清單 2. AccountDao.Java

這個 DAO 我們這裡為了簡單起見,採用 Spring Jdbc Template 來實現。

AccountDao 定義了幾個賬號物件的資料庫訪問方法:

  • saveAccount:負責把傳入的賬號物件入庫
  • getAccountById:負責根據 Id 查詢賬號

清單 3. AccountService.Java

AccountService 包括下列方法:

  • getAccountById:根據 Id 查詢賬號資訊
  • insertIfNotExist:根據傳入的物件插入資料庫

其依賴的 DAO 物件 accountDao 是通過 Spring 註釋標籤 @Autowired 自動注入的。

清單 4. Spring 配置檔案

上述幾個類的依賴關係是通過 Spring 進行管理的,配置檔案如下:

注意其中的“<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

注意上面的 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 進行單元測試

對於這種不使用 Spring test 框架進行的單元測試,我們注意到,需要做這些工作:

  • 在測試開始之前,需要手工載入 Spring 的配置檔案,並獲取需要的 bean 例項
  • 在測試結束的時候,需要手工清空搭建的資料庫環境,比如清除您插入或者更新的資料,以保證對下一個測試沒有影響

另外,在這個測試類中,我們還不能使用 Spring 的依賴注入特性。一切都靠手工編碼實現。好,那麼我們看看 Spring test 框架能做到什麼。

首先我們修改一下 Spring 的 XML 配置檔案,刪除 <context:annotation-config/> 行,其他不變。

清單 6. Spring-db1.xml

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

清單 7. AccountServiceTest1.Java

對這個類解釋一下:

  • @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 進行單元測試

小結

如果您希望在 Spring 環境中進行單元測試,那麼可以做如下配置:

  • 繼續使用 Junit4 測試框架,包括其 @Test 註釋標籤和相關的類和方法的定義,這些都不用變
  • 您需要通過 @RunWith(SpringJUnit4ClassRunner.class) 來啟動 Spring 對測試類的支援
  • 您需要通過 @ContextConfiguration 註釋標籤來指定 Spring 配置檔案或者配置類的位置
  • 您需要通過 @Transactional 來啟用自動的事務管理
  • 您可以使用 @Autowired 自動織入 Spring 的 bean 用來測試

另外您不再需要:

  • 手工載入 Spring 的配置檔案
  • 手工清理資料庫的每次變更
  • 手工獲取 application context 然後獲取 bean 例項

Spring 測試註釋標籤
我們已經看到利用 Spring test framework 來進行基於 Junit4 的單元測試是多麼的簡單,下面我們來看一下前面遇到的各種註釋標籤的一些可選用法。

@ContextConfiguration 和 @Configuration 的使用

剛才已經介紹過,可以輸入 Spring xml 檔案的位置,Spring test framework 會自動載入 XML 檔案,得到 application context,當然也可以使用 Spring 3.0 新提供的特性 @Configuration,這個註釋標籤允許您用 Java 語言來定義 bean 例項,舉個例子:

現在我們將前面定義的 Spring-db1.xml 進行修改,我們希望其中的三個 bean:initer、accountDao、accountService 通過配置類來定義,而不是 XML,則我們需要定義如下配置類:

注意:如果您想使用 @Configuration,請在 classpath 中加入 cglib 的 jar 包(cglib-nodep-2.2.3.jar),否則會報錯。

清單 8. SpringDb2Config.Java

注意上面的註釋標籤:

  • @Configuration:表明這個類是一個 Spring 配置類,提供 Spring 的 bean 定義,實際效果等同於 XML 配置方法
  • @Bean:表明這個方法是一個 bean 的定義,預設情況下,方法名稱就是 bean 的 Id
  • @Autowired:這個 datasource 採用自動注入的方式獲取

注意,我們採用的是 XML+config bean 的方式進行配置,這種方式比較符合實際專案的情況。相關的 Spring 配置檔案也要做變化,如下清單所示:

清單 9. Spring-db2.xml

注意裡面的 context 名稱空間的定義,如程式碼中黑體字所示。另外還必須有 <context:annotaiton-config/> 的定義,這個定義允許採用註釋標籤的方式來控制 Spring 的容器,最後我們看到 beans 已經沒有 initer、accountDao 和 accountService 這些 bean 的定義,取而代之的是一個 SpringDb2Config bean 的定義,注意這個 bean 沒有名稱,因為不需要被引用。

現在有了這些配置,我們的測試類只要稍稍修改一下,即可實現載入配置類的效果,如下:

通過上面的配置,測試用例就可以實現載入 Spring 配置類,執行結果也是成功的 green bar。

@DirtiesContext

預設情況下,Spring 測試框架一旦載入 applicationContext 後,將一直快取,不會改變,但是,

由於 Spring 允許在執行期修改 applicationContext 的定義,例如在執行期獲取 applicationContext,然後呼叫 registerSingleton 方法來動態的註冊新的 bean,這樣的情況下,如果我們還使用 Spring 測試框架的被修改過 applicationContext,則會帶來測試問題,我們必須能夠在執行期重新載入 applicationContext,這個時候,我們可以在測試類或者方法上註釋:@DirtiesContext,作用如下:

  • 如果定義在類上(預設),則在此測試類執行完成後,重新載入 applicationContext
  • 如果定義在方法上,即表示測試方法執行完成後,重新載入 applicationContext

@TransactionConfiguration 和 @Rollback
預設情況下,Spring 測試框架將事務管理委託到名為 transactionManager 的 bean 上,如果您的事務管理器不是這個名字,那需要指定 transactionManager 屬性名稱,還可以指定 defaultRollback 屬性,預設為 true,即所有的方法都 rollback,您可以指定為 false,這樣,在一些需要 rollback 的方法,指定註釋標籤 @Rollback(true)即可。

對 Junit4 的註釋標籤支援

看了上面 Spring 測試框架的註釋標籤,我們來看看一些常見的基於 Junit4 的註釋標籤在 Spring 測試環境中的使用方法。

@Test(expected=…)

此註釋標籤的含義是,這是一個測試,期待一個異常的發生,期待的異常通過 xxx.class 標識。例如,我們修改 AccountService.Java 的 insertIfNotExist 方法,對於傳入的引數如果為空,則丟擲 IllegalArgumentException,如下:

然後,在測試類中增加一個測試異常的方法,如下:

執行結果是 green bar。

@Test(timeout=…)

可以給測試方法指定超時時間(毫秒級別),當測試方法的執行時間超過此值,則失敗。

比如在 AccountService 中增加如下方法:

上述方法模擬任務執行時間 2 秒,則測試方法如下:

上述測試方法期待 service.doSomeHugeJob 方法能在 3 秒內結束,執行測試結果是 green bar。

@Repeat

通過 @Repeat,您可以輕鬆的多次執行測試用例,而不用自己寫 for 迴圈,使用方法:

這樣,testInsertException 就能被執行 3 次。

在測試類中基於 profile 載入測試 bean

從 Spring 3.2 以後,Spring 開始支援使用 @ActiveProfiles 來指定測試類載入的配置包,比如您的配置檔案只有一個,但是需要相容生產環境的配置和單元測試的配置,那麼您可以使用 profile 的方式來定義 beans,如下:

清單 10. Spring-db.xml

上面的定義,我們看到:

  • 在 XML 頭中我們引用了 Spring 3.2 的 beans 定義,因為只有 Spring 3.2+ 才支援基於 profile 的定義
  • 在 <beans> 根節點下可以巢狀 <beans> 定義,要指定 profile 屬性,這個配置中,我們定義了兩個 datasource,一個屬於 test profile,一個輸入 production profile,這樣,我們就能在測試程式中載入 test profile,不影響 production 資料庫了
  • 在下面定義了一些屬於兩個 profile 的 beans,即 <beans profile=”test,production”> 這樣方便重用一些 bean 的定義,因為這些 bean 在兩個 profile 中都是一樣的

清單 11. AccountServiceTest.Java

注意上面的 @ActiveProfiles,可以指定一個或者多個 profile,這樣我們的測試類就僅僅載入這些名字的 profile 中定義的 bean 例項。

對 TestNG 的支援

Spring 2.5 以後,就開始支援 TestNG 了,支援的方法包括:

  • 將您的 TestNG 測試類繼承 Spring 的測試父類:AbstractTransactionalTestNGSpringContextTests 或者 AbstractTestNGSpringContextTests,這樣您的 TestNG 測試類內部就可以訪問 applicationContext 成員變數了
  • 不繼承 Spring 父類,在測試類上使用 @TestExecutionListeners 註釋標籤,可以引入的監聽器包括
    • DependencyInjectionTestExecutionListener:使得測試類擁有依賴注入特性
    • DirtiesContextTestExecutionListener:使得測試類擁有更新 applicationContext 能力
    • TransactionalTestExecutionListener:使得測試類擁有自動的事務管理能力

這裡我們演示一下如何使用 Spring 提供的 TestNG 父類來進行測試。

清單 12. AccountServiceTestNGTest.Java

執行測試,我們將看到測試成功。

圖 4. 測試成功

使用 Spring 進行單元測試

搜尋資料庫對應的表,我們看到裡面沒有資料,說明自動事務起作用了。

基本原理

Spring test framework 主要位於 org.Springframework.test.context 包中,主要包括下面幾個類:

圖 5. Spring 測試框架類圖

使用 Spring 進行單元測試

  • TestContextManager:主要的入口類,提供 TestContext 例項的管理,負責根據各種事件來通知測試監聽器
  • TestContext:實體類,提供訪問 Spring applicatoin context 的能力,並負責快取 applicationContext
  • TestExecutionListener:測試監聽器,提供依賴注入、applicationContext 快取和事務管理等能力
  • ContextLoader:負責根據配置載入 Spring 的 bean 定義,以構建 applicationContext 例項物件
  • SmartContextLoader:Spring 3.1 引入的新載入方法,支援按照 profile 載入

Spring 通過 AOP hook 了測試類的例項建立、beforeClass、before、after、afterClass 等事件入口,執行順序主要如下:

圖 6. Spring 測試框架執行序列圖

使用 Spring 進行單元測試

  • 測試執行者開始執行測試類,這個時候 Spring 獲取訊息,自動建立 TestContextManager 例項
  • TestContextManager 會建立 TestContext,以記錄當前測試的上下文資訊,TestContext 則通過 ContextLoader 來獲取 Spring ApplicationContext 例項
  • 當測試執行者開始執行測試類的 BeforeClass、Before、After、AfterClass 的時候,TestContextManager 將截獲事件,發通知給對應的 TestExecutionListener

總結

根據上面的例子和介紹,我們可以看到,Spring 測試框架的主要特點如下:

  • 完美的支援了 Junit4(提供特別的 SpringJunit4ClassRunner),比較好的支援了 TestNG
  • 在支援原有單元測試能力的基礎上,通過各種監聽器,支援了測試類的依賴注入、對 Spring applicationContext 的訪問以及事務管理能力,為使用 Spring 架構的應用程式的測試帶來了極大的便利性
  • Spring 3.1 引入的基於 profile 的載入能力使得測試環境和正式環境可以在一個 XML 定義中完美的結合

總之,如果您的程式中使用了 Spring,且對用 Junit 或者 testNG 來對他們進行單元測試感到力不從心,可以考慮使用 Spring test framework,它將使您的應用程式的質量上一個新的臺階。

相關文章