一、Rule簡介
Rule是JUnit4中的新特性,它讓我們可以擴展JUnit的功能,靈活地改變測試方法的行爲。JUnit中用@Rule和@ClassRule兩個註解來實現Rule擴展,這兩個註解需要放在實現了TestRule藉口的成員變量(@Rule)或者靜態變量(@ClassRule)上。@Rule和@ClassRule的不同點是,@Rule是方法級別的,每個測試方法執行時都會調用被註解的Rule,而@ClassRule是類級別的,在執行一個測試類的時候只會調用一次被註解的Rule
二、JUnit內置Rule
JUnit4中默認實現了一些常用的Rule:
TemporaryFolder Rule
使用這個Rule可以創建一些臨時目錄或者文件,在一個測試方法結束之後,系統會自動清空他們。
//創建TemporaryFolder Rule
//可以在構造方法上加入路徑參數來指定臨時目錄,否則使用系統臨時目錄
@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void testTempFolderRule() throws IOException {
//在系統的臨時目錄下創建文件或者目錄,當測試方法執行完畢自動刪除
tempFolder.newFile("test.txt");
tempFolder.newFolder("test");
}
ExternalResource Rule
ExternalResource 是TemporaryFolder的父類,主要用於在測試之前創建資源,並在測試完成後銷燬。
File tempFile;
@Rule
public ExternalResource extResource = new ExternalResource() {
//每個測試執行之前都會調用該方法創建一個臨時文件
@Override
protected void before() throws Throwable {
tempFile = File.createTempFile("test", ".txt");
}
//每個測試執行之後都會調用該方法刪除臨時文件
@Override
protected void after() {
tempFile.delete();
}
};
@Test
public void testExtResource() throws IOException {
System.out.println(tempFile.getCanonicalPath());
}
ErrorCollector Rule
ErrorCollector允許我們收集多個錯誤,並在測試執行完後一次過顯示出來
@Rule
public ErrorCollector errorCollector = new ErrorCollector();
@Test
public void testErrorCollector() {
errorCollector.addError(new Exception("Test Fail 1"));
errorCollector.addError(new Throwable("fff"));
}
Verifier Rule
Verifier是ErrorCollector的父類,可以在測試執行完成之後做一些校驗,以驗證測試結果是不是正確
String result;
@Rule
public Verifier verifier = new Verifier() {
//當測試執行完之後會調用verify方法驗證結果,拋出異常表明測試失敗
@Override
protected void verify() throws Throwable {
if (!"Success".equals(result)) {
throw new Exception("Test Fail.");
}
}
};
@Test
public void testVerifier() {
result = "Fail";
}
TestWatcher Rule
TestWatcher 定義了五個觸發點,分別是測試成功,測試失敗,測試開始,測試完成,測試跳過,能讓我們在每個觸發點執行自定義的邏輯。
@Rule
public TestWatcher testWatcher = new TestWatcher() {
@Override
protected void succeeded(Description description) {
System.out.println(description.getDisplayName() + " Succeed");
}
@Override
protected void failed(Throwable e, Description description) {
System.out.println(description.getDisplayName() + " Fail");
}
@Override
protected void skipped(AssumptionViolatedException e, Description description) {
System.out.println(description.getDisplayName() + " Skipped");
}
@Override
protected void starting(Description description) {
System.out.println(description.getDisplayName() + " Started");
}
@Override
protected void finished(Description description) {
System.out.println(description.getDisplayName() + " finished");
}
};
@Test
public void testTestWatcher() {
/*
測試執行後會有以下輸出:
testTestWatcher(org.haibin369.test.RulesTest) Started
Test invoked
testTestWatcher(org.haibin369.test.RulesTest) Succeed
testTestWatcher(org.haibin369.test.RulesTest) finished
*/
System.out.println("Test invoked");
}
TestName Rule
TestName能讓我們在測試中獲取目前測試方法的名字。
@Rule
public TestName testName = new TestName();
@Test
public void testTestName() {
//打印出測試方法的名字testTestName
System.out.println(testName.getMethodName());
}
Timeout與ExpectedException Rule
分別用於超時測試與異常測試,在JUnit4學習筆記(一):基本應用中有提到,這裏不再舉例。
三、實現原理與部分源碼解析
在Junit4的默認Test Runner - org.junit.runners.BlockJUnit4ClassRunner中,有一個methodBlock方法:
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;
}
在JUnit執行每個測試方法之前,methodBlock方法都會被調用,用於把該測試包裝成一個Statement。Statement代表一個具體的動作,例如測試方法的執行,Before方法的執行或者Rule的調用,類似於J2EE中的Filter,Statement也使用了責任鏈模式,將Statement層層包裹,就能形成一個完整的測試,JUnit最後會執行這個Statement。從上面代碼可以看到,有以下內容被包裝進Statement中:
1)測試方法的執行;
2)異常測試,對應於@Test(expected=XXX.class);
3)超時測試,對應與@Test(timeout=XXX);
4)Before方法,對應於@Before註解的方法;
5)After方法,對應於@After註解的方法;
6)Rule的執行。
在Statement中,可以用evaluate方法控制Statement執行的先後順序,比如Before方法對應的Statement - RunBefores:
public class RunBefores extends Statement {
private final Statement fNext;
private final Object fTarget;
private final List<FrameworkMethod> fBefores;
public RunBefores(Statement next, List<FrameworkMethod> befores, Object target) {
fNext = next;
fBefores = befores;
fTarget = target;
}
@Override
public void evaluate() throws Throwable {
for (FrameworkMethod before : fBefores) {
before.invokeExplosively(fTarget);
}
fNext.evaluate();
}
}
在evaluate中,所有Before方法會先被調用,因爲Before方法必須要在測試執行之前調用,然後再執行fNext.evaluate()調用下一個Statement。
理解了Statement,再看回Rule的接口org.junit.rules.TestRule:
public interface TestRule {
Statement apply(Statement base, Description description);
}
裏面只有一個apply方法,用於包裹上級Statement並返回一個新的Statement。因此實現Rule主要是需要實現一個Statement。
四、自定義Rule
通過上面的分析,我們大概知道了如何實現一個Rule,下面是一個例子:
/*
用於循環執行測試的Rule,在構造函數中給定循環次數。
*/
public class LoopRule implements TestRule{
private int loopCount;
public LoopRule(int loopCount) {
this.loopCount = loopCount + 1;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
//在測試方法執行的前後分別打印消息
@Override
public void evaluate() throws Throwable {
for (int i = 1; i < loopCount; i++) {
System.out.println("Loop " + i + " started!");
base.evaluate();
System.out.println("Loop "+ i + " finished!");
}
}
};
}
}
使用該自定義的Rule:
@Rule
public LoopRule loopRule = new LoopRule(3);
@Test
public void testLoopRule() {
System.out.println("Test invoked!");
}
執行後打印出以下信息:
Loop 1 started!
Test invoked!
Loop 1 finished!
Loop 2 started!
Test invoked!
Loop 2 finished!
Loop 3 started!
Test invoked!
Loop 3 finished!