簡介:在采用測試驅動開發的項目中,有一個經常困擾開發者的問題是:當存在大量的測試用例時, 一次運行完所有的測試用例要花費很長的時間,采用 TestSuite 來組織測試用例的方式缺乏靈活性,通 常它的組織結構大體和 Java Package/Class 的組織結構類似,不能和當前實現的業務需求完全相關。本 文將通過擴展 JUnit4 來實現一種可以更加高效靈活地組織和運行測試用例的解決方案,促進測試驅動開 發實踐更有效地進行。
實際 Java 開發中單元測試常遇到的問題
在敏捷開發中,為了提高軟件開發的效率和質量,測試驅動開發實踐已經被廣泛使用。在測試驅動開 發的項目中,隨著項目開發不斷地深入,積累的測試用例會越來越多。測試驅動開發的一個最佳實踐是隨 時運行測試用例,保證任何時候測試用例都能成功執行,從而保證項目的代碼是可工作的。當測試用例數 量很多時,一次運行所有測試用例所消耗的時間可能會很長,導致運行測試用例的成本很高。所以在實際 敏捷開發中,如何組織、運行測試用例以促進測試驅動開發成為一個值得探究的問題。
JUnit 是 Java 開發中最常用的單元測試工具。在 JUnit3 用 TestSuite 來顯式地組織想要運行的 TestCase,通常 TestSuite 的組織大體上和 Java Package/Class 的組織類似,但這樣並不能和當前正 在實現的業務需求完全相關,顯得比較笨拙,比如說要運行某個子模塊下所有的 TestCase,或者運行跟 某個具體功能相關的 TestCase,涉及到的 TestCase 數量可能較多,采用定義 TestSuite 的方式一個個 地添加 TestCase 很低效並且繁瑣。在 JUnit4 中同樣只能顯式地組織要運行的 TestCase。
怎麼樣解決這些問題,新發布的 JUnit4 提供了開發人員擴展的機制,可以通過對 JUnit 進行擴展來 提供一種解決的方法。
JUnit4 的新特性和擴展機制
JUnit4 引入了 Java5 的 Annotation 機制,來簡化原有的使用方法。測試用例不再需要繼承 TestCase 類,TestSuite 類也取消了,改用 @Suite.SuiteClasses 來組織 TestCase。但是這種還是通 過顯示指定 TestCase 來組織運行的結構,不能解決上述的問題。關於 JUnit4 的新特性具體可以參考 developerworks 的文章。
JUnit4 的實現代碼中提供了 Runner 類來封裝測試用例的執行。它本身提供了 Runner 的多種實現, 比如 ParentRunner 類、Suite 類,BlockJUnit4ClassRunner 類。我們可以充分利用 JUnit4 提供的已 有設施來對它進行擴展,實現我們期望的功能。
首先我們來分析一下 JUnit4 在運行一個測試用例時,它內部的核心類是如何工作的。圖 1 展示了 JUnit4 運行測試用例時,核心類之間的調用關系。
圖 1. JUnit4 核心類之間的調用關系
在 JUnit4 中,Runner 類定義了運行測試用例的接口,默認提供的 Runner 實現類有 Suite、 BlockJUnit4ClassRunner、Parameterized 等等。Suite 類相當於 JUnit3 中的 TestSuite, BlockJUnit4ClassRunner 用來執行單個的測試用例。BlockJUnit4ClassRunner 關聯了一個 TestClass 類,TestClass 封裝了測試用例的 Class 元數據,可以訪問到測試用例的 method、annotation 等。 FrameworkMethod 封裝了測試用例方法的元數據。從下圖中我們可以看到這些類的關系。
圖 2. JUnit4 核心類
通過擴展 JUnit4,我們一方面可以無縫地利用 JUnit4 執行測試用例的能力,另一方面可以將我們定 義的一些業務功能添加到 JUnit 中來。我們將自定義一套與運行測試用例相關的業務屬性的 Annotation 庫,定義自己的過濾器,擴展 JUnit 類的 Runner,從而實現定制化的測試用例的執行。
JUnit4 擴展的實現
下面我們來描述一下對 JUnit4 擴展的實現。擴展包括 4 個模塊,Annotation 定義、用戶查詢條件 封裝、過濾器定義、核心類定義。
JUnit4 用 Annotation 來定義測試用例運行時的屬性。我們可以定義自己的 Annotation 庫。通過定 義出具體項目中和執行測試用例相關的屬性元數據, 比如某個模塊,某個特性,將這些屬性通過 Annotation 附加到測試用例中,在擴展的 Runner 中利用過濾器對測試用例進行過濾,從而執行目標測 試用例。
根據實際項目中的開發經驗,我們大體抽象出了如下的幾種 Annotation, 可以映射到我們項目的業務 功能劃分上;
表 1. 擴展的 Annotation 的具體用法
名稱 參數 作用域 Product 字符串參數,指定要測試的產品項目名稱 類 Release 字符串參數,指定具體的 Release 編號 類、方法 Component 字符串參數,指定子模塊、子系統 類 Feature 字符串參數,指定某個具體的功能、需求 類、方法 Defect 字符串參數,指定測試中發現的 Defect 的編號 類、方法 UseCaseID 字符串參數,指定 UseCase 的編號 類、方法
當我們想要運行所有和 Feature 相關的測試用例時,我們只要指定執行條件,就可以只運行那部分測 試用例,而不會去運行全部的測試用例。這種方法從業務的角度來看,更加具有針對性,而且簡潔快速, 比用傳統的通過 TestSuite 指定測試用例的方式更加適合測試驅動開發的場景。下面給出 Feature Annotation 和 Release Annotation 的定義作為示例。
清單 1:Feature Annotation 的定義
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Feature {
String value();
}
清單 2:Release Annotation 的定義
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Release {
String value();
}
接下來是封裝用戶輸入的執行條件。在這裡我們約定用戶輸入的執行條件的格式是:“條件 A = 值 A ,條件 B = 值 B”。比如用戶想執行 Release A 中的跟 Feature B 相關的測試用例和方法,那麼用戶 的輸入條件可以定義為“Release=A,Feature=B”。下圖是封裝用戶輸入的類的結構:
圖 3. 封裝用戶輸入的執行條件的類
過濾器是用來根據用戶輸入,對目標測試用例和測試方法進行過濾,從而找到符合條件的測試用例方 法。用戶輸入的每個條件都會生出相應的一個過濾器,只有測試用例滿足過濾器鏈中所有的過濾條件,測 試用例才能被執行。下面的清單展示了過濾器接口的定義和過濾器工廠的核心實現。過濾器工廠會根據用 戶輸入的條件來創建對應的過濾器。
清單 3 . Filter 接口的定義
public interface Filter {
public boolean shouldRun(IntentionObject object);
}
清單 4 . FilterFactory 的部分實現
public class FilterFactory {
public static Map<Class<?>, List<Filter>> createFilters (String intention)
throws ClassNotFoundException{
Map<Class<?>, List<Filter>> filters = new HashMap<Class<?>, List<Filter>>();
String[] splits = intention.split(ExtensionConstant.REGEX_COMMA);
for(String split : splits){
String[] pair = split.split(ExtensionConstant.REGEX_EQUAL);
if(pair != null && pair.length == 2){
Filter filter = createFilter(pair[0],pair[1]);
String annotationType = ExtensionConstant.ANNOTATION_PREFIX + pair [0];
Class<?> annotation = Class.forName(annotationType);
List<Filter> filterList = null;
if(filters.containsKey(annotation)){
filterList = filters.get(annotation);
}else{
filterList = new ArrayList<Filter>();
}
filterList.add(filter);
filters.put(annotation, filterList);
}
}
return filters;
}
………………
}
核心類模塊中的類是對 JUnit4 中的類的擴展,從下圖中可以看到兩者的繼承關系:
圖 4. 核心擴展類和 JUnit4 中類的繼承關系
Request 類是 JUnit4 中用來表示一次測試用例請求的抽象概念。它是一次測試用例執行的發起點。 RunerBuilder 會根據測試用例來創建相應的 Runner 實現類。BlockJUnit4ClassRunner 是 JUnit4 中用 來執行單獨一個測試用例的 Runner 實現類。我們通過擴展它,來獲得 JUnit 執行測試用例的能力,同 時在 ExtensionRunner 中調用過濾器對測試用例方法進行過濾,從而根據我們定義的業務規則來執行測 試用例。Result 類是 JUnit4 中用來封裝測試用例執行結果的類,我們對它進行了擴展,來格式化測試 用例執行結果的輸出。下面給出 ExtensionRunner 的部分實現。
清單 5. ExtensionRunner 部分實現
public class ExtensionRunner extends BlockJUnit4ClassRunner {
private Map<Class<?>, List<Filter>> filtersForAnnotation;
public ExtensionRunner(Class<?> klass, String intention)
throws InitializationError, ClassNotFoundException {
super(klass);
filtersForAnnotation = FilterFactory.createFilters(intention);
}
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
runChildren(notifier);
}
};
}
protected void runChildren(final RunNotifier notifier) {
for (final FrameworkMethod each : getFilteredChildren()) {
runChild(each, notifier);
}
}
protected List<FrameworkMethod> getFilteredChildren() {
ArrayList<FrameworkMethod> filtered = new ArrayList<FrameworkMethod>();
for (FrameworkMethod each : getChildren()) {
if (shouldRun(each)) {
filtered.add(each);
}
}
return filtered;
}
protected boolean shouldRun(FrameworkMethod method) {
List<Boolean> result = new ArrayList<Boolean>();
Annotation[] classAnnotations = method.getAnnotations();
Map<Class<?>,Annotation> methodAnnotationMap =
getAnnotaionTypeMap(classAnnotations);
Set<Class<?>> annotationKeys = filtersForAnnotation.keySet ();
for(Class<?> annotationKey : annotationKeys ){
if(methodAnnotationMap.containsKey(annotationKey)){
List<Filter> filters = filtersForAnnotation.get (annotationKey);
if (filters != null) {
for (Filter filter : filters) {
if (filter != null
&& filter.shouldRun(
IntentionFactory.createIntentionObject(
methodAnnotationMap.get(annotationKey)))) {
result.add(true);
}else{
result.add(false);
}
}
}
}else{
return false;
}
}
if(result.contains(false)){
return false;
}else{
return true;
}
……………………
}
}
通過測試用例實例展示 JUnit 擴展的執行效果
1)創建一個 Java 項目,添加對 JUnit4 擴展的引用。項目的結構如下:
圖 5. JUnit4 擴展示例程序的項目結構
2)創建一個簡單的待測試類 Demo 類。
清單 6. 待測試類
public class Demo {
public int add(int a, int b){
return a + b;
}
public int minus(int a, int b){
return a - b;
}
}
3)創建一個 JUnit4 風格的測試用例 DemoTest 類,對上述 Demo 類的方法編寫測試,並將我們自定 義的 Annotation 元數據嵌入到 DemoTest 的測試方法中。
清單 7. 包含了自定義 Annotation 的測試用例
public class DemoTest {
@Test
@Feature("Test Add Feature")
@Release("9.9")
public void testAdd() {
Demo d = new Demo();
Assert.assertEquals(4, d.add(1, 2));
}
@Test
@Release("9.9")
public void testMinus() {
Demo d = new Demo();
Assert.assertEquals(2, d.minus(2, 1));
}
}
4)編寫 Main 類來執行測試用例,輸入自定義的執行測試用例的條件“Release=9.9,Feature=Test Add Feature”,來執行 9.9 Release 中跟 Add Feature 相關的測試用例方法,而不執行跟 Minus Feature 相關的測試用例方法。
清單 8. 調用 JUnit4 擴展來執行測試用例
public class Main {
public static void main(String... args){
new JUnitExtensionCore().runMain(args);
}
}
圖 6. 自定義執行測試用例的條件
5) 執行結果:testAdd() 方法滿足執行的條件,它執行了。testMinus() 方法不滿足執行條件,它 沒有執行。
圖 7. 測試用例執行結果
6)改變自定義的執行條件為“Release=9.9”,執行跟 9.9 Release 相關的所有測試用例方法。
圖 8. 自定義執行測試用例的條件
7) 執行結果:testAdd() 方法和 testMinus() 方法都滿足執行條件,都執行了。
圖 9. 測試用例執行結果
結論
通過上述的代碼示例我們可以看出,我們通過對 JUnit4 進行擴展,從而可以自定義測試用例執行的 條件,將測試用例的執行和具體的業務功能結合在一起,快速地根據業務功能來執行相應的測試用例。這 種細粒度的,以業務屬性來組織測試用例的方法,更加適合以測試用例為本的測試驅動開發的需求。可以 實現快速地運行目標測試用例,從而促進測試驅動開發在項目中更好地實踐。