您好,登錄后才能下訂單哦!
這篇文章主要為大家分析了如何分析JUnit 5中的Extension Model擴展模型的相關知識點,內容詳細易懂,操作細節合理,具有一定參考價值。如果感興趣的話,不妨跟著跟隨小編一起來看看,下面跟著小編一起深入學習“如何分析JUnit 5中的Extension Model擴展模型”的知識吧。
JUnit 4 的擴展模型
我們先來看看 JUnit 4 中是如何實現擴展的。在 JUnit 4 中實現擴展主要是通過兩個,有時也互有重疊的擴展機制:運行器(Runners)和規則(Rules)。
運行器(Runners)
測試運行器負責管理諸多測試的生命周期,包括它們的實例化、setup/teardown 方法的調用、測試運行、異常處理、發送消息等。在 JUnit 4 提供的運行器實現中,它負責了這所有的事情。
在 JUnit 4 中,擴展 JUnit 的唯一方法是:創建一個新的運行器,然后使用它標記你新的測試類:@Runwith(MyRunner.class)。這樣 JUnit 就會識別并使用它來運行測試,而不會使用其默認的實現。
這個方式很重,對于小定制小擴展來說很不方便。同時它有個很苛刻的限制:一個測試類只能用一個運行器來跑,這意味著你不能組合不同的運行器。也即是說,你不能同時享受到兩個以上運行器提供的特性,比如說不能同時使用 Mockito 和 Spring 的運行器,等。
規則(Rules)
為了克服這個限制,JUnit 4.7 中引入了規則的概念,它是指測試類中特別的注解字段。 JUnit 4 會把測試方法(與一些其他的行為)包裝一層傳給規則。規則因此可以在測試代碼執行前后插入,執行一些代碼。很多時候在測試方法中也會直接調規則類上的方法。
這里有一個例子,展示的是 temporary folder (臨時文件夾)規則:
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"); // ... } }
因為 @Rule 注解的存在,JUnit 會先把測試方法 testUsingTempFolder 包裝成一個可執行代碼塊,傳給 folder 規則。這個規則的作用是執行時, 由 folder 創建一個臨時目錄,執行測試,測試完成后刪除臨時目錄。因此,在測試內部可以放心地在臨時目錄下創建文件和文件夾。
當然還有其他的規則,比如允許你在 Swing 的事件分發線程中執行測試 的規則,負責連接和斷開數據庫的規則,以及讓運行過久的測試直接超時的規則等。
規則特性其實已經是個很大的改進了,不過仍有局限,它只能在測試運行之前或之后定制操作。如果你想在此之外的時間點進行擴展,這個特性也無能為力了。
現狀
總而言之,在 JUnit 4 中存在兩種不同的擴展機制,兩者均各有局限,并且功能還有重疊的部分。在 JUnit 4 下編寫干凈的擴展是很難的事。此外,即使你嘗試組合兩種不同的擴展方式,通常也不會一帆風順,有時它可能根本不按照開發者期望的方式工作。
JUnit 5 的擴展模型
Junit Lambda 項目成立伊始便有幾點核心準則,其中一條便是“擴展點優于新特性”。這個準則其實也就是新版本 JUnit 中最重要的擴展機制了——并非唯一,但無疑是最重要之一。
擴展點
JUnit 5 擴展可以聲明其主要關注的是測試生命周期的哪部分。JUnit 5 引擎在處理測試時,它會依次檢查這些擴展點,并調用每個已注冊的擴展。大體來說,這些擴展點出現次序如下:
測試類實例 后處理
BeforeAll 回調
測試及容器執行條件檢查
BeforeEach 回調
參數解析
測試執行前
測試執行后
異常處理
AfterEach 回調
AfterAll 回調
(如果上面有你覺得不甚清晰或理解的點,請不用擔心,我們接下來會挑其中的一些來講解。)
每個擴展點都對應一個接口。接口方法會接受一些參數,一些擴展點所處生命周期的上下文信息。比如,被測實例與方法、測試的名稱、參數、注解等信息。
一個擴展可以實現任意個以上的接口方法,引擎會在調用它們時傳入相應的上下文信息作為參數。有了這些信息,擴展就可以放心地實現所需的功能了。
無狀態
這里我們需要考慮一個重要的細節:引擎對擴展實例的初始化時間、實例的生存時間未作出任何規約和保證,因此,擴展必須是無狀態的。如果一個擴展需要維持任何狀態信息,那么它必須使用 JUnit 提供的一個倉庫(store)來進行信息讀取和寫入。
這樣做的原因有幾個:
擴展的初始化時機和方式對引擎是未知的(每個測試實例化一次?每個類實例化一次?還是每次運行實例化一次?)。
JUnit 不想額外維護和管理每個擴展創建的實例。
如果擴展之間想要進行通信,那么無論如何 JUnit 都必須提供一個數據交互的機制。
應用擴展
創建完擴展后,接下來需要做的就僅僅是告訴 JUnit 它的存在。這可以通過在需要使用該擴展的測試類或測試方法上添加一個@ExtendWith(MyExtension.class) 簡單實現。
其實,還有另一種更簡明的方式。不過要理解那種方式,我們必須先看一下 JUnit 的擴展模型中還有哪些內容。
自定義注解
JUnit 5 的 API 大部分是基于注解的,而且引擎在檢查注解時還做了些額外的工作:它不僅會查找字段、類、參數上應用的注解,還會注解上的注解。引擎會把找到的所有注解都應用到被注解元素上。注解另一個注解可以通過所謂的元注解做到,酷的是 Junit 提供的所有注解都說得上是元注解了。
它的意義在于,JUnit 5 中我們就能夠創建并組合不同的注解了,并且它們具備組合多個注解特性的能力:
/** * 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 { }
這個自定義的“集成測試”注解 @IntegrationTest 可以這樣使用:
@IntegrationTest void runsWithCustomAnnotation() { // this gets executed // even though `@IntegrationTest` is not defined by JUnit }
進一步我們可以為擴展使用更簡明的注解:
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(ExternalDatabaseExtension.class) public @interface Database { }
現在我們可以直接使用 @Database 注解了,而不需要再聲明測試應用了特定的擴展@ExtendWith(ExternalDatabaseExtension.class)。并且由于我們把注解類型 ElementType.ANNOTATION_TYPE 也添加到擴展支持的目標類型中去了,因此該注解也可以被我們或他人進一步的使用、組合。
例子
假設現在有個場景,我想量化一下測試運行花費的時間。首先,可以先創建一個我們想要的注解:
@Target({ TYPE, METHOD, ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(BenchmarkExtension.class) public @interface Benchmark { }
注解聲明其應用了 BenchmarkExtension 擴展,這是我們接下來要實現的。TODOLIST 如下:
計算所有測試類的運行時間,在所有測試執行前保存其起始時間
計算每個測試方法的運行時間,在每個測試方法執行前保存其起始時間
在每個測試方法執行完畢后,獲取其結束時間,計算并輸出該測試方法的運行時間
在所有測試類執行完畢后,獲取其結束時間,計算并輸出所有測試的運行時間
以上操作,僅對所有注解了 @BenchMark 的測試類或測試方法生效
最后一點需求可能不是一眼便能發現。如果一個方法并未注解 @Benchmark 注解,它有什么可能被我們的擴展處理? 一個語法上的原因是,如果一個擴展被應用到了一個類上,那么它默認也會應用到類中的所有方法上。因此,如果我們的需求是計算整個測試類的運行時間,但不需具體到類中每個單獨方法的運行時間時,類中的測試方法就必須被手動排除。這點我們可以通過單獨檢查每個方法是否應用了注解來做到。
有趣的是,需求的前四點與擴展點中的其中四個是一一對應的:BeforeAll、BeforeTestExecution、AfterTestExecution 與 AfterAll。因此我們要做的任務便是實現這四個對應的接口。具體實現很簡單,把上面說的翻譯成代碼即是:
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 } } 「譯者:啊這代碼讓人心曠神怡。」
上面代碼有幾個地方值得留意。首先是 shouldBeBenchmarked 方法,它使用了 JUnit 的 API 來獲取當前元素是否(被元)注解了@Benchmark 注解;其次, writeCurrentTime / loadLaunchTime 方法中使用了 Junit 提供的 store 以寫入和讀取運行時間。
關于“如何分析JUnit 5中的Extension Model擴展模型”就介紹到這了,更多相關內容可以搜索億速云以前的文章,希望能夠幫助大家答疑解惑,請多多支持億速云網站!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。