Junit 是由 Kent Beck 和 Erich Gamma 於 1995 年末着手編寫的框架,自此之後,Junit 框架日益普及,如今已經成爲單元測試 Java 應用程序的事實上的標準。
java
在軟件開發領域中,歷來沒有這樣的事情:少數幾行代碼對大量代碼起着如此重要的做用 --- Martin Fowler緩存
本文注重點在於研究 Junit 運行的基本原理和執行單元測試的流程,因此對於一些額外的信息和數據不單獨準備,本文所使用的測試 case 以下:markdown
package com.glmapper.bridge.boot;
import org.junit.*;
public class JunitSamplesTest {
@Before
public void before(){
System.out.println(".....this is before test......");
}
@After
public void after(){
System.out.println(".....this is after test......");
}
@BeforeClass
public static void beforeClass(){
System.out.println(".....this is before class test......");
}
@AfterClass
public static void afterClass(){
System.out.println(".....this is after class test......");
}
@Test
public void testOne(){
System.out.println("this is test one");
}
@Test
public void testTwo(){
System.out.println("this is test two");
}
}
複製代碼
執行結果以下:app
.....this is before class test...... Disconnected from the target VM, address: '127.0.0.1:65400', transport: 'socket' .....this is before test...... this is test one .....this is after test...... .....this is before test...... this is test two .....this is after test...... .....this is after class test...... 複製代碼
從代碼和執行結果來看,BeforeClass 和 AfterClass 註解分別在測試類開始以前和以後執行,Before 和 After 註解在測試類中每一個測試方法的先後執行。框架
從開發者的角度來看,對於任何一個技術產品組件,若是想要更好的使用它,就意味着必須瞭解它。經過上面提供的 case 能夠看到,Junit 使用很是簡單,基本 0 門檻上手,經過給測試的方法加一個 @Test 註解,而後將待測試邏輯放在 被 @Test 標註的方法內,而後 run 就行了。簡單源於組件開發者的頂層抽象和封裝,將技術細節屏蔽,而後以最簡潔的 API 或者註解面向用戶,這也是 Junit 可以讓廣大開發者容易接受的根本緣由,值得咱們借鑑學習。socket
迴歸正題,基於上面分析,Junit 使用簡單在於其提供了很是簡潔的 API 和註解,那對於咱們來講,這些就是做爲分析 Junit 的基本着手點;經過這些,來撥開 Junit 的基本原理。基於第一節的小案例,這裏拋出這樣幾個問題:ide
這裏把斷點直接打在目標測試方法位置,而後 debug 執行函數
經過堆棧來找到用例執行的整個路徑。由於本 case 是經過 idea 啓動執行,因此能夠看到的入口實際是被 idea 包裝過的。可是這裏也抓到了 JUnitCore 這樣的一個入口。單元測試
JUnitCore 是運行測試用例的門面入口,經過源碼註釋能夠看到,JUnitCore 從 junit 4 纔有,可是其向下兼容了 3.8.x 版本系列。咱們在跑測試用例時,其實大多數狀況下在本地都是經過 IDE 來觸發用例運行,或者經過 mvn test 來運行用例,實際上,不論是 IDE 仍是 mvn 都是對 JUnitCore 的封裝。咱們徹底能夠經過 main 方法的方式來運行,好比運行下面代碼的 main 方法來經過一個 JUnitCore 實例,而後指定被測試類來觸發用例執行,爲了儘可能使得堆棧更貼近 Junit 本身的代碼,咱們經過這種方式啓動來減小堆棧對於代碼執行路徑的干擾。學習
public class JunitSamplesTest {
@Before
public void before(){
System.out.println(".....this is before test......");
}
@After
public void after(){
System.out.println(".....this is after test......");
}
@BeforeClass
public static void beforeClass(){
System.out.println(".....this is before class test......");
}
@AfterClass
public static void afterClass(){
System.out.println(".....this is after class test......");
}
@Test
public void testOne(){
System.out.println("this is test one");
}
@Test
public void testTwo(){
System.out.println("this is test two");
}
public static void main(String[] args) {
JUnitCore jUnitCore = new JUnitCore();
jUnitCore.run(JunitSamplesTest.class);
}
}
複製代碼
這裏獲得了最簡化的測試執行入口:
若是使用 java 命令來引導啓動,其實就是從 JunitCore 內部本身的 main 方法開始執行的
/** * Run the tests contained in the classes named in the args. If all tests run successfully, exit with a status of 0. Otherwise exit with a status of 1. Write * feedback while tests are running and write stack traces for all failed tests after the tests all complete. * Params: * args – names of classes in which to find tests to run **/
public static void main(String... args) {
Result result = new JUnitCore().runMain(new RealSystem(), args);
System.exit(result.wasSuccessful() ? 0 : 1);
}
複製代碼
這裏比較好理解,被打了 @Test 註解的方法,必定是 Junit 經過某種方式將其掃描到了,而後做爲待執行的一個集合或者隊列中。下面經過分析代碼來論證下。
org.junit.runners.BlockJUnit4ClassRunner#getChildren
@Override
protected List<FrameworkMethod> getChildren() {
return computeTestMethods();
}
複製代碼
經過方法 computeTestMethods 方法名其實就能夠看出其目的,就是計算出全部的測試方法。
etAnnotatedMethods 經過指定的 annotationClass 類型,將當前 TestClass 中類型爲 annotationClass 類型註解標註的方法過濾出來,
getFilteredChildren 中最後將獲取獲得的測試方法放在 filteredChildren 中緩存起來。這裏簡單彙總下 @Test 註解被識別的整個過程(其餘註解如 @Before 都是同樣的)
// clazz 是待測試類
public TestClass(Class<?> clazz) {
this.clazz = clazz;
if (clazz != null && clazz.getConstructors().length > 1) {
// 測試類不能有有參構造函數
throw new IllegalArgumentException(
"Test class can only have one constructor");
}
Map<Class<? extends Annotation>, List<FrameworkMethod>> methodsForAnnotations =
new LinkedHashMap<Class<? extends Annotation>, List<FrameworkMethod>>();
Map<Class<? extends Annotation>, List<FrameworkField>> fieldsForAnnotations =
new LinkedHashMap<Class<? extends Annotation>, List<FrameworkField>>();
// 掃描待測試類中全部的 Junit 註解,包括 @Test @Before @After 等等
scanAnnotatedMembers(methodsForAnnotations, fieldsForAnnotations);
// 過濾出打在方法上的註解,
this.methodsForAnnotations = makeDeeplyUnmodifiable(methodsForAnnotations);
// 過濾出打在變量上的註解
this.fieldsForAnnotations = makeDeeplyUnmodifiable(fieldsForAnnotations);
}
複製代碼
methodsForAnnotations 和 fieldsForAnnotations 緩存了當前待測試類全部被 junit 註解標註過的方法和變量
要搞定這個問題,其實有必要了解下 Junit 中一個比較重要的概念 Statement。
public abstract class Statement {
/** * Run the action, throwing a {@code Throwable} if anything goes wrong. */
public abstract void evaluate() throws Throwable;
}
複製代碼
Statement 從 junit 4.5 版本被提出,Statement 表示在運行 JUnit 測試組件的過程當中要在運行時執行的一個或多個操做,簡單說就是,對於被 @Before @After 註解標註的方法,在 JUnit 會被做爲一種 Statement 存在,分別對應於 RunBefores 和 RunnerAfter,這些 statement 中持有了當前運行全部的 FrameworkMethod。
FrameworkMethod 是 JUnit 中全部被 junit 註解標註方式的內部描述,@Test, @Before, @After, @BeforeClass, @AfterClass 標註的方法最終都做爲 FrameworkMethod 實例存在。
Statement 的建立有兩種方式,基於 FrameworkMethod 的 methodBlock 和基於 RunNotifier 的 classBlock,這裏介紹 methodBlock ,classBlock 下節討論。
protected Statement methodBlock(final FrameworkMethod method) {
Object test;
try {
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
return createTest(method);
}
}.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);
statement = withInterruptIsolation(statement);
return statement;
}
複製代碼
withAfters、withBefores 會將 RunAfters 和 RunBefore 綁定到 statement,最後 造成一個 statement 鏈,這個鏈的執行入口時 RunAfters#evaluate。
@Override
public void evaluate() throws Throwable {
List<Throwable> errors = new ArrayList<Throwable>();
try {
next.evaluate();
} catch (Throwable e) {
errors.add(e);
} finally {
// 在 finally 中執行 after 方法
for (FrameworkMethod each : afters) {
try {
invokeMethod(each);
} catch (Throwable e) {
errors.add(e);
}
}
}
MultipleFailureException.assertEmpty(errors);
}
複製代碼
next 鏈中包括 before 和待執行的測試方法
因此咱們看到的就是 before -> testMethod -> after。
這裏其實和預想的不太同樣,關於 before 和 after 這種邏輯,第一想法是經過代理的方式,對測試方法進行代理攔截,相似 Spring AOP 中的 Before 和 After,其實否則。
前面分析了 methodBlock,瞭解到 junit 中經過這個方法建立 statement 而且將 before 和 after 的方法綁定給 statement,以此推斷,classBlock 的做用就是將 BeforeClass 和 AfterClass 綁定給statement 。
protected Statement classBlock(final RunNotifier notifier) {
// childrenInvoker 這裏會調用到 methodBlock
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
statement = withClassRules(statement);
statement = withInterruptIsolation(statement);
}
return statement;
}
複製代碼
BeforeClass 和 before 都會對應建立一個 RunnerBefores,區別在於 BeforeClass 在建立 RunnerBefores 時,不會指定目標測試方法。
junit 全部執行的結果都存放在 Result 中
// 全部 case 數
private final AtomicInteger count;
// 忽略執行的 case 數(被打了 ignore)
private final AtomicInteger ignoreCount;
// 失敗 case 數
private final AtomicInteger assumptionFailureCount;
// 全部失敗 case 的結果
private final CopyOnWriteArrayList<Failure> failures;
// 執行時間
private final AtomicLong runTime;
// 開始時間
private final AtomicLong startTime;
複製代碼
Result 中內置了一個默認的來監聽器,這個監聽器會在每一個 case 執行完成以後進行相應的回調,Listener 以下:
@RunListener.ThreadSafe
private class Listener extends RunListener {
// 設置開始時間
@Override
public void testRunStarted(Description description) throws Exception {
startTime.set(System.currentTimeMillis());
}
// 執行完全部 case
@Override
public void testRunFinished(Result result) throws Exception {
long endTime = System.currentTimeMillis();
runTime.addAndGet(endTime - startTime.get());
}
// 執行完某個 case
@Override
public void testFinished(Description description) throws Exception {
count.getAndIncrement();
}
// 執行完某個 case 失敗
@Override
public void testFailure(Failure failure) throws Exception {
failures.add(failure);
}
// 執行完某個ignore case
@Override
public void testIgnored(Description description) throws Exception {
ignoreCount.getAndIncrement();
}
@Override
public void testAssumptionFailure(Failure failure) {
// Assumption 產生的失敗
assumptionFailureCount.getAndIncrement();
}
}
複製代碼
JUnit 4 開始在測試中支持假設 Assumptions,在 Assumptions 中,封裝了一組使用的方法,以支持基於假設的條件測試執行。假設實際就是指定某個特定條件,假如不能知足假設條件,假設不會致使測試失敗,只是終止當前測試。這也是假設與斷言的最大區別,由於對於斷言而言,會致使測試失敗。
因此 JUnit 經過監聽器機制收集全部的測試信息,最終封裝到 Result 中返回。
Junit 中有一些比較基本的概念,好比 Runner,statement 等;在初始化時,默認狀況下 junit 會構建出 BlockJUnit4ClassRunner 這樣的一個 Runner,而且在這個 Runner 中會持有被測試類的全部信息。Runner 運行測試並在執行此操做時將重要事件通知 RunNotifier。
也可使用 RunWith 調用自定義 Runner,這裏只要你的 Runner 是 org.junit.runner.Runner 子類便可;建立自定義運行程序時,除了在此處實現抽象方法外,還必須提供一個構造函數,這個構造函數將包含測試的類做爲參數--如:SpringRunner。
Runner 的 run 方法內部就是構建和執行 Statement 鏈的過程,Statement 中描述了單元測試中須要執行的一系列操做,每一個 case 均以 RunnerAfter -> TargetMethod -> RunnerBefore 的執行順序依次執行;執行過程當中,junit 經過監聽器機制回調 case 調用的每一個生命週期階段,並將各個case 執行的信息進行收集彙總,最終返回執行結果 Result 。