單元測試框架之Junit使用及原理分析
阿新 • • 發佈:2022-06-05
前言
單元測試用來保證我們的程式碼能夠正常執行,輸入一組資料,能夠得到期望的結果,一般以方法作為最小單元。
簡單使用
新增依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
簡單例子
import org.hamcrest.Matchers; import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; public class TestJunit { @BeforeClass public static void beforeClass() { System.out.println("beforeClass"); } @Before public void before() { System.out.println("before"); } @Test public void testBase() { //如果實際值和期望值不一致,丟擲AssertionError錯誤 Assert.assertEquals(11, 10); Assert.assertThat(11, Matchers.equalTo(11)); } @Test(expected = RuntimeException.class) public void testException() { throw new RuntimeException(); } @Test(timeout = 3000) //單位毫秒 public void testTimeout() { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } @After public void after() { System.out.println("after"); } @AfterClass public static void afterClass() { System.out.println("afterClass"); } }
- @Test: 標記為一個測試用例,expected引數表示方法必須丟擲指定的異常,timeout引數表示方法必須在指定時間內執行完
- @BeforeClass: 必須為靜態方法,在所有測試用例之前執行
- @AfterClass: 必須為靜態方法,在所有測試用例之後執行
- @Before: 在每一個測試用例之前執行
- @After: 在每一個測試用例之後執行
import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; public class TestJunitSource { public static void main(String[] args) { //執行測試類,得到一個結果,包含執行錯誤的用例資訊 Result result = JUnitCore.runClasses(TestJunit.class); for (Failure failure : result.getFailures()) { System.out.println(failure.toString()); } //所有測試用例都是成功的 if (result.wasSuccessful()) { System.out.println("Both Tests finished successfully..."); } } }
輸出結果為
beforeClass
before
after
before
after
before
after
afterClass
testBase(com.imooc.sourcecode.java.test.junit.TestJunit): expected:<11> but was:<10>
符合每種註解的作用
原理分析
首先進入JUnitCore的runClasses()方法
public static Result runClasses(Class<?>... classes) { //defaultComputer()方法返回一個Computer物件,不重要 return runClasses(defaultComputer(), classes); }
繼續跟進去
public static Result runClasses(Computer computer, Class<?>... classes) {
//建立JUnitCore物件並執行
return new JUnitCore().run(computer, classes);
}
public Result run(Computer computer, Class<?>... classes) {
return run(Request.classes(computer, classes));
}
核心分為兩部分,根據測試類建立Runner物件,通過Runner物件執行測試方法。
建立Runner物件
public static Request classes(Computer computer, Class<?>... classes) {
try {
//這是一個RunnerBuilder(執行器構造器),用來建立Runner(執行器)
AllDefaultPossibilitiesBuilder builder = new AllDefaultPossibilitiesBuilder(true);
//最終執行AllDefaultPossibilitiesBuilderel的runnerForClass()方法來獲取Runner物件,並封裝成Suite
Runner suite = computer.getSuite(builder, classes);
//將Runner包裝成一個Request物件,核心還是Runner
return runner(suite);
}
}
進入AllDefaultPossibilitiesBuilderel的runnerForClass()方法
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
//內部通過其他RunnerBuilder來建立Runner
List<RunnerBuilder> builders = Arrays.asList(
ignoredBuilder(), //包含@Ignore註解的情況
annotatedBuilder(), //包含@RunWith註解的情況
suiteMethodBuilder(),//包含suite()方法
junit3Builder(),//繼承TestCase類
junit4Builder());//預設BlockJUnit4ClassRunner
for (RunnerBuilder each : builders) {
//如果滿足以上的任意一種情況,直接返回
Runner runner = each.safeRunnerForClass(testClass);
if (runner != null) {
return runner;
}
}
return null;
}
通過@RunWith註解,我們可以擴充套件使用其他Runner實現類,如SpringRunner,配合Spring使用,可以幫我們建立IOC容器並注入需要的依賴。
我們的例子中以上情況都不滿足,所以最終使用BlockJUnit4ClassRunner。
通過Runner物件執行測試方法
public Result run(Computer computer, Class<?>... classes) {
//執行建立的Request物件,其中包含一個Runner物件
return run(Request.classes(computer, classes));
}
跟進去
public Result run(Request request) {
//還是通過Runner物件,這裡就是Suite型別,其中包含真正工作的執行器BlockJUnit4ClassRunner物件
return run(request.getRunner());
}
執行Runner物件
public Result run(Runner runner) {
Result result = new Result();
//建立一個執行監聽器
RunListener listener = result.createListener();
notifier.addFirstListener(listener);
try {
//執行開始,這裡就是記錄開始時間
notifier.fireTestRunStarted(runner.getDescription());
//真正開始執行的地方
runner.run(notifier);
//執行結束,這裡就是記錄結束時間
notifier.fireTestRunFinished(result);
} finally {
removeListener(listener);
}
return result;
}
進入Suite父類ParentRunner的run()方法
@Override
public void run(final RunNotifier notifier) {
EachTestNotifier testNotifier = new EachTestNotifier(notifier,
getDescription());
try {
//建立Statement物件
Statement statement = classBlock(notifier);
//執行Statement
statement.evaluate();
} catch (AssumptionViolatedException e) {
testNotifier.addFailedAssumption(e);
} catch (StoppedByUserException e) {
throw e;
} catch (Throwable e) {
testNotifier.addFailure(e);
}
}
看一下是如何建立Statement物件的,當前的Runner型別為Suite,其中的測試類為空,真正的測試類在BlockJUnit4ClassRunner中
protected Statement classBlock(final RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!areAllChildrenIgnored()) {
//如果測試類包含@BeforeClass的方法,使用裝飾者模式建立一個裝飾器(RunBefores型別),在測試方法之前執行
statement = withBeforeClasses(statement);
//如果測試類包含@AfterClass的方法,使用裝飾者模式建立一個裝飾器(RunAfters型別),在測試方法之後執行
statement = withAfterClasses(statement);
//處理包含@ClassRule的方法,使用RunRules包裝,暫時不管
statement = withClassRules(statement);
}
return statement;
}
//建立Statement物件
protected Statement childrenInvoker(final RunNotifier notifier) {
return new Statement() {
@Override
public void evaluate() {
//執行Statement的核心
runChildren(notifier);
}
};
}
private void runChildren(final RunNotifier notifier) {
final RunnerScheduler currentScheduler = scheduler;
try {
//Suite物件的children只有BlockJUnit4ClassRunner物件
for (final T each : getFilteredChildren()) {
currentScheduler.schedule(new Runnable() {
public void run() {
//又跳轉到了runChild()方法,這裡的each就是BlockJUnit4ClassRunner物件
ParentRunner.this.runChild(each, notifier);
}
});
}
} finally {
currentScheduler.finished();
}
}
Suite的runChild()方法又會呼叫BlockJUnit4ClassRunner的run()方法,最終會執行BlockJUnit4ClassRunner的runChild()方法
@Override
protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
//獲取方法的描述資訊,不重要
Description description = describeChild(method);
//方法上是否包含@Ignore註解,表示忽略此方法
if (isIgnored(method)) {
notifier.fireTestIgnored(description);
} else {
runLeaf(methodBlock(method), description, notifier);
}
}
//建立一個Statement物件,並進行各種裝飾
protected Statement methodBlock(FrameworkMethod method) {
Object test;
try {
//建立測試類例項,這裡就是TestJunit物件
test = new ReflectiveCallable() {
@Override
protected Object runReflectiveCall() throws Throwable {
//通過反射建立
return createTest();
}
}.run();
} catch (Throwable e) {
return new Fail(e);
}
//建立一個Statement物件,實際型別為InvokeMethod,通過反射執行測試方法
Statement statement = methodInvoker(method, test);
//處理@Test註解的expected引數,通過ExpectException類包裝
statement = possiblyExpectingExceptions(method, test, statement);
//處理@Test註解的timeout引數,通過FailOnTimeout類包裝
statement = withPotentialTimeout(method, test, statement);
//處理@Before註解,通過RunBefores類包裝
statement = withBefores(method, test, statement);
//處理@After註解,通過RunAfters類包裝
statement = withAfters(method, test, statement);
//處理@Rule註解,通過RunRules類包裝,暫時不管
statement = withRules(method, test, statement);
return statement;
}
//真正執行Statement物件
protected final void runLeaf(Statement statement, Description description,
RunNotifier notifier) {
EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
eachNotifier.fireTestStarted();
try {
statement.evaluate();
} catch (AssumptionViolatedException e) {
eachNotifier.addFailedAssumption(e);
} catch (Throwable e) {
//如果我們的測試方法丟擲未被捕獲的異常,就當做一次失敗,主要是程式碼中丟擲的AssertionError錯誤
eachNotifier.addFailure(e);
} finally {
eachNotifier.fireTestFinished();
}
}
總結
- 通過RunnerBuilder(執行器構造器)建立一個Runner(執行器),最終的Runner為Suite型別,內部包含多個實際用來工作的Runner,
預設為BlockJUnit4ClassRunner型別,我們可以通過@RunWith註解自定義Runner實現。 - BlockJUnit4ClassRunner解析測試類建立TestClass物件,並解析出其中包含@BeforeClass,@AfterClass,@Before,@After,@Test等註解的方法。
- BlockJUnit4ClassRunner物件建立一個Statement物件,使用RunBefores和RunAfters裝飾此物件並執行(就是執行所有的測試方法)。
- 執行每一個測試方法,建立一個Statement物件,使用ExpectException,FailOnTimeout,RunBefores,RunAfters等類裝飾此物件並執行。
參考
Java JUnit 單元測試小結
Junit原始碼閱讀筆記一(從JunitCore開始)
Junit原始碼閱讀筆記二(Runner構建)
Junit原始碼閱讀筆記三(TestClass)
JUnit 5 教程 之 基礎篇