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是否有作用了。