不久以前,developerWorks 的作者 Andrew Glover 撰寫了一篇介紹 Groovy 的文章,該文章是 alt.lang.jre 系列的一部分,而 Groovy 是一個新提議的用于 Java 平臺的標準語言。讀者對這篇文章的反應非常熱烈,所以我們決定開辦這個專欄,提供使用這項熱門新技術的實用指導。本文是第一期,將介紹使用 Groovy 和 JUnit 對 Java 代碼進行單元測試的一個簡單策略。

開始之前,我首先要招認:我是一個單元測試狂。實際上,我總是無法編寫足夠的單元測試。如果我相當長一段時間都在進行開發,而 沒有編寫相應的單元測試,我就會覺得緊張。單元測試給我信心,讓我相信我的代碼能夠工作,而且我只要看一下,可以修改它,就不會害怕它會崩潰。

而且,作為一個單元測試狂,我喜歡編寫多余的測試用例。但是,我的興奮不是來自 編寫測試用例,而是 看著它們生效。所以,如果我能用更快的方式編寫測試,我就能更迅速地看到它們的結果。這讓我感覺更好。更快一些!

后來,我找到了 Groovy,它滿足了我的單元測試狂,而且至今為止,對我仍然有效。這種新語言給單元測試帶來的靈活性,非常令人興奮,值得認真研究。本文是介紹 Groovy 實踐方面的新系列的第一部分,在文中,我將向您介紹使用 Groovy 進行單元測試的快樂。我從概述開始,概述 Groovy 對 Java 平臺上的開發所做的獨特貢獻,然后轉而討論使用 Groovy 和 JUnit 進行單元測試的細節,其中重點放在 Groovy 對 JUnit 的 TestCase 類的擴展上。最后,我用一個實用的示例進行總結,用第一手材料向您展示如何把 groovy 的這些特性與 Eclipse 和 Maven 集成在一起。

不要再堅持 Java 純粹主義了!
在我開始介紹用 Groovy 進行單元測試的實際經驗之前,我認為先談談一個更具一般性的問題 —— 它在您的開發工具箱中的位置,這非常重要。事實是,Groovy 不僅是運行在 Java 運行時環境(JRE)中的腳本語言,它還被提議作為用于 Java 平臺的標準語言。正如您們之中的人已經從 alt.lang.jre 系列(請參閱 參考資料)中學習到的,在為 Java 平臺進行腳本編程的時候,有無數的選擇,其中大多數是面向快速應用程序開發的高度靈活的環境。

雖然有這么豐富的選擇,但還是有許多開發人選擇堅持自己喜歡的、最熟悉的范式:Java 語言。雖然大多數情況下,Java 編程都是很好的選擇,但是它有一個非常重要的缺點,蒙住了只看見 Java 的好處的這些人的眼睛。正如一個智者曾經指出的: 如果您僅有的一個工具是一把錘子,那么您看每個問題時都會覺得它像是釘子。我認為這句諺語道出了適用于軟件開發的許多事實。

雖然我希望用這個系列說服您 Java 不是也不應當是開發應用程序的惟一選擇,但該腳本確實既有適用的地方也有不適用的地方。專家和新手的區別在于:知道什么時候 運用該腳本,什么時候 避免使用它。

關于本系列
把工具整合到開發實踐中的關鍵是了解什么時候使用它,以及什么時候把它留在工具箱中。腳本語言能夠成為工具包中極為強大的附件,但是只有正確地應用在適當的場合時才是這樣。為了實現 實戰 Groovy 系列文章這個目標,我專門研究了 Groovy 的一些實戰,教給您什么時候怎樣才能成功地應用它們。

例如,對于高性能、事務密集型、企業級應用程序,Groovy 腳本通常不太適合;在這些情況下,您最好的選擇 可能是普通的 J2EE 系統。但另一方面,一些腳本 —— 特別是用 Groovy 編寫的腳本 —— 會非常有用,因為它能迅速地為小型的、非常特殊的、不是性能密集型的應用程序(例如配置系統/生成系統)快速制作原型。對于報表應用程序來說, Groovy 腳本也是近乎完美的選擇,而最重要的是,對單元測試更是如此。

