先介紹下這篇博文的由來,以前已經對JUnit的使用經行了深刻的介紹和演示(參考JUnit學習(一),JUnit學習(二)),其中的部分功能是經過分析JUnit源代碼找到的。得益於這個過程有幸完整的拜讀了JUnit的源碼十分讚歎做者代碼的精美,一直計劃着把源碼的分析也寫出來。突發奇想決定從設計模式入手賞析JUnit的流程和模式的應用,但願由此能寫出一篇耐讀好看的文章。因而又花了些時日重讀《設計模式》以期可以順暢的把二者結合在一塊兒,因爲我的水平有限不免出現錯誤、疏漏,還請各位高手多多指出、討論。 java
首先,介紹下JUnit的測試用例運行會通過哪些過程,這裏提及來有些抽象會讓人比較迷惑,在看了後面章節的內容以後就比較清晰了: 算法
首先先介紹下JUnit中的模型類(Model),在JUnit模型類能夠劃分爲三個範圍: 設計模式
言歸正傳,下面討論設計模式和JUnit的源碼:
工廠方法模式、職責鏈:用例啓動,Client在建立Request後會調用RunnerBuilder(工廠方法的抽象類)來建立Runner,默認的實現是AllDefaultPosibilitiesBuilder,根據不一樣的測試類定義(@RunWith的信息)返回Runner。AllDefaultPosibilitiesBuilder使用職責鏈模式來建立Runner,部分代碼以下。代碼A是AllDefaultPosibilitiesBuilder的主要構造邏輯構造了一個【IgnoreBuilder->AnnotatedBuilder->SuitMethodBuilder->JUnit3Builder->JUnit4Builder】的職責鏈,構造Runner的過程當中有且只有一個handler會響應請求。代碼B是Junit4Builder類實現會返回一個BlockJUnit4ClassRunner對象,這個是JUnit4的默認Runner。 app
public Runner runnerForClass(Class<?> testClass) throws Throwable { List<RunnerBuilder> builders = Arrays.asList( ignoredBuilder(), annotatedBuilder(), suiteMethodBuilder(), junit3Builder(), junit4Builder()); for (RunnerBuilder each : builders) { Runner runner = each.safeRunnerForClass(testClass); if (runner != null) { return runner; } } return null; }
public class JUnit4Builder extends RunnerBuilder { @Override public Runner runnerForClass(Class<?> testClass) throws Throwable { return new BlockJUnit4ClassRunner(testClass); } }
代碼B JUnit4Builder實現 框架
組合模式:將具有樹形結構的數據抽象出公共的接口,在遍歷的過程當中應用一樣的處理方式。這個模式在Runner中的應用不是很明顯,扣進來略有牽強。Runner是分層次的,父層包括@BeforeClass、@AfterClass、@ClassRule註解修飾的方法或變量,它們在測試執行前或執行後執行一次。兒子層是@Before、@After、@Rule修飾的方法或變量它們在每一個測試方法執行先後執行。當編寫的用例使用Suit來運行時則是三層結構,上面的父子結構中間插入了一層childrenRunners,也就是一個Suilt中每一個測試類都會生成一個Runner,調用順序變成了Runner.run()>childRunner.run()<即遍歷childrenRunners>->testMethod()。ParentRunner中將變化的部分封裝爲runChild()方法交給子類實現,達到了遍歷過程使用一樣處理方式的目的——ParentRunner.this.runChild(each,notifier)。 eclipse
public void run(final RunNotifier notifier) { EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription()); try { Statement statement = classBlock(notifier); statement.evaluate(); } catch (AssumptionViolatedException e) { testNotifier.fireTestIgnored(); } catch (StoppedByUserException e) { throw e; } catch (Throwable e) { testNotifier.addFailure(e); } } private void runChildren(final RunNotifier notifier) { for (final T each : getFilteredChildren()) { fScheduler.schedule(new Runnable() { public void run() { ParentRunner.this.runChild(each, notifier); } }); } fScheduler.finished(); }
代碼C ParentRunner的組合模式應用 maven
模板方法模式:模板方法的目的是抽取公共部分封裝變化,在父類中會包含公共流程的代碼,將變化的部分封裝爲抽象方法由子類實現(就像模板同樣框架式定好的,你去填寫你須要的內容就好了)。JUnit的默認Runner——BlockJUnit4ClassRunner繼承自ParentRunner,ParentRunner類定義了Statement的構造和執行流程,而如何執行兒子層的runChild方法時交給子類實現的,在BlockJUnit4ClassRunner中就是去構造和運行TestMethod,而另外一個子類Suit中則是執行子層次的runner.run。 ide
觀察者模式:Runner在執行TestCase過程當中的各個階段都會通知RunNotifier,其中RunNotifier負責listener的管理者角色,支持添加和刪除監聽者,提供了監聽JUnit運行的方法:如用例開始、完成、失敗、成功、忽略等。代碼D截取自RunNotifier。SafeNotifier是RunNotifier的內部類,抽取了公共邏輯——遍歷註冊的listener,調用notifyListener方法。fireTestRunStarted()方法是RunNotifier衆多fireXXX()方法的一個,它在方法裏構造SafeNotifier的匿名類實現notifyListener()方法。private abstract class SafeNotifier { private final List<RunListener> fCurrentListeners; SafeNotifier() { this(fListeners); } SafeNotifier(List<RunListener> currentListeners) { fCurrentListeners = currentListeners; } void run() { synchronized (fListeners) { List<RunListener> safeListeners = new ArrayList<RunListener>(); List<Failure> failures = new ArrayList<Failure>(); for (Iterator<RunListener> all = fCurrentListeners.iterator(); all .hasNext(); ) { try { RunListener listener = all.next(); notifyListener(listener); safeListeners.add(listener); } catch (Exception e) { failures.add(new Failure(Description.TEST_MECHANISM, e)); } } fireTestFailures(safeListeners, failures); } } abstract protected void notifyListener(RunListener each) throws Exception; } public void fireTestRunStarted(final Description description) { new SafeNotifier() { @Override protected void notifyListener(RunListener each) throws Exception { each.testRunStarted(description); } ; }.run(); }
裝飾模式:保持對象原有的接口不改變而透明的增長對象的行爲,看起來像是在原有對象外面包裝了一層(或多層)行爲——雖然對象仍是原來的類型可是行爲逐漸豐富起來。 以前一直在強調Statement描述了測試類的執行細節,究竟是如何描述的呢?代碼E展現了Statement的構築過程,首先是調用childrenInvoker方法構建了Statement的基本行爲——執行全部的子測試runChildren(notifier)(非Suit狀況下就是TestMethod了,若是是Suit的話則是childrenRunners)。
接着是裝飾模式的應用,代碼F是withBeforeClasses()的實現——很簡單,檢查是否使用了@BeforeClasses註解修飾若是存在構造RunBefores對象——RunBefore繼承自Statement。代碼H中的evaluate()方法能夠發現新生成的Statement在執行runChildren(fNext.evaluate())以前遍歷全部使用@BeforeClasses註解修飾的方法並執行。產生的效果即便用@BeforeClasses修飾的方法會在全部用例運行前執行且只執行一次。後面的withAfterClasses、withClassRules方法原理同樣都使用了裝飾模式,再也不贅述。 函數
protected Statement classBlock(final RunNotifier notifier) { Statement statement = childrenInvoker(notifier); statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); return statement; } protected Statement childrenInvoker(final RunNotifier notifier) { return new Statement() { @Override public void evaluate() { runChildren(notifier); } }; }
protected Statement withBeforeClasses(Statement statement) { List<FrameworkMethod> befores = fTestClass .getAnnotatedMethods(BeforeClass.class); return befores.isEmpty() ? statement : new RunBefores(statement, befores, null); }
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(); } }
策略模式:針對相同的行爲在不一樣場景下算法不一樣的狀況,抽象出接口類,在子類中實現不一樣的算法並提供算法執行必須Context信息。JUnit中提供了Timeout、ExpectedException、ExternalResource等一系列的TestRule用於豐富測試用例的行爲,這些TestRule的都是經過修飾Statement實現的。
修飾Statement的代碼在withRules()方法中實現,使用了策略模式。代碼I描述了JUnit是如何處理@Rule標籤的,withRules方法獲取到測試類中全部的@Rule修飾的變量,分別調用withMethodRules和withTestRules方法,前者是爲了兼容JUnit3版本的Rule這裏忽略,後者withTestRules的邏輯很簡單首先查看是否使用了@Rule,如存在就交給RunRules類處理。代碼J是RunRules的實現,在構造函數中處理了修飾Statement的邏輯(applyAll方法)——抽象接口是TestRule,根據不一樣的場景(即便用@Rule修飾的不一樣的TestRule的實現)選擇不一樣的策略(TestRule的具體實現),而Context信息就是入參(result:Statement, description:Description)。 學習
private Statement withRules(FrameworkMethod method, Object target, Statement statement) { List<TestRule> testRules = getTestRules(target); Statement result = statement; result = withMethodRules(method, testRules, target, result); result = withTestRules(method, testRules, result); return result; } private Statement withTestRules(FrameworkMethod method, List<TestRule> testRules, Statement statement) { return testRules.isEmpty() ? statement : new RunRules(statement, testRules, describeChild(method)); }
public class RunRules extends Statement { private final Statement statement; public RunRules(Statement base, Iterable<TestRule> rules, Description description) { statement = applyAll(base, rules, description); } @Override public void evaluate() throws Throwable { statement.evaluate(); } private static Statement applyAll(Statement result, Iterable<TestRule> rules, Description description) { for (TestRule each : rules) { result = each.apply(result, description); } return result; } }
public class Timeout implements TestRule { private final int fMillis; /** * @param millis the millisecond timeout */ public Timeout(int millis) { fMillis = millis; } public Statement apply(Statement base, Description description) { return new FailOnTimeout(base, fMillis); } }
public class TestClass { private final Class<?> fClass; private Map<Class<?>, List<FrameworkMethod>> fMethodsForAnnotations = new HashMap<Class<?>, List<FrameworkMethod>>(); private Map<Class<?>, List<FrameworkField>> fFieldsForAnnotations = new HashMap<Class<?>, List<FrameworkField>>(); /** * Creates a {@code TestClass} wrapping {@code klass}. Each time this * constructor executes, the class is scanned for annotations, which can be * an expensive process (we hope in future JDK's it will not be.) Therefore, * try to share instances of {@code TestClass} where possible. */ public TestClass(Class<?> klass) { fClass = klass; if (klass != null && klass.getConstructors().length > 1) { throw new IllegalArgumentException( "Test class can only have one constructor"); } for (Class<?> eachClass : getSuperClasses(fClass)) { for (Method eachMethod : MethodSorter.getDeclaredMethods(eachClass)) { addToAnnotationLists(new FrameworkMethod(eachMethod), fMethodsForAnnotations); } for (Field eachField : eachClass.getDeclaredFields()) { addToAnnotationLists(new FrameworkField(eachField), fFieldsForAnnotations); } } } …… }
讀JUnit代碼時確實很是讚歎其合理的封裝和靈活的設計,本身雖然也寫了幾年代碼可是在JUnit的源碼中收穫不少。因爲對源碼的鑽研深度以及設計模式的領會不夠深刻,文中有不少牽強和錯誤的地方歡迎你們討論指正。最喜歡的是JUnit對裝飾模式和職責鏈的應用,在看到AllDefaultPossiblitiesBuilder中對職責鏈的應用還以爲設計比較合理,等到看到Statement的建立和組裝就感慨設計的精湛了,不管是基本的調用測試方法的邏輯仍是@Before、@After等以及實現自TestRule的邏輯一併融入到Statement的構造中,又不會牽扯出太多的耦合。總之不管是設計模式仍是設計思想,歸根結底就是抽取公共部分,封裝變化,作到靈活、解耦。最後說明這篇文章根據的源代碼是JUnit4.11的,maven座標以下,在JUnit的其它版本中源碼差異比較大沒有研究過。
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency>