JUnit4學習筆記(四):利用Rule擴展JUnit

一、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!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章