擴充 BlockJUnit4ClassRunner


JUnit 4的預設Runner是org.junit.runners.JUnit4,實際上JUnit4繼承BlockJUnit4ClassRunnerJUnit 4.7引進)之後,並沒有定義 任何新的方法:
...
public final class JUnit4 extends BlockJUnit4ClassRunner {
    public JUnit4(Class<?> klass) throws InitializationError {
        super(klass);
    }
}

BlockJUnit4ClassRunner 執行測試的方式是以Statement block為單位,是將測試 分為數種職責,由每個Statement負責,每個Statement各負責自己的執行目的,執行完轉交下一個Statement,一路執行下去,直到所 有Statement執行完畢為止,具體而言,是實現了 Chain of Responsibility 模式 的設計方式。

具體而言,如果你想在執行測試時,加入額外的職責,你可以繼承Statement類 別,定義其evaluate()方 法,實現你在測試中想要加入的動作。例如,雖然JUnit 4本身提供有@Before、@After, 在每個測試方法前、後執行,但也許你有些測試方法執行前、後,想要單獨指定某幾個方法執行,而這幾個方法並非其它測試方法所需要,你可以如下實作Statement
package cc.openhome;

import org.junit.runners.model.Statement;

import java.lang.reflect.Method;
import java.util.List;
import java.util.ArrayList;

public class PrePostTestStatement extends Statement {
private Statement invoker;
private Object target;
private List<Method> preMethods = new ArrayList<Method>();
private List<Method> postMethods = new ArrayList<Method>();

public PrePostTestStatement(Statement invoker, Object target) {
this.invoker = invoker;
this.target = target;
}

@Override
public void evaluate() throws Throwable {
for(Method method : preMethods) {
method.invoke(target, null);
}

Throwable throwable = null;
try {
invoker.evaluate(); // 記得呼叫下一個Statement
}
catch(Throwable t) {
throwable = t;
}

for(Method method : postMethods) {
method.invoke(target, null);
}
if(throwable != null) {
throw throwable;
}
}

public void addPre(Method method) {
preMethods.add(method);
}

public void addPost(Method method) {
postMethods.add(method);
}
}


在這邊,你定義了兩個標註(Annotation):
package cc.openhome;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PreTest {
String[] value();
}

package cc.openhome;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PostTest {
String[] value();
}

你希望可以如下使用這兩個新標註:
package cc.openhome;

import static org.junit.Assert.assertEquals;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import test.cc.openhome.TestCase;

@RunWith(value = BlockGossipClassRunner.class)
public class CalculatorTest {
private Calculator calculator;

@Before
public void setUp() {
calculator = new Calculator();
}

@After
public void tearDown() {
calculator = null;
}

public void preTestPlus() {
System.out.println("preTestPlus");
}

public void postTestPlus() {
System.out.println("postTestPlus");
}

@PreTest("preTestPlus")
@PostTest("postTestPlus")
@Test
public void testPlus() {
int expected = 1;
int result = calculator.plus(3, 2);
assertEquals(expected, result);
}

@Test
public void testMinus() {
int expected = 1;
int result = calculator.minus(3, 2);
assertEquals(expected, result);
}
}

該如何自訂Runner來讓這兩個標註生效?你可以檢視BlockJUnit4ClassRunner的 原始碼,可發現在測試運行開始時, 會依序呼叫run()、classBlock()方法,其中classBlock()方 法為:
    protected Statement classBlock(final RunNotifier notifier) {
        Statement statement= childrenInvoker(notifier);
        statement= withBeforeClasses(statement);
        statement= withAfterClasses(statement);
        return statement;
    }

childrenInvoker()是 實例範圍的Statement block,之後套用@BeforeClass與 @AfterClass的Statement Block。換言之,如果你想要在Class block層級介入某些Statement block,可以重新定義classBlock()方法。

childrenInvoker ()的呼叫中,會呼叫到getChildren()方法,其透過computeTestMethods()傳回一個List,當中會有一些 FrameworkMethod實例,每個FrameworkMethod實例,是標註有@Test的方法之包裹物件:
    protected List<FrameworkMethod> computeTestMethods() {
        return getTestClass().getAnnotatedMethods(Test.class);
    }

getTestClass ()會取得TestClass實例,這是一開始傳入測試類別Class實例給BlockJUnit4ClassRunner建構時就產生的 物件,是一個輔助類別,用來取得一些反射(Reflection)的相關資訊。

在取得 FrameworkMethod的List後,會再呼叫到BlockJUnit4ClassRunner 的runChild()方法:
    protected void runChild(FrameworkMethod method, RunNotifier notifier) {
        EachTestNotifier eachNotifier= makeNotifier(method, notifier);
        if (method.getAnnotation(Ignore.class) != null) {
            runIgnored(eachNotifier);
        } else {
            runNotIgnored(method, eachNotifier);
        }
    }

對於標註有 @Ignore的方法並不執行,直接發出一個忽略的通知給RunNotifier,否則就呼叫runNotIgnored():
    private void runNotIgnored(FrameworkMethod method,
            EachTestNotifier eachNotifier) {
        eachNotifier.fireTestStarted();
        try {
            methodBlock(method).evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            eachNotifier.addFailure(e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }

methodBlock ()會為每個FrameworkMethod建立測試類別的實例:
    protected Statement methodBlock(FrameworkMethod method) {
        Object test;
        try {
            test= new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (Throwable e) {
            return new Fail(e);
        }

        Statement statement= methodInvoker(method, test);
        statement= possiblyExpectingExceptions(method, test, statement);
        statement= withPotentialTimeout(method, test, statement);
        statement= withBefores(method, test, statement);
        statement= withAfters(method, test, statement);
        statement= withRules(method, test, statement);
        return statement;
    }

所以,如果你想在Method block的層次,作一些額外的執行,則可以重新定義methodBlock()方 法。

methodInvoker()中,會建立InvokeMethod實例:
    protected Statement methodInvoker(FrameworkMethod method, Object test) {
        return new InvokeMethod(method, test);
    }

InvokeMethod 包裹了FrameworkMethod,在它的evaluate()中,實現了被標註為@Test的方法之執行。所以,如果你想在被標註為@Test的方法執行之前後,作 一些Statement的介入,則可以重新定義methodInvoker()方法。

對於我們的需求,則需定義methodInvoker()方 法:
package cc.openhome;

import java.lang.reflect.Method;

import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

public class BlockGossipClassRunner extends BlockJUnit4ClassRunner {
public BlockGossipClassRunner(Class<?> clz)
throws InitializationError {
super(clz);
}

@Override
protected Statement methodInvoker(FrameworkMethod method,
Object test) {
PrePostTestStatement statement =
new PrePostTestStatement(
super.methodInvoker(method, test), test);
PreTest preTest = method.getAnnotation(PreTest.class);
if(preTest != null) {
addMethod(test, statement, preTest.value(), true);
}
PostTest postTest = method.getAnnotation(PostTest.class);
if(postTest != null) {
addMethod(test, statement, postTest.value(), false);
}
return statement;
}

private void addMethod(Object test, PrePostTestStatement statement,
String[] methodNames, boolean isPre) {
for(String methodName : methodNames) {
Method[] methods = test.getClass().getMethods();
for(Method method : methods) {
if(method.getName().equals(methodName)) {
if(isPre) {
statement.addPre(method);
}
else {
statement.addPost(method);
}
}
}
}
}
}

現在,你可以直接 運行先前定義的CalculatorTest,看看指定的@PreTest、@PostTest是否有作用了。