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