為什么用 Groovy 進行單元測試?
是什么讓 Groovy 比起其他腳本平臺顯得更具有吸引力呢?是它與Java 平臺無縫的集成。還是因為它是基于 Java 的語言(不像其他語言,是對 JRE 的替代,因此可能基于舊版的處理器),對于 Java 開發人員來說,Groovy 意味著一條短得讓人難以置信的學習曲線。而且一旦將這條學習曲線拉直,Groovy 就能提供一個無與倫比的快速開發平臺。

從這個角度來說,Groovy 成功的秘密,在于它的語法 就是 Java 語法,但是規則更少。例如,Groovy 不要求使用分號,變量類型和訪問修飾符也是可選的。而且,Groovy 利用了標準的 Java 庫,這些都是您已經很熟悉的,包括 CollectionsFile/IO。而且,您還可以利用任何 Groovy 提供的 Java 庫,包括 JUnit。

事實上,令人放松的類 Java 語法、對標準 Java 庫的重用以及快捷的生成-運行周期,這些都使 Groovy 成為快速開發單元測試的理想替代品。但是會說的不如會做的,還是讓我們在代碼中看看它的實際效果!

JUnit 和 Groovy
用 Groovy 對 Java 代碼進行單元測試簡單得不能再簡單了,有很多入門選擇。最直接的選擇是沿襲行業標準 —— JUnit。Unit 的簡單性和其功能的強大都是無與倫比的,作為非常有幫助的 Java 開發工具,它的普遍性也是無與倫比的,而且沒有什么能夠阻擋 JUnit 和 Groovy 結合,所以為什么多此一舉呢?實際上,只要您看過 JUnit 和 Groovy 在一起工作,我敢打賭您就永遠再也不會回頭!在這里,要記住的關鍵的事,您在 Java 語言中能用 JUnit 做到的事,在 Groovy 中用 JUnit 也全都能做到;但是需要的代碼要少得多。

入門
在您下載了 JUnit 和 Groovy(請參閱 參考資料)之后,您將有兩個選擇。第一個選擇是編寫普通的 JUnit 測試用例,就像以前一直做的那樣,擴展 JUnit 令人稱贊的 TestCase。第二個選擇是應用 Groovy 簡潔的 GroovyTestCase 擴展,它會擴展 JUnit 的 TestCase。第一個選項是您最快的成功途徑,它擁有最多 與 Java 類似的相似性。而另一方面,第二個選擇則把您推進了 Groovey 的世界,這個選擇有最大的敏捷性。

開始的時候,我們來想像一個 Java 對象,該對象對指定的 string 應用了一個過濾器,并根據匹配結果返回 boolean 值。該過濾器可以是簡單的 string 操作,例如 indexOf(),也可以更強大一些,是正則表達式。可能要通過 setFilter() 方法在運行時設置將使用的過濾器, apply() 方法接受要過濾的 string。清單 1 用普通的 Java 代碼顯示了這個示例的 Filter 接口:

清單 1. 一個簡單的 Java Filter 接口

public interface Filter {
  void setFilter(String fltr);  
  boolean applyFilter(String value);
}

我們的想法是用這個特性從大的列表中過濾掉想要的或者不想要的包名。所以,我建立了兩個實現: RegexPackageFilterSimplePackageFilter

把 Groovy 和 JUnit 的強大功能與簡單性結合在一起,就形成了如清單 2 所示的簡潔的測試套件:

清單 2. 用 JUunit 制作的 Groovy RegexFilterTest

import junit.framework.TestCase
import com.vanward.sedona.frmwrk.filter.impl.RegexPackageFilter

class RegexFilterTest extends TestCase {  

  void testSimpleRegex() {
    fltr = new RegexPackageFilter()
    fltr.setFilter("java.*")
    val = fltr.applyFilter("java.lang.String")		
    assertEquals("value should be true", true, val)		
  }
}

不管您是否熟悉 Groovy,清單 2 中的代碼對您來說應當很面熟,因為它只不過是沒有分號、訪問修飾符或變量類型的 Java 代碼而已!上面的 JUnit 測試中有一個測試用例 testSimpleRegex(),它試圖斷言 RegexPackageFilter 用正則表達式 "java.*" 正確地找到了與 “ java.lang.String”匹配的對象。

