1. 程式人生 > 其它 >單元測試框架之Junit使用及原理分析

單元測試框架之Junit使用及原理分析

前言

單元測試用來保證我們的程式碼能夠正常執行,輸入一組資料,能夠得到期望的結果,一般以方法作為最小單元。

簡單使用

新增依賴

<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();
        }
    }

總結

  1. 通過RunnerBuilder(執行器構造器)建立一個Runner(執行器),最終的Runner為Suite型別,內部包含多個實際用來工作的Runner,
    預設為BlockJUnit4ClassRunner型別,我們可以通過@RunWith註解自定義Runner實現。
  2. BlockJUnit4ClassRunner解析測試類建立TestClass物件,並解析出其中包含@BeforeClass,@AfterClass,@Before,@After,@Test等註解的方法。
  3. BlockJUnit4ClassRunner物件建立一個Statement物件,使用RunBefores和RunAfters裝飾此物件並執行(就是執行所有的測試方法)。
  4. 執行每一個測試方法,建立一個Statement物件,使用ExpectException,FailOnTimeout,RunBefores,RunAfters等類裝飾此物件並執行。

參考

Java JUnit 單元測試小結
Junit原始碼閱讀筆記一(從JunitCore開始)
Junit原始碼閱讀筆記二(Runner構建)
Junit原始碼閱讀筆記三(TestClass)
JUnit 5 教程 之 基礎篇