這篇文章主要為大家分析了如何分析JUnit 5中的Extension Model擴(kuò)展模型的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì)易懂,操作細(xì)節(jié)合理,具有一定參考價(jià)值。如果感興趣的話,不妨跟著跟隨小編一起來看看,下面跟著小編一起深入學(xué)習(xí)“如何分析JUnit 5中的Extension Model擴(kuò)展模型”的知識(shí)吧。
隆化ssl適用于網(wǎng)站、小程序/APP、API接口等需要進(jìn)行數(shù)據(jù)傳輸應(yīng)用場(chǎng)景,ssl證書未來市場(chǎng)廣闊!成為成都創(chuàng)新互聯(lián)公司的ssl證書銷售渠道,可以享受市場(chǎng)價(jià)格4-6折優(yōu)惠!如果有意向歡迎電話聯(lián)系或者加微信:18980820575(備注:SSL證書合作)期待與您的合作!
JUnit 4 的擴(kuò)展模型
我們先來看看 JUnit 4 中是如何實(shí)現(xiàn)擴(kuò)展的。在 JUnit 4 中實(shí)現(xiàn)擴(kuò)展主要是通過兩個(gè),有時(shí)也互有重疊的擴(kuò)展機(jī)制:運(yùn)行器(Runners)和規(guī)則(Rules)。
運(yùn)行器(Runners)
測(cè)試運(yùn)行器負(fù)責(zé)管理諸多測(cè)試的生命周期,包括它們的實(shí)例化、setup/teardown 方法的調(diào)用、測(cè)試運(yùn)行、異常處理、發(fā)送消息等。在 JUnit 4 提供的運(yùn)行器實(shí)現(xiàn)中,它負(fù)責(zé)了這所有的事情。
在 JUnit 4 中,擴(kuò)展 JUnit 的唯一方法是:創(chuàng)建一個(gè)新的運(yùn)行器,然后使用它標(biāo)記你新的測(cè)試類:@Runwith(MyRunner.class)。這樣 JUnit 就會(huì)識(shí)別并使用它來運(yùn)行測(cè)試,而不會(huì)使用其默認(rèn)的實(shí)現(xiàn)。
這個(gè)方式很重,對(duì)于小定制小擴(kuò)展來說很不方便。同時(shí)它有個(gè)很苛刻的限制:一個(gè)測(cè)試類只能用一個(gè)運(yùn)行器來跑,這意味著你不能組合不同的運(yùn)行器。也即是說,你不能同時(shí)享受到兩個(gè)以上運(yùn)行器提供的特性,比如說不能同時(shí)使用 Mockito 和 Spring 的運(yùn)行器,等。
規(guī)則(Rules)
為了克服這個(gè)限制,JUnit 4.7 中引入了規(guī)則的概念,它是指測(cè)試類中特別的注解字段。 JUnit 4 會(huì)把測(cè)試方法(與一些其他的行為)包裝一層傳給規(guī)則。規(guī)則因此可以在測(cè)試代碼執(zhí)行前后插入,執(zhí)行一些代碼。很多時(shí)候在測(cè)試方法中也會(huì)直接調(diào)規(guī)則類上的方法。
這里有一個(gè)例子,展示的是 temporary folder (臨時(shí)文件夾)規(guī)則:
public static class HasTempFolder { @Rule public TemporaryFolder folder= new TemporaryFolder(); @Test public void testUsingTempFolder() throws IOException { File createdFile= folder.newFile("myfile.txt"); File createdFolder= folder.newFolder("subfolder"); // ... } }
因?yàn)?@Rule 注解的存在,JUnit 會(huì)先把測(cè)試方法 testUsingTempFolder 包裝成一個(gè)可執(zhí)行代碼塊,傳給 folder 規(guī)則。這個(gè)規(guī)則的作用是執(zhí)行時(shí), 由 folder 創(chuàng)建一個(gè)臨時(shí)目錄,執(zhí)行測(cè)試,測(cè)試完成后刪除臨時(shí)目錄。因此,在測(cè)試內(nèi)部可以放心地在臨時(shí)目錄下創(chuàng)建文件和文件夾。
當(dāng)然還有其他的規(guī)則,比如允許你在 Swing 的事件分發(fā)線程中執(zhí)行測(cè)試 的規(guī)則,負(fù)責(zé)連接和斷開數(shù)據(jù)庫(kù)的規(guī)則,以及讓運(yùn)行過久的測(cè)試直接超時(shí)的規(guī)則等。
規(guī)則特性其實(shí)已經(jīng)是個(gè)很大的改進(jìn)了,不過仍有局限,它只能在測(cè)試運(yùn)行之前或之后定制操作。如果你想在此之外的時(shí)間點(diǎn)進(jìn)行擴(kuò)展,這個(gè)特性也無能為力了。
現(xiàn)狀
總而言之,在 JUnit 4 中存在兩種不同的擴(kuò)展機(jī)制,兩者均各有局限,并且功能還有重疊的部分。在 JUnit 4 下編寫干凈的擴(kuò)展是很難的事。此外,即使你嘗試組合兩種不同的擴(kuò)展方式,通常也不會(huì)一帆風(fēng)順,有時(shí)它可能根本不按照開發(fā)者期望的方式工作。
JUnit 5 的擴(kuò)展模型
Junit Lambda 項(xiàng)目成立伊始便有幾點(diǎn)核心準(zhǔn)則,其中一條便是“擴(kuò)展點(diǎn)優(yōu)于新特性”。這個(gè)準(zhǔn)則其實(shí)也就是新版本 JUnit 中最重要的擴(kuò)展機(jī)制了——并非唯一,但無疑是最重要之一。
擴(kuò)展點(diǎn)
JUnit 5 擴(kuò)展可以聲明其主要關(guān)注的是測(cè)試生命周期的哪部分。JUnit 5 引擎在處理測(cè)試時(shí),它會(huì)依次檢查這些擴(kuò)展點(diǎn),并調(diào)用每個(gè)已注冊(cè)的擴(kuò)展。大體來說,這些擴(kuò)展點(diǎn)出現(xiàn)次序如下:
測(cè)試類實(shí)例 后處理
BeforeAll 回調(diào)
測(cè)試及容器執(zhí)行條件檢查
BeforeEach 回調(diào)
參數(shù)解析
測(cè)試執(zhí)行前
測(cè)試執(zhí)行后
異常處理
AfterEach 回調(diào)
AfterAll 回調(diào)
(如果上面有你覺得不甚清晰或理解的點(diǎn),請(qǐng)不用擔(dān)心,我們接下來會(huì)挑其中的一些來講解。)
每個(gè)擴(kuò)展點(diǎn)都對(duì)應(yīng)一個(gè)接口。接口方法會(huì)接受一些參數(shù),一些擴(kuò)展點(diǎn)所處生命周期的上下文信息。比如,被測(cè)實(shí)例與方法、測(cè)試的名稱、參數(shù)、注解等信息。
一個(gè)擴(kuò)展可以實(shí)現(xiàn)任意個(gè)以上的接口方法,引擎會(huì)在調(diào)用它們時(shí)傳入相應(yīng)的上下文信息作為參數(shù)。有了這些信息,擴(kuò)展就可以放心地實(shí)現(xiàn)所需的功能了。
無狀態(tài)
這里我們需要考慮一個(gè)重要的細(xì)節(jié):引擎對(duì)擴(kuò)展實(shí)例的初始化時(shí)間、實(shí)例的生存時(shí)間未作出任何規(guī)約和保證,因此,擴(kuò)展必須是無狀態(tài)的。如果一個(gè)擴(kuò)展需要維持任何狀態(tài)信息,那么它必須使用 JUnit 提供的一個(gè)倉(cāng)庫(kù)(store)來進(jìn)行信息讀取和寫入。
這樣做的原因有幾個(gè):
擴(kuò)展的初始化時(shí)機(jī)和方式對(duì)引擎是未知的(每個(gè)測(cè)試實(shí)例化一次?每個(gè)類實(shí)例化一次?還是每次運(yùn)行實(shí)例化一次?)。
JUnit 不想額外維護(hù)和管理每個(gè)擴(kuò)展創(chuàng)建的實(shí)例。
如果擴(kuò)展之間想要進(jìn)行通信,那么無論如何 JUnit 都必須提供一個(gè)數(shù)據(jù)交互的機(jī)制。
應(yīng)用擴(kuò)展
創(chuàng)建完擴(kuò)展后,接下來需要做的就僅僅是告訴 JUnit 它的存在。這可以通過在需要使用該擴(kuò)展的測(cè)試類或測(cè)試方法上添加一個(gè)@ExtendWith(MyExtension.class) 簡(jiǎn)單實(shí)現(xiàn)。
其實(shí),還有另一種更簡(jiǎn)明的方式。不過要理解那種方式,我們必須先看一下 JUnit 的擴(kuò)展模型中還有哪些內(nèi)容。
自定義注解
JUnit 5 的 API 大部分是基于注解的,而且引擎在檢查注解時(shí)還做了些額外的工作:它不僅會(huì)查找字段、類、參數(shù)上應(yīng)用的注解,還會(huì)注解上的注解。引擎會(huì)把找到的所有注解都應(yīng)用到被注解元素上。注解另一個(gè)注解可以通過所謂的元注解做到,酷的是 Junit 提供的所有注解都說得上是元注解了。
它的意義在于,JUnit 5 中我們就能夠創(chuàng)建并組合不同的注解了,并且它們具備組合多個(gè)注解特性的能力:
/** * We define a custom annotation that: * - stands in for '@Test' so that the method gets executed * - has the tag "integration" so we can filter by that, * e.g. when running tests from the command line */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Test @Tag("integration") public @interface IntegrationTest { }
這個(gè)自定義的“集成測(cè)試”注解 @IntegrationTest 可以這樣使用:
@IntegrationTest void runsWithCustomAnnotation() { // this gets executed // even though `@IntegrationTest` is not defined by JUnit }
進(jìn)一步我們可以為擴(kuò)展使用更簡(jiǎn)明的注解:
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(ExternalDatabaseExtension.class) public @interface Database { }
現(xiàn)在我們可以直接使用 @Database 注解了,而不需要再聲明測(cè)試應(yīng)用了特定的擴(kuò)展@ExtendWith(ExternalDatabaseExtension.class)。并且由于我們把注解類型 ElementType.ANNOTATION_TYPE 也添加到擴(kuò)展支持的目標(biāo)類型中去了,因此該注解也可以被我們或他人進(jìn)一步的使用、組合。
例子
假設(shè)現(xiàn)在有個(gè)場(chǎng)景,我想量化一下測(cè)試運(yùn)行花費(fèi)的時(shí)間。首先,可以先創(chuàng)建一個(gè)我們想要的注解:
@Target({ TYPE, METHOD, ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(BenchmarkExtension.class) public @interface Benchmark { }
注解聲明其應(yīng)用了 BenchmarkExtension 擴(kuò)展,這是我們接下來要實(shí)現(xiàn)的。TODOLIST 如下:
計(jì)算所有測(cè)試類的運(yùn)行時(shí)間,在所有測(cè)試執(zhí)行前保存其起始時(shí)間
計(jì)算每個(gè)測(cè)試方法的運(yùn)行時(shí)間,在每個(gè)測(cè)試方法執(zhí)行前保存其起始時(shí)間
在每個(gè)測(cè)試方法執(zhí)行完畢后,獲取其結(jié)束時(shí)間,計(jì)算并輸出該測(cè)試方法的運(yùn)行時(shí)間
在所有測(cè)試類執(zhí)行完畢后,獲取其結(jié)束時(shí)間,計(jì)算并輸出所有測(cè)試的運(yùn)行時(shí)間
以上操作,僅對(duì)所有注解了 @BenchMark 的測(cè)試類或測(cè)試方法生效
最后一點(diǎn)需求可能不是一眼便能發(fā)現(xiàn)。如果一個(gè)方法并未注解 @Benchmark 注解,它有什么可能被我們的擴(kuò)展處理? 一個(gè)語法上的原因是,如果一個(gè)擴(kuò)展被應(yīng)用到了一個(gè)類上,那么它默認(rèn)也會(huì)應(yīng)用到類中的所有方法上。因此,如果我們的需求是計(jì)算整個(gè)測(cè)試類的運(yùn)行時(shí)間,但不需具體到類中每個(gè)單獨(dú)方法的運(yùn)行時(shí)間時(shí),類中的測(cè)試方法就必須被手動(dòng)排除。這點(diǎn)我們可以通過單獨(dú)檢查每個(gè)方法是否應(yīng)用了注解來做到。
有趣的是,需求的前四點(diǎn)與擴(kuò)展點(diǎn)中的其中四個(gè)是一一對(duì)應(yīng)的:BeforeAll、BeforeTestExecution、AfterTestExecution 與 AfterAll。因此我們要做的任務(wù)便是實(shí)現(xiàn)這四個(gè)對(duì)應(yīng)的接口。具體實(shí)現(xiàn)很簡(jiǎn)單,把上面說的翻譯成代碼即是:
public class BenchmarkExtension implements BeforeAllExtensionPoint, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterAllExtensionPoint { private static final Namespace NAMESPACE = Namespace.of("BenchmarkExtension"); @Override public void beforeAll(ContainerExtensionContext context) { if (!shouldBeBenchmarked(context)) return; writeCurrentTime(context, LaunchTimeKey.CLASS); } @Override public void beforeTestExecution(TestExtensionContext context) { if (!shouldBeBenchmarked(context)) return; writeCurrentTime(context, LaunchTimeKey.TEST); } @Override public void afterTestExecution(TestExtensionContext context) { if (!shouldBeBenchmarked(context)) return; long launchTime = loadLaunchTime(context, LaunchTimeKey.TEST); long runtime = currentTimeMillis() - launchTime; print("Test", context.getDisplayName(), runtime); } @Override public void afterAll(ContainerExtensionContext context) { if (!shouldBeBenchmarked(context)) return; long launchTime = loadLaunchTime(context, LaunchTimeKey.CLASS); long runtime = currentTimeMillis() - launchTime; print("Test container", context.getDisplayName(), runtime); } private static boolean shouldBeBenchmarked(ExtensionContext context) { return context.getElement() .map(el -> el.isAnnotationPresent(Benchmark.class)) .orElse(false); } private static void writeCurrentTime( ExtensionContext context, LaunchTimeKey key) { context.getStore(NAMESPACE).put(key, currentTimeMillis()); } private static long loadLaunchTime( ExtensionContext context, LaunchTimeKey key) { return (Long) context.getStore(NAMESPACE).remove(key); } private static void print( String unit, String displayName, long runtime) { System.out.printf("%s '%s' took %d ms.%n", unit, displayName, runtime); } private enum LaunchTimeKey { CLASS, TEST } } 「譯者:啊這代碼讓人心曠神怡?!?/pre>上面代碼有幾個(gè)地方值得留意。首先是 shouldBeBenchmarked 方法,它使用了 JUnit 的 API 來獲取當(dāng)前元素是否(被元)注解了@Benchmark 注解;其次, writeCurrentTime / loadLaunchTime 方法中使用了 Junit 提供的 store 以寫入和讀取運(yùn)行時(shí)間。
關(guān)于“如何分析JUnit 5中的Extension Model擴(kuò)展模型”就介紹到這了,更多相關(guān)內(nèi)容可以搜索創(chuàng)新互聯(lián)以前的文章,希望能夠幫助大家答疑解惑,請(qǐng)多多支持創(chuàng)新互聯(lián)網(wǎng)站!
文章題目:如何分析JUnit5中的ExtensionModel擴(kuò)展模型
鏈接地址:http://weahome.cn/article/gogpec.html