Groovy 擴展了 JUnit
擴展 JUnit 的 TestCase 類,加入附加特性,實際上是每個 JUnit 擴展通常采用的技術。例如,DbUnit 框架(請參閱 參考資料)提供了一個方便的 DatabaseTestCase 類,能夠比以往任何時候都更容易地管理數據庫的狀態,還有重要的 MockStrutsTestCase(來自 StrutsTestCase 框架;請參閱 參考資料),它生成虛擬的 servlet 容器,用來執行 struts 代碼。這兩個強大的框架都極好地擴展了 JUnit,提供了 JUnit 核心代碼中所沒有的其他特性;而現在 Groovy 來了,它也是這么做的!

與 StrutsTestCase 和 DbUnit 一樣,Groovy 對 JUnit 的 TestCase 的擴展給您的工具箱帶來了一些重要的新特性。這個特殊的擴展允許您通過 groovy 命令運行測試套件,而且提供了一套新的 assert 方法。可以用這些方法很方便地斷言腳本的運行是否正確,以及斷言各種數組類型的長度和內容等。

享受 GroovyTestCase 的快樂
了解 GroovyTestCase 的能力最好的辦法,莫過于實際看到它的效果。在清單 3 中,我已經編寫了一個新的 SimpleFilterTest,但是這次我要擴展 GroovyTestCase 來實現它:

清單 3. 一個真正的 GroovyTestCase

import groovy.util.GroovyTestCase
import com.vanward.sedona.frmwrk.filter.impl.SimplePackageFilter

class SimpleFilterTest extends GroovyTestCase {
	
  void testSimpleJavaPackage() {
    fltr = new SimplePackageFilter()
    fltr.setFilter("java.")		
    val = fltr.applyFilter("java.lang.String")		
    assertEquals("value should be true", true, val)
  }	
}

請注意,可以通過命令行來運行該測試套件,沒有運行基于 Java 的 JUnit 測試套件所需要的 main() 方法。實際上,如果我用 Java 代碼編寫上面的 SimpleFilterTest,那么代碼看起來會像清單 4 所示的那樣:

清單 4. 用 Java 代碼編寫的同樣的測試用例

import junit.framework.TestCase;
import com.vanward.sedona.frmwrk.filter.Filter;
import com.vanward.sedona.frmwrk.filter.impl.SimplePackageFilter;

public class SimplePackageFilterTest extends TestCase {       

   public void testSimpleRegex() {
	Filter fltr = new SimplePackageFilter();
	fltr.setFilter("java.");
	boolean val = fltr.applyFilter("java.lang.String");
	assertEquals("value should be true", true, val);
   }
	
   public static void main(String[] args) {
 	junit.textui.TestRunner.run(SimplePackageFilterTest.class);
   }
}

用斷言進行測試
除了可以讓您通過命令行運行測試之外, GroovyTestCase 還向您提供了一些特別方便的 assert 方法。例如, assertArrayEquals,它可以檢查兩個數據中對應的每一個值和各自的長度,從而斷言這兩個數據是否相等。從清單 5 的示例開始,就可以看到 Groovy 斷言的實際效果,清單 5 是一個簡潔的基于 Java 的方法,它把 string 分解成數組。(請注意,我可能使用了 Java 1.4 中新添加的 string 特性編寫以下的示例類。我采用 Jakarta Commons StringUtils 類來確保與 Java 1.3 的后向兼容性。)

清單 5. 定義一個 Java StringSplitter 類

import org.apache.commons.lang.StringUtils;

public class StringSplitter {
  public static String[] split(final String input, final String separator){
   return StringUtils.split(input, separator);
  }
}

清單 6 展示了用 Groovy 測試套件及其對應的 assertArrayEquals 方法對這個類進行測試是多么簡單:

清單 6. 使用 GroovyTestCase 的 assertArrayEquals 方法

import groovy.util.GroovyTestCase
import com.vanward.resource.string.StringSplitter

class StringSplitTest extends GroovyTestCase {
	
  void testFullSplit() {
    splitAr = StringSplitter.split("groovy.util.GroovyTestCase", ".")		
    expect = ["groovy", "util", "GroovyTestCase"].toArray()
    assertArrayEquals(expect, splitAr)		
  }	
}

