JUnit 5系列之架構(gòu)體系介紹
現(xiàn)在,我們已經(jīng)知道了 如何配置 JUnit 5 環(huán)境 及 如何寫一些測試,接下來就來看一點(diǎn)封面下的內(nèi)容吧。本篇我們將討論 JUnit 5 的架構(gòu)體系,以及它之成形如此的原因。
概述
本文章是這個(gè) JUnit 5 系列的一部分:
(如果不喜歡看文章,你可以戳這里看我的演講,或者看一下最近的 vJUG 講座,或者我在 DevoxxPL 上的 PPT。
本系列文章都基于 Junit 5發(fā)布的先行版 Milestone 2。它可能會有變化。如果有新的里程碑(milestone)版本發(fā)布,或者試用版正式發(fā)行時(shí),我會再來更新這篇文章。
這里要介紹的多數(shù)知識你都可以在 JUnit 5 用戶指南 中找到(這個(gè)鏈接指向的是先行版 Milestone 2,想看的***版本文檔的話請戳這里),并且指南還有更多的內(nèi)容等待你發(fā)掘。下面的所有代碼都可以在 我的 Github 上找到。
目錄
- JUnit 4
- JUnit 5
* 分離的關(guān)注點(diǎn)
* JUnit 5 的重新組織
* 架構(gòu)及體系
* API 生命周期
- Open test alliance
- 回顧總結(jié)
- 分享&關(guān)注
JUnit 4
除了 Hamcrest,JUnit 4沒有任何外部依賴,其所有的功能都被打包在一個(gè)構(gòu)件(artifact)中。這完全違反了單一職責(zé)原則,它被提供給開發(fā)者、IDE、構(gòu)建工具、其他測試框架、其他擴(kuò)展等使用,不同的使用者,依賴的都是一個(gè)同樣的構(gòu)件。
而在這其中,只有開發(fā)者能——或者說曾經(jīng)能——以最干凈的方法來使用它。他們通常只需要 JUnit 的公共 API,不需要管其他的。非常好。
但生態(tài)圈中的其他成分則不是這樣使用 JUnit:測試框架、擴(kuò)展,特別是 IDE 和構(gòu)建工具的開發(fā)者,他們需要深入到 JUnit 的深處,到它的細(xì)枝末節(jié):非 public 的類、內(nèi)部 API,甚至 private 字段。它們的正常工作極大地依賴于 JUnit 的實(shí)現(xiàn)細(xì)節(jié)。這使得 JUnit 維護(hù)團(tuán)隊(duì)不能輕易地修改框架的這些內(nèi)部實(shí)現(xiàn),因此團(tuán)隊(duì)的開發(fā)進(jìn)度受到了很大的影響。
當(dāng)然,這些工具的開發(fā)者們也并非有意為之。為了實(shí)現(xiàn)那些我們十分喜愛的特性,他們不得不使用內(nèi)部的 API,因?yàn)?JUnit 4 并沒有提供相應(yīng)的 API:一個(gè)強(qiáng)大到足以滿足工具開發(fā)者們需求的 API。
Junit Lambda 團(tuán)隊(duì)開始著手于 JUnit 5 的開發(fā),希望能讓這一切變得明朗起來。
JUnit 5
分離的關(guān)注點(diǎn)
退一步想,我們不難辨識出,這里至少有兩個(gè)不同的關(guān)注點(diǎn)需要分離:
- 一個(gè)支持測試代碼撰寫的 API
- 一個(gè)識別測試、運(yùn)行測試的機(jī)制
再仔細(xì)思考一下第二點(diǎn),我們可能會問,“哪些測試?”這個(gè)當(dāng)然是指 Junit 測試。“我知道,但具體是哪些版本的測試呢?”呃…“還有,具體是指什么類型的測試?”好吧,你讓我給你……“只能跑那些老版本的 @Test 注解的測試么?有沒有其他新的方法來運(yùn)行測試呢?……”行行行,都給我閉嘴!聽我講著。
為了進(jìn)一步將待識別測試的類型 與 實(shí)際運(yùn)行它們 這兩個(gè)關(guān)注點(diǎn)解耦,上面的第二點(diǎn)需要細(xì)分:
- 一個(gè)支持測試代碼撰寫的 API
- 一個(gè)識別測試、運(yùn)行測試的機(jī)制
* 一個(gè)識別、運(yùn)行特定類型(比如,JUnit 5測試的機(jī)制)
* 另一套協(xié)調(diào)上述機(jī)制的機(jī)制
* 上兩者之間的 API
JUnit 5 的重新的組織
識別出這兩個(gè)關(guān)注點(diǎn)以后,“作為平臺的 JUnit ”(用于運(yùn)行我們的測試)和“作為工具的 JUnit ”(用于撰寫我們的測試)這兩個(gè)概念的分離就清晰了。為了完成這個(gè)徹底的分離,JUnit 團(tuán)隊(duì)決定將 JUnit 5 分成三個(gè)子項(xiàng)目:
JUnit Jupiter
包含了我們用于撰寫測試的 API(關(guān)注點(diǎn)1),以及一個(gè)能理解測試代碼的引擎(關(guān)注點(diǎn)2.1)。
JUnit Platform
提供了一套統(tǒng)一的 API 以運(yùn)行測試,及基于 API 之上的一套工具(關(guān)注點(diǎn)2.2和2.3)。
JUnit Vintage
提供了一套引擎,用以在 JUnit 5 中運(yùn)行 JUnit 3 和 JUnit 4 的測試(關(guān)注點(diǎn)2.1)。
架構(gòu)與體系
JUnit 5 的架構(gòu)體系完全是遵循這個(gè)關(guān)注點(diǎn)分離思想的產(chǎn)物:
junit-jupiter-api(1)
開發(fā)者用于撰寫測試的 API,包含了我們在JUnit 5 的基礎(chǔ)知識一節(jié)中所提及的所有注解、斷言等。
junit-platgorm-engine(2.3)
包含了一套所有測試引擎都必須實(shí)現(xiàn)的 API。這樣,不同的測試引擎之間可以通過統(tǒng)一的接口被調(diào)用。引擎可以跑正常的 JUnit 測試,但也可以實(shí)現(xiàn)不同的引擎用以執(zhí)行其他框架寫成的測試,如 TestNG、Spock、Cucumber 等。
junit-jupiter-engine(2.1)
junit-platform-engine API 的一個(gè)實(shí)現(xiàn),專門用于執(zhí)行 JUnit 5 撰寫的測試。
junit-vintage-engine(2.1)
junit-platform-engine API 的一個(gè)實(shí)現(xiàn),專門用于執(zhí)行 JUnit 3 或 JUnit 4 撰寫的測試。過去,JUnit 4 的構(gòu)件 junit-4.12 充當(dāng)了兩個(gè)角色:它既是開發(fā)人員用于實(shí)現(xiàn)測試的 API,又包含了用以執(zhí)行測試的核心組件。這個(gè)引擎,可以認(rèn)為是低版本的 JUnit 3/4 與 JUnit 5 之間的一個(gè)適配器。
junit-platform-launcher(2.2)
這部分使用了一個(gè)服務(wù)加載器 ServiceLoader 來發(fā)現(xiàn)測試引擎,并協(xié)調(diào)不同實(shí)現(xiàn)之間的執(zhí)行。它提供了一個(gè) API 給 IDE 和構(gòu)建工具,使得它們能夠與測試執(zhí)行過程交互,比如,運(yùn)行單個(gè)的測試、搜集測試結(jié)果并展示等。
聽起來怎樣,很酷吧。
這部分架構(gòu)對于我們生態(tài)鏈前端的使用者來說基本是透明的。我們的項(xiàng)目只需要引入一個(gè)用于編寫測試的 API 依賴,其余的組件讓工具去操心即可。
API 生命周期
現(xiàn)在來說說那些大家都在使用的內(nèi)部 API。JUnit 5 團(tuán)隊(duì)希望這個(gè)問題也能得到解決,為此給 JUnit 的 API 設(shè)立了生命周期。這里,我將源碼中給出的部分解釋截取于此。
內(nèi)部 API(internal)
不允許被 JUnit 開發(fā)者之外的任何人使用。這部分 API 可能被移除,并且不會事先通知。
已過時(shí)(Deprecated)
不應(yīng)該再被使用的 API,它們可能在下次小版本發(fā)布時(shí)被移除。
實(shí)驗(yàn)階段(Experimental)
為一些新的、實(shí)驗(yàn)階段的特性所使用的 API,這些新特性可能會或已經(jīng)被公開使用并接受反饋中。
可以使用,但要謹(jǐn)慎。這些 API 未來可能被提升至 維護(hù)中 或 穩(wěn)定 級別,但也可能不帶提前通知就被移除。
維護(hù)中(Maintained)
使用該 API 的特性,至少在該大版本的下一個(gè)小版本發(fā)布時(shí)不會發(fā)生向后不兼容的改變。如果未來有移除維護(hù)中 API 的計(jì)劃,它會先被打回到 已過時(shí) 階段。
穩(wěn)定(Stable)
使用該 API 的特性,至少在下個(gè)大版本發(fā)布之前不會發(fā)生向后不兼容的改變。
JUnit 對外公開的類都帶有一個(gè) @API(usage) 注解,其中 usage 是上面幾個(gè)值中的其中一個(gè)。團(tuán)隊(duì)希望這能給 API 的調(diào)用方以充足的信息,即他們所使用的 API 處于什么生命周期中,同時(shí),也希望給每個(gè)團(tuán)隊(duì)以自由,讓他們決定是否改變或移除過時(shí) API 。
Open Test Alliance
其實(shí)還有一件事。Junit 5 的體系結(jié)構(gòu)使得 IDE 和構(gòu)建工具能夠?qū)⑵渥鳛橹虚g層,以運(yùn)行所有類型的測試框架(前提是該框架實(shí)現(xiàn)了其對應(yīng)的引擎)。這樣的話,工具本身就不需要去實(shí)現(xiàn)框架相關(guān)的測試支持,它們只需要使用一套統(tǒng)一的借口,即可實(shí)現(xiàn)測試發(fā)現(xiàn)、測試執(zhí)行和結(jié)果收集。
是嘛,真的可以嗎?
失敗的測試,通常使用異常來描述。但不同的測試框架和斷言庫之間并無一個(gè)統(tǒng)一的接口。相反,它們通常實(shí)現(xiàn)了各自不同的版本(常見的是繼承 AssertionError 或 RuntimeException )。這就使得不同框架間的互操作變得更加復(fù)雜,也使得工具之間無法簡單使用一套統(tǒng)一的接口。
為了解決這個(gè)問題,Junit Lambda 團(tuán)隊(duì)又分出來一個(gè)獨(dú)立的項(xiàng)目,The Open Test Alliance for the JVM。這是它們的提議:
基于 JUnit Lambda 團(tuán)隊(duì)近來與來自Eclipse、Gradle 及 Intellij 等 IDE 和構(gòu)建工具開發(fā)者所展開的討論,我們呼吁要建立這樣一個(gè)開源項(xiàng)目:它用于提供一套基于 JVM的 測試庫與測試框架 間的最小公共接口集。
項(xiàng)目主要目標(biāo)是,為各測試框架(如 JUnit、TestNG、Spock 等)和三方斷言庫(Hamcrest、Assert 等)提供一個(gè)公共的異常集合。有了這個(gè)集合,IDE 和構(gòu)建工具就可以一個(gè)統(tǒng)一的接口對所有測試過程——如對失敗斷言、失敗假言判定的處理、對測試執(zhí)行過程的可視化、在 IDE 中生成測試結(jié)果報(bào)告等——進(jìn)行處理。
截止目前,該項(xiàng)目的呼吁似乎并未引起太多重視,或說是基本未得到重視。如果你覺得這是個(gè)好的想法,你可以通過一些方式來支持,比如向你經(jīng)常使用的測試框架維護(hù)者發(fā)出聲音。
回顧總結(jié)
本篇我們介紹了 JUnit 5 的架構(gòu)設(shè)計(jì),它將原有的 API 分成了兩部分:編寫測試部分的 API 和 執(zhí)行測試的引擎。這個(gè)引擎進(jìn)一步地被切分成三個(gè)部分:一個(gè)解析測試代碼的 API、一個(gè)測試執(zhí)行器(launcher),和一些支持不同測試框架的引擎實(shí)現(xiàn)。這樣開發(fā)者只需要為項(xiàng)目引入 API 部分的依賴(用于編寫測試),而測試框架的開發(fā)者們則只需要實(shí)現(xiàn)引擎部分的 API(其他工作已經(jīng)由 JUnit 處理了),構(gòu)建工具方面也只需要實(shí)現(xiàn) launcher API以協(xié)調(diào)測試執(zhí)行。