單元測試-Junit 使用及其原理分析
引入
在 build.gradle 文件中
dependencies {
testCompile 'junit:junit:4.12'
}
這其中會引入兩個jar:junit-4.12.jar 和 hamcrest-core-1.3.jar
介紹
junit 中兩個重要的類 Assume 和 Assert , 以及其他一些重要的注解: BeforeClass , AfterClass , After , Before 及 Test , Ignore 。
其中, BeforeClass 和 AfterClass 在每個類加載的開始和結束時運行,需要設置 static 方法;而 Before 和 After 則是在每個測試方法的開始之前和結束之后運行。
在 hamcrest-core 的 jar 包中,在 org.hamcrest.core 包中提供了一系列操作運算封裝,使測試代碼更加地易讀。如 is , not , allOf , anyOf 等。
代碼示例
@Test
public void testAssertArrayEquals() {
byte[] expected = "trial".getBytes();
byte[] actual = "trial".getBytes();
assertArrayEquals("failure - byte arrays not same", expected, actual);
}
@Test
public void testAssertEquals() {
assertEquals("failure - strings are not equal", "text", "text");
}
@Test
public void testAssertFalse() {
assertFalse("failure - should be false", false);
}
@Test
public void testAssertNotNull() {
assertNotNull("should not be null", new Object());
}
@Test
public void testAssertNotSame() {
assertNotSame("should not be same Object", new Object(), new Object());
}
@Test
public void testAssertNull() {
assertNull("should be null", null);
}
@Test
public void testAssertSame() {
Integer aNumber = Integer.valueOf(768);
assertSame("should be same", aNumber, aNumber);
}
@Test
public void testAssertTrue() {
assertTrue("failure - should be true", true);
}</pre>
以上代碼來自官方介紹的 Demo , 列舉的是常用而又基礎的操作,但遇到復雜的集合判斷操作,就力不從心了,不過 Junit 提供了另一更為強大的 assertThat 方法,首先來看看它的使用:
// JUnit Matchers assertThat
@Test
public void testAssertThatBothContainsString() {
assertThat("albumen", both(containsString("a")).and(containsString("b")));
}
@Test
public void testAssertThatHasItems() {
assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
}
@Test
public void testAssertThatEveryItemContainsString() {
assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
}
// Core Hamcrest Matchers with assertThat
@Test
public void testAssertThatHamcrestCoreMatchers() {
assertThat("good", allOf(equalTo("good"), startsWith("good")));
assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
assertThat(new Object(), not(sameInstance(new Object())));
}</pre>
這里的 assertThat 用了兩種方法:一個是 JunitMatchers ,另一個就是 hamcrest matchers 的 assertThat,不過后者提供的功能相當強大,前者的方法已經標為廢棄了。
另外,官方也提及了其它第三方提供的 Matchers 實現:
所以再次我們只看后者,可以看出來的是其方法的最后一個參數非常靈活,緊接著我們看看其怎么實現的?
assertThat 方法實現
public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
assertThat("", actual, matcher);
}
public static <T> void assertThat(String reason, T actual,
Matcher<? super T> matcher) {
MatcherAssert.assertThat(reason, actual, matcher);
}</pre>
再定位到 MatcherAssert 類的方法 assertThat :
public static <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
if (!matcher.matches(actual)) {
Description description = new StringDescription();
description.appendText(reason)
.appendText("\nExpected: ")
.appendDescriptionOf(matcher)
.appendText("\n but: ");
matcher.describeMismatch(actual, description);
throw new AssertionError(description.toString());
}
}</pre>
可以看出真正地判斷方法是通過 Matcher 類的 matches 方法,若是不滿足的話,則返回 AssertionError 。所以真正的核心就是 Matcher ,而關于它的實現都在 hamcrest-core-1.3 包中,看看其實現的類結構圖:

matcher
看一下其的實現,就可發現上文提到的 is , anyof 等等靜態方法都是返回一個相應的 Matcher,這樣通過一個簡單的抽象,在這里就提供了極大的靈活性。若是感覺它提供的這些不滿足的話,也可自己進行來進行重寫,按自己的需求來定制實現。
Rule 介紹
同樣地,當我們越來越多需要進行單元測試時,就需要使用 Rule 來幫忙了。其主要目的是針對一個測試類中的每個單元測試方法進行統一添加一些行為。代碼則使用 @Rule 注解的形式來添加至類的屬性上。
在 Junit 框架中,其相對應的接口是 TestRule ,而主要的實現有:
- ErrorCollector: 將大量的錯誤收集起來
- ExpectedException: 對拋出的錯誤做斷言
- ExternalResource: 可對測試方法的開始和結束添加回調
- TemporaryFolder: 用來創建文件,并在測試結束時自動刪除
- TestName: 用來獲取測試所執行的方法名稱
- TestWatcher: 可在測試方法的執行期間添加邏輯
- Timeout: 超過固定的時間讓測試結束
- Verifier: 當狀態不正確時,可讓測試結束
它們的更多使用方法,可參照官網的 Rules 介紹 。
實現原理分析
Junit4 中的測試代碼可被執行,是因為其真正的入口是名為 JUnitCore 。它作為 Junit 的 Facade (門面)模式,來對外進行交互。另外,其有一個靜態的 main 方法:
public static void main(String... args) {
Result result = new JUnitCore().runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}
所以,當我們執行單元測試的時候,其實也就是運行了一個新的進程應用程序,其入口就在這里。我們執行分析的時候,也從這里開始:
其會調到一個 run(Runner runner) 的方法,而 Runner 是一個抽象類,其實現針對不同的平臺又有好多個。這里主要提及兩個,一個是 Junit4ClassRunner,它是 4.4 版本及之前的采用的,之后被廢棄掉了,而采用了繼承實現抽象類 ParentRunner 的 BlockJUnit4ClassRunner 類,它在 4.5 之后被采用。這里主要查看后者,先看 ParentRunner 對其接口 Runner 中方法 run 的實現:
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
try {
Statement statement = classBlock(notifier);
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
testNotifier.addFailure(e);
}
}
其中,主要通過 classBlock 方法生成的 Statement 的 evaluate 來進行調用,先看它是怎么生成的:
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
}
return statement;
}
這里主要的方法 childrenInvoker 會調用一個抽象的方法 protected abstract void runChild(T child, RunNotifier notifier); ,它則是由子類來實現。另外看到的是,當測試類中的測試方法都沒有被忽略的時候,則會使用 with 對應的三個方法來添加其獲取注解 BeforeClass , AfterClass , ClassRule 對應的信息,并添加至其調用的 statement 中。
接下來查看 BlockJUnit4ClassRunner 的 runChild 的實現:
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
Description description = describeChild(method);
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
其中,若是添加了 @ignore 的注解,則不會得到調用。看看 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;
}</pre>
在這個 statement 的獲取中,通過使用組合的方式,會這個 statement 添加 Before , After 及其它 Rule 的鏈式調用,最后生成一個 statement 來返回。
總結
可以看出 Junit 是一個簡單而又強大的庫,不然不會經久不衰。其簡單的實現但又強大的功能已經基本滿足我們絕大多數的需求。但在這里還有一個疑問就是不知道 Junit 是如何繼承到 Android Studio 的 IDE 中,并是如何直接調用我們的測試方法或者測試類的?有興趣的小伙伴可加 qq 群 289926871 一起討論哈。
參考資料
來自:http://www.jianshu.com/p/ae9c837a0af9