先介紹下這篇博文的由來,之前已經(jīng)對JUnit的使用經(jīng)行了深入的介紹和演示(參考JUnit學(xué)習(xí)(一),JUnit學(xué)習(xí)(二)),其中的部分功能是通過分析JUnit源代碼找到的。得益于這個過程有幸完整的拜讀了JUnit的源碼十分贊嘆作者代碼的精美,一直計劃著把源碼的分析也寫出來。突發(fā)奇想決定從設(shè)計模式入手賞析JUnit的流程和模式的應(yīng)用,希望由此能寫出一篇耐讀好看的文章。于是又花了些時日重讀《設(shè)計模式》以期能夠順暢的把兩者結(jié)合在一起,由于個人水平有限難免出現(xiàn)錯誤、疏漏,還請各位高手多多指出、討論。
圖一 TestCase運行時序圖
首先,介紹下JUnit的測試用例運行會經(jīng)過哪些過程,這里說起來有些抽象會讓人比較迷惑,在看了后面章節(jié)的內(nèi)容之后就比較清晰了:
Client(JUnitCore、Eclipse):這里是TestCase開始的地方,如果是Main函數(shù)來啟動測試用例的話一般會調(diào)用JUnitCore的方法,如果是在Eclipse中使用JUnit插件則實際上調(diào)用的是org.eclipse.jdt.internal.junit4.runner.JUnit4TestClassReference這個類。 Request(org.junit.runner.Request):Client會根據(jù)測試類或方法而創(chuàng)建一個Request實體,Request包含了要被執(zhí)行的測試類、測試方法等信息。 RunnerBuilder:在創(chuàng)建后Client調(diào)用Request.getRunner()方法獲取用于執(zhí)行測試的Runner,該過程是由RunnerBuilder這個工廠類完成的。 RunNotifier:在執(zhí)行Runner.run方法時Client還會傳遞一個RunNotifier對象,是事件的監(jiān)聽器的管理員。Runner在開始執(zhí)行、成功、失敗和執(zhí)行結(jié)束時會調(diào)用RunNotifier中相應(yīng)的方法從而發(fā)送事件給注冊了的監(jiān)聽器,JUnit運行的最終結(jié)果就是這些監(jiān)聽器收集展現(xiàn)的。 Runner:從名字可以猜出這里應(yīng)該是測試用例運行的地方了,在Client調(diào)用了Runner.run()方法之后,Runner會先構(gòu)造Statement對象將所有要執(zhí)行的邏輯委托給它,接著執(zhí)行Statement.evaluate()方法。在這期間Runner會觸發(fā)RunNotifier(如測試開始、測試結(jié)束等等)。 Statement:測試用例的運行時描述,關(guān)注于測試用例如何運行和調(diào)用測試代碼。比如在執(zhí)行測試用例前是否有@Before注釋的方法需要調(diào)用等信息都會被Runner構(gòu)造為Statement。
圖二 JUnit的類結(jié)構(gòu)
首先先介紹下JUnit中的模型類(Model),在JUnit模型類可以劃分為三個范圍:
描述模型:是對要執(zhí)行的測試用例的描述(比如要執(zhí)行哪個類的哪個方法,是否有指定Runner,使用了哪些注解等),這一層類似于流程文件之于流程引擎——不是用來執(zhí)行的,而是描述要有哪些環(huán)節(jié)、細(xì)節(jié)。個人認(rèn)為這一模型包括測試類本身(即你自己編寫的測試用例)和Request。其中測試類本身等同于描述文件,Request則記錄了是要運行的Suit、測試類或者是某個具體的方法、過濾器、排序的Comparator等信息(JUnit是支持對測試方法排序和過濾的)。運行時模型:是JUnit中可執(zhí)行的模型,包括FrameworkMember(org.junit.runners.model.FrameworkMember)及其子類、TestClass(org.junit.runners.model.TestClass)、Statement。FrameworkMember的子類包括FrameworkMethod和FrameworkField分別描述了測試類的方法和變量信息,比如是否為靜態(tài)、作用域、包含哪些注解等JUnit運行時需要用到的信息;TestClass的作用有些類似FrameworkMember,是針對測試的Class的描述。Statement在上面已經(jīng)介紹過是對測試執(zhí)行流程和細(xì)節(jié)的描述。結(jié)果模型:JUnit中用于描述用例的類,包括Description(org.junit.runner.Description)、Result(org.junit.runner.Result)、Failure(org.junit.runner.notification.Failure)。Description是對測試用例的描述(測試名稱、所在Class的名字、是否是suit等等)只為RunNotifier提供服務(wù)。Result是運行結(jié)果的描述,用例執(zhí)行完成后RunNotifier的 fireTestRunFinished(final Result result)方法會被觸發(fā),傳入的Result實例描述了運行耗時、忽略的用例次數(shù)、是否成功等信息。Failure則是用例失敗后Runner傳遞給RunNotifier的對象用于描述錯誤信息,特別包含了錯誤的StackTrace。 言歸正傳,下面討論設(shè)計模式和JUnit的源碼:
工廠方法模式、職責(zé)鏈:用例啟動,Client在創(chuàng)建Request后會調(diào)用RunnerBuilder(工廠方法的抽象類)來創(chuàng)建Runner,默認(rèn)的實現(xiàn)是AllDefaultPosibilitiesBuilder,根據(jù)不同的測試類定義(@RunWith的信息)返回Runner。AllDefaultPosibilitiesBuilder使用職責(zé)鏈模式來創(chuàng)建Runner,部分代碼如下。代碼A是AllDefaultPosibilitiesBuilder的主要構(gòu)造邏輯構(gòu)造了一個【IgnoreBuilder->AnnotatedBuilder->SuitMethodBuilder->JUnit3Builder->JUnit4Builder】的職責(zé)鏈,構(gòu)造Runner的過程中有且只有一個handler會響應(yīng)請求。代碼B是Junit4Builder類實現(xiàn)會返回一個BlockJUnit4ClassRunner對象,這個是JUnit4的默認(rèn)Runner。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; }代碼A 職責(zé)鏈實現(xiàn)
public class JUnit4Builder extends RunnerBuilder { @Override public Runner runnerForClass(Class<?> testClass) throws Throwable { return new BlockJUnit4ClassRunner(testClass); }}代碼B JUnit4Builder實現(xiàn)
組合模式:將具備樹形結(jié)構(gòu)的數(shù)據(jù)抽象出公共的接口,在遍歷的過程中應(yīng)用同樣的處理方式。這個模式在Runner中的應(yīng)用不是很明顯,扣進(jìn)來略有牽強。Runner是分層次的,父層包括@BeforeClass、@AfterClass、@ClassRule注解修飾的方法或變量,它們在測試執(zhí)行前或執(zhí)行后執(zhí)行一次。兒子層是@Before、@After、@Rule修飾的方法或變量它們在每個測試方法執(zhí)行前后執(zhí)行。當(dāng)編寫的用例使用Suit來運行時則是三層結(jié)構(gòu),上面的父子結(jié)構(gòu)中間插入了一層childrenRunners,也就是一個Suilt中每個測試類都會生成一個Runner,調(diào)用順序變成了Runner.run()>childRunner.run()<即遍歷childrenRunners>->testMethod()。ParentRunner中將變化的部分封裝為runChild()方法交給子類實現(xiàn),達(dá)到了遍歷過程使用同樣處理方式的目的——ParentRunner.this.runChild(each,notifier)。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的組合模式應(yīng)用
模板方法模式:模板方法的目的是抽取公共部分封裝變化,在父類中會包含公共流程的代碼,將變化的部分封裝為抽象方法由子類實現(xiàn)(就像模板一樣框架式定好的,你去填寫你需要的內(nèi)容就行了)。JUnit的默認(rèn)Runner——BlockJUnit4ClassRunner繼承自ParentRunner,ParentRunner類定義了Statement的構(gòu)造和執(zhí)行流程,而如何執(zhí)行兒子層的runChild方法時交給子類實現(xiàn)的,在BlockJUnit4ClassRunner中就是去構(gòu)造和運行TestMethod,而另一個子類Suit中則是執(zhí)行子層次的runner.run。 圖三 RunNotifier的公共方法
代碼D RunNotifier代碼截取
裝飾模式:保持對象原有的接口不改變而透明的增加對象的行為,看起來像是在原有對象外面包裝了一層(或多層)行為——雖然對象還是原來的類型但是行為逐漸豐富起來。 之前一直在強調(diào)Statement描述了測試類的執(zhí)行細(xì)節(jié),到底是如何描述的呢?代碼E展示了Statement的構(gòu)筑過程,首先是調(diào)用childrenInvoker方法構(gòu)建了Statement的基本行為——執(zhí)行所有的子測試runChildren(notifier)(非Suit情況下就是TestMethod了,如果是Suit的話則是childrenRunners)。接著是裝飾模式的應(yīng)用,代碼F是withBeforeClasses()的實現(xiàn)——很簡單,檢查是否使用了@BeforeClasses注解修飾如果存在構(gòu)造RunBefores對象——RunBefore繼承自Statement。代碼H中的evaluate()方法可以發(fā)現(xiàn)新生成的Statement在執(zhí)行runChildren(fNext.evaluate())之前遍歷所有使用@BeforeClasses注解修飾的方法并執(zhí)行。產(chǎn)生的效果即使用@BeforeClasses修飾的方法會在所有用例運行前執(zhí)行且只執(zhí)行一次。后面的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); } }; }代碼E Statement的構(gòu)造
protected Statement withBeforeClasses(Statement statement) { List<FrameworkMethod> befores = fTestClass .getAnnotatedMethods(BeforeClass.class); return befores.isEmpty() ? statement : new RunBefores(statement, befores, null); }代碼F withBeforeClasses實現(xiàn)
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(); }}代碼H RunBefores實現(xiàn)
策略模式:針對相同的行為在不同場景下算法不同的情況,抽象出接口類,在子類中實現(xiàn)不同的算法并提供算法執(zhí)行必須Context信息。JUnit中提供了Timeout、ExpectedException、ExternalResource等一系列的TestRule用于豐富測試用例的行為,這些TestRule的都是通過修飾Statement實現(xiàn)的。修飾Statement的代碼在withRules()方法中實現(xiàn),使用了策略模式。代碼I描述了JUnit是如何處理@Rule標(biāo)簽的,withRules方法獲取到測試類中所有的@Rule修飾的變量,分別調(diào)用withMethodRules和withTestRules方法,前者是為了兼容JUnit3版本的Rule這里忽略,后者withTestRules的邏輯很簡單首先查看是否使用了@Rule,如存在就交給RunRules類處理。代碼J是RunRules的實現(xiàn),在構(gòu)造函數(shù)中處理了修飾Statement的邏輯(applyAll方法)——抽象接口是TestRule,根據(jù)不同的場景(即使用@Rule修飾的不同的TestRule的實現(xiàn))選擇不同的策略(TestRule的具體實現(xiàn)),而Context信息就是入?yún)ⅲ╮esult: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)); }代碼I rule的執(zhí)行
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; }}代碼J RunRules實現(xiàn)
這里簡單舉一個Timeout的代碼。Timeout首先實現(xiàn)了TestRule方法并在apply中定義了自己的算法——構(gòu)造一個FailOnTimeout對象并返回。不再詳細(xì)描述FailOnTimeout的實現(xiàn),主要原理就是起一個線程來跑測試代碼并等待線程指定的時間,如果超時就會拋出異常。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); }}代碼K Timeout實現(xiàn)
外觀模式:封裝統(tǒng)一的對外接口,隱藏內(nèi)部各個小模塊之間的調(diào)用細(xì)節(jié),使得用戶既可以簡單的使用facade來達(dá)到目的,必要時又可以自行操作內(nèi)部對象。這里的舉例可能不是非常明顯。圖四是TestClass的公共方法,代碼L給出了 TestClass(org.junit.runners.model.TestClass)的一小部分代碼,TestClass主要作用是封裝對Class的操作提供了JUnit運行時需要用到的功能并隱藏其中操作的細(xì)節(jié)。在TestClass內(nèi)部將功能委托給了三個對象Class(java.lang.Class)、FrameworkMethod、FrameworkField來實現(xiàn),本身充當(dāng)了外觀(facade)對外提供了getName、getOnlyConstructor、getAnnotations等等接口,在必要的時又可以通過這個外觀獲取到Class、FrameworkMethod等對象。圖四 TestClass的公共方法
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); } } } ……}代碼L TestClass截取
轉(zhuǎn)自:junit源碼與設(shè)計模式欣賞
|
新聞熱點
疑難解答