其他方法
Groovy 可以讓您單獨或成批運行測試。使用 GroovyTestCase 擴展,運行單個測試毫不費力。只要運行 groovy 命令,后面跟著要運行的測試套件即可,如清單 7 所示:

清單 7. 通過 groovy 命令運行 GroovyTestCase 測試用例

$./groovy test/com/vanward/sedona/frmwrk/filter/impl/SimpleFilterTest.groovy
.
Time: 0.047

OK (1 test)

Groovy 還提供了一個標準的 JUnit 測試套件,叫作 GroovyTestSuite。只要運行該測試套件,把腳本的路徑傳給它,它就會運行腳本,就像 groovy 命令一樣。這項技術的好處是,它可以讓您在 IDE 中運行腳本。例如,在 Eclipse 中,我只是為示例項目建立了一個新的運行配置(一定要選中 “Include external jars when searching for a main class”),然后找到主類 groovy.util.GroovyTestSuite,如圖 1 所示:

圖 1. 用 Eclipse 運行 GroovyTestSuite
圖 1. 用 Eclipse 運行 GroovyTestSuite

在圖 2 中,您可以看到當點擊 Arguments 標簽,寫入腳本的路徑時,會發生了什么:

圖 2. 在 Eclipse 中指定腳本的路徑
圖 2. 在 Eclipse 中指定腳本的路徑

運行一個自己喜歡的 JUnit Groovy 腳本,實在是很簡單,只要在 Eclipse 中找到對應的運行配置就可以了。

用 Ant 和 Maven 進行測試
這個像 JUnit 一樣的框架的美妙之處還在于,它可以把整套測試作為 build 的一部分運行,不需要人工進行干預。隨著越來越多的人把測試用例加入代碼基(code base),整體的測試套件日益增長,形成一個極好的回歸平臺(regression platform)。更妙的是,Ant 和 Maven 這樣的 build 框架已經加入了報告特性,可以歸納 Junit 批處理任務運行的結果。

把一組 Groovy 測試用例整合到某一個構建中的最簡單的方法是把它們編譯成普通的 Java 字節碼,然后把它們包含在 Ant 和 Maven 提供的標準的 Junit 批命令中。幸運的是,Groovy 提供了一個 Ant 標簽,能夠把未編譯的 Groovy 腳本集成到字節碼中,這樣,把腳本轉換成有用的字節碼的處理工作就變得再簡單不過。例如,如果正在使用 Maven 進行構建工作,那么只需在maven.xml 文件中添加兩個新的目標、在 project.xml 中添加兩個新的相關性、在 build.properties 文件中添加一個簡單的標志就可以了。

我要從更新 maven.xml 文件開始,用新的目標來編譯示例腳本,如清單 8 所示:

清單 8. 定義 Groovyc 目標的新 maven.xml 文件

 <goal name="run-groovyc" prereqs="java:compile,test:compile">
   
   <path id="groovy.classpath">
     <pathelement path="${maven.build.dest}"/>
     <pathelement path="target/classes"/>
     <pathelement path="target/test-classes"/>
     <path refid="maven.dependency.classpath"/>
   </path>

 <taskdef name="groovyc" classname="org.codehaus.groovy.ant.Groovyc">
    <classpath refid="groovy.classpath"/>
 </taskdef>

 <groovyc destdir="${basedir}/target/test-classes" srcdir="${basedir}/test/groovy" 
          listfiles="true">
	<classpath refid="groovy.classpath"/>
 </groovyc>

 </goal>

上面代碼中發生了以下幾件事。第一,我定義一個名為 run-groovyc 的新目標。該目標有兩個前提條件, java:compile 編譯示例源代碼, test:compile 編譯普通的 Java-JUnit 類。我還用 <path> 標簽創建了一個 classpath。在該例中,classpath 把 build 目錄(保存編譯后的源文件)和與它相關的所有依存關系(即 JAR 文件)整合在一起。接著,我還用 <taskdef> Ant 標簽定義了 groovyc 任務。

而且,請您注意我在 classpath 中是如何告訴 Maven 到哪里去找 org.codehaus.groovy.ant.Groovyc 這個類。在示例的最后一行,我定義了 <groovyc> 標簽,它會編譯在 test/groovy 目錄中發現的全部 Groovy 腳本,并把生成的 .class 文件放在 target/test-classes 目錄中。

一些重要細節
為了編譯 Groovy 腳本,并運行生成的字節碼,我必須要通過 project.xml 文件定義兩個新的依存關系( groovyasm),如清單 9 所示:

清單 9. project.xml 文件中的新的依存關系

  <dependency>
    <groupId>groovy</groupId>
    <id>groovy</id>
    <version>1.0-beta-6</version>
  </dependency>

  <dependency>
    <groupId>asm</groupId>
    <id>asm</id>
    <version>1.4.1</version>
  </dependency>

一旦將腳本編譯成普遍的 Java 字節碼,那么任何標準的 JUnit 運行器就都能運行它們。因為 Ant 和 Maven 都擁有 JUnit 運行器標簽,所以下一步就是讓 JUnit 挑選新編譯的 Groovy 腳本。而且,因為 Maven 的 JUnit 運行器使用模式匹配來查找要運行的測試套件,所以需要在 build.properties 文件中添加一個特殊標記,如清單 10 所示,該標記告訴 Maven 去搜索類而不是搜索 .java 文件。

清單 10. Maven 項目的 build.properties 文件

 maven.test.search.classdir = true

最后,我在 maven.xml 文件中定義了一個測試目標( goal),如清單 11 所示。這樣做可以確保 在單元測試運行之前,使用新的 run-groovyc 目標編譯 Groovy 腳本。

清單 11. maven.xml 的新目標

  <goal name="test">
    <attainGoal name="run-groovyc"/>
    <attainGoal name="test:test"/>    	
  </goal>

最后一個,但并非最不重要
有了新定義的兩個目標(一個用來編譯腳本,另外一個用來運行 Java 和 Groovy 組合而成的單元測試),剩下的事就只有運行它們,檢查是不是每件事都順利運行!

在清單 12 中,您可以看到,當我運行 Maven,給 test 傳遞目標之后,會發生了什么,它首先包含 run-groovyc 目標(而該目標恰好還包含 java:compiletest:compile 這兩個目標),然后包含 Maven 中自帶的標準的 test:test 目標。請注意觀察目標 test:test 是如何處理新生成的 Groovy 腳本(在該例中,是新 編譯的 Groovy 腳本) 以及普通的 Java JUnit 測試。

清單 12. 運行新的測試目標

$ ./maven test

test:
java:compile:
    [echo] Compiling to /home/aglover/dev/target/classes
    [javac] Compiling 15 source files to /home/aglover/dev/target/classes

test:compile:
    [javac] Compiling 4 source files to /home/aglover/dev/target/test-classes

run-groovyc:
    [groovyc] Compiling 2 source files to /home/aglover/dev/target/test-classes
    [groovyc] /home/aglover/dev/test/groovy/test/RegexFilterTest.groovy
    [groovyc] /home/aglover/dev/test/groovy/test/SimpleFilterTest.groovy

test:test:    
    [junit] Running test.RegexFilterTest
    [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.656 sec    
    [junit] Running test.SimpleFilterTest
    [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.609 sec
    [junit] Running test.SimplePackageFilterTest
    [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 0.578 sec    
BUILD SUCCESSFUL
Total time: 42 seconds
Finished at: Tue Sep 21 17:37:08 EDT 2004

結束語
實戰 Groovy 系列的第一期中,您學習了 Groovy 這個令人興奮的腳本語言最實用的應用當中的一個。對于越來越多開發人員而言,單元測試是開發過程的重要組成部分;而使用 Groovy 和 JUnit 對 Java 代碼進行測試就變成了輕而易舉的事情。

Groovy 簡單的語法、內部的靈活性,使其成為迅速編寫有效的 JUnit 測試、將測試整合到自動編譯中的一個優秀平臺。對于像我一樣為代碼質量發狂的人來說,這種組合極大地減少了我的 神經緊張,還讓我可以得到我想做得最好的東西:編寫“防彈”軟件。快點行動吧。

因為這是一個新的系列,所以我非常希望您能一起來推動它前進。如果您對 Groovy 有什么想了解的事情,請 發郵件給我,讓我知道您的要求!我希望您會繼續支持本系列的下一期,在下期中,我將介紹用 Groovy 進行 Ant 腳本編程。