原文地址:http://blog.codefx.org/libraries/junit-5-conditions/
原文日期:08, May, 2016
譯文首發: Linesh 的博客:「譯」JUnit 5 系列:條件測試
個人 Github:http://github.com/linesh-simplicityjava
上一節咱們瞭解了 JUnit 新的擴展模型,瞭解了它是如何支持咱們向引擎定製一些行爲的。而後我還預告會爲你們講解條件測試,這一節主題就是它了。react
條件測試,指的是容許咱們自定義靈活的標準,來決定一個測試是否應該執行。條件(condition) 官方的叫法是條件測試執行。git
(若是不喜歡看文章,你能夠戳這裏看個人演講,或者看一下最近的 vJUG 講座,或者我在 DevoxxPL 上的 PPT。
本系列文章都基於 Junit 5發佈的先行版 Milestone 2。它可能會有變化。若是有新的里程碑(milestone)版本發佈,或者試用版正式發行時,我會再來更新這篇文章。
這裏要介紹的多數知識你均可以在 JUnit 5 用戶指南 中找到(這個連接指向的是先行版 Milestone 2,想看的最新版本文檔的話請戳這裏),而且指南還有更多的內容等待你發掘。下面的全部代碼均可以在 個人 Github 上找到。
相關的擴展點
動手實現一個@Disabled 註解
@DisabledOnOs
一種簡單的實現方式
更簡潔的API
代碼重構
@DisabledIfTestFails
異常收集
禁用測試
集成
回顧總結
分享&關注
還記得 拓展點 一節講的內容嗎?不記得了?好吧,簡單來講,JUnit 5 中定義了許多擴展點,每一個擴展點都對應一個接口。你本身的擴展能夠實現其中的某些接口,而後經過 @ExtendWith
註解註冊給 JUnit,後者會在特定的時間點調用你的接口實現。
要實現條件測試,你須要關注其中的兩個擴展點: ContainerExecutionCondition
(容器執行條件)和 TestExecutionCondition
(測試執行條件)。
public interface ContainerExecutionCondition extends Extension { /** * Evaluate this condition for the supplied ContainerExtensionContext. * * An enabled result indicates that the container should be executed; * whereas, a disabled result indicates that the container should not * be executed. * * @param context the current ContainerExtensionContext; never null * @return the result of evaluating this condition; never null */ ConditionEvaluationResult evaluate(ContainerExtensionContext context); } public interface TestExecutionCondition extends Extension { /** * Evaluate this condition for the supplied TestExtensionContext. * * An enabled result indicates that the test should be executed; * whereas, a disabled result indicates that the test should not * be executed. * * @param context the current TestExtensionContext; never null * @return the result of evaluating this condition; never null */ ConditionEvaluationResult evaluate(TestExtensionContext context); }
ContainerExecutionCondition
接口將決定容器中的測試是否會被執行。一般狀況下,你使用 @Test
註解來標記測試,此時測試所在的類就是容器。同時,單獨的測試方法是否執行則是由 TestExecutionCondition
接口決定的。
(這裏,我說的是「一般狀況下」,由於其餘測試引擎可能對容器和測試有大相徑庭的定義。但通常狀況下,測試就是單個的方法,容器指的就是測試類。)
嗯,基本知識就這麼多。想實現條件測試,至少須要實現以上兩個接口中的一個,並在接口的 evalute
方法中執行本身的條件檢查。
最簡單的「條件」就是判斷都沒有,直接禁用測試。若是在方法上發現了 @Disabled
註解,咱們就直接禁用該測試。
讓咱們來寫一個這樣的 @Disabled
註解吧:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(@DisabledCondition.class) public @interface Disabled { }
對應的擴展以下:
public class DisabledCondition implements ContainerExecutionCondition, TestExecutionCondition { private static final ConditionEvaluationResult ENABLED = ConditionEvaluationResult.enabled("@Disabled is not present"); @Override public ConditionEvaluationResult evaluate( ContainerExtensionContext context) { return evaluateIfAnnotated(context.getElement()); } @Override public ConditionEvaluationResult evaluate( TestExtensionContext context) { return evaluateIfAnnotated(context.getElement()); } private ConditionEvaluationResult evaluateIfAnnotated( Optional<AnnotatedElement> element) { Optional<Disabled> disabled = AnnotationUtils .findAnnotation(element, Disabled.class); if (disabled.isPresent()) return ConditionEvaluationResult .disabled(element + " is @Disabled"); return ENABLED; } }
寫起來小菜一碟吧?在 JUnit 真實的產品代碼中,@Disabled
也是這麼實現的。不過,有兩個地方有一些細微的差異:
官方 @Disabled
註解不須要再使用 @ExtendWith
註冊擴展,由於它是默認註冊了的
官方 @Disabled
註解能夠接收一個參數,解釋測試被忽略的理由。它會在測試被忽略時被記錄下來
使用時請注意,AnnotationUtils
是個內部 API。不過,官方可能很快就會將它提供的功能給開放出來。
接下來讓咱們寫點更有意思的東西吧。
若是有些測試咱們只想讓它在特定的操做系統上面運行,這個要怎麼實現呢?
固然,咱們仍是從註解開始咯:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(OsCondition.class) public @interface DisabledOnOs { OS[] value() default {}; }
這回註解須要接收一個或多個參數值,你須要告訴它想禁用測試的操做系統有哪些。 OS
是個枚舉類,定義了全部操做系統的名字。同時,它還提供了一個靜態的 static OS determine()
方法,你可能已經從名字猜到了,它會推斷並返回你當前所用的操做系統。
如今咱們能夠着手實現 OsCondition
擴展類了。它必須檢查兩點:註解是否存在,以及當前操做系統是否在註解聲明的禁用列表中。
public class OsCondition implements ContainerExecutionCondition, TestExecutionCondition { // both `evaluate` methods forward to `evaluateIfAnnotated` as above private ConditionEvaluationResult evaluateIfAnnotated( Optional<AnnotatedElement> element) { Optional<DisabledOnOs> disabled = AnnotationUtils .findAnnotation(element, DisabledOnOs.class); if (disabled.isPresent()) return disabledIfOn(disabled.get().value()); return ENABLED; } private ConditionEvaluationResult disabledIfOn(OS[] disabledOnOs) { OS os = OS.determine(); if (Arrays.asList(disabledOnOs).contains(os)) return ConditionEvaluationResult .disabled("Test is disabled on " + os + "."); else return ConditionEvaluationResult .enabled("Test is not disabled on " + os + "."); } }
而後使用的時候就能夠像這樣:
@Test @DisabledOnOs(OS.WINDOWS) void doesNotRunOnWindows() { assertTrue(false); }
棒。
但代碼還能夠寫得更好!JUnit 的註解是可組合的,基於此咱們可讓這個條件註解更簡潔:
@TestExceptOnOs(OS.WINDOWS) void doesNotRunOnWindowsEither() { assertTrue(false); }
@TestExceptionOnOs
完美的實現方案是這樣的:
@Retention(RetentionPolicy.RUNTIME) @Test @DisabledOnOs(/* 經過某種方式取得註解下的 `value` 值 */) public @interface TestExceptOnOs { OS[] value() default {}; }
測試實際運行時, OsCondition::evaluateIfAnnotated
方法會掃描 @DisabledOnOs
註解,而後咱們發現它又是對 @TestExceptOnOs
的註解,前面寫的代碼就能夠如期工做了。但我不知道如何在 @DisabledOnOs
註解中獲取 @TestExceptOnOs
中的value()
值。:((你能作到嗎?)
次佳的選擇是,簡單地在 @TestExceptOnOs
註解上直接聲明應用的擴展就能夠了:
@Retention(RetentionPolicy.RUNTIME) @ExtendWith(OsCondition.class) @Test public @interface TestExceptOnOs { OS[] value() default {}; }
而後直接把 OsCondition:evaluateIfAnnotated
方法拉過來改改便可:
private ConditionEvaluationResult evaluateIfAnnotated( Optional<Annotatedelement> element) { Optional<DisabledOnOs> disabled = AnnotationUtils .findAnnotation(element, DisabledOnOs.class); if (disabled.isPresent()) return disabledIfOn(disabled.get().value()); Optional<TestExceptOnOs> testExcept = AnnotationUtils .findAnnotation(element, TestExceptOnOs.class); if (testExcept.isPresent()) return disabledIfOn(testExcept.get().value()); return ConditionEvaluationResult.enabled(""); }
收工。如今咱們能夠如期使用這個註解了。
咱們還須要建立一個意義恰好相反的註解(即如今變爲,當前操做系統不在提供列表時,才禁用測試),工做是相似的,可是註解名會更表意,再加入靜態導入後,咱們的代碼最終能夠整理成這樣:
@TestOn(WINDOWS) void doesNotRunOnWindoesEither() { assertTrue(false); }
還挺好看的,是不?
「譯者注:英文中condition有多個意思:「條件;空調」。做者這裏配圖取雙關」
咱們再考慮一種場景——我保證此次能夠接觸更有意思的東西!假設如今有許多(集成)測試,若是其中有一個拋出了特定的異常而失敗,那麼其餘測試也必須會掛。爲了節省時間,咱們但願在這種狀況下直接禁用掉其餘的測試。
那麼咱們須要作些什麼工做呢?首先第一反應不難想到,咱們 必須先能收集測試執行過程拋出的異常。這確定須要在單個測試類級別的生命週期中進行處理,不然就可能由於其餘測試類中拋出的異常而影響到本測試類的運行。其次,咱們須要一個實現一個條件:它會檢查某個特定的異常是否已被拋出過,如果,禁用當前測試。
翻閱一下文檔中提供的 擴展點列表,不難發現有一項「異常處理」,看起來就是咱們想要的東西:
/** * TestExecutionExceptionHandler defines the API for Extensions that wish to react to thrown exceptions in tests. * * [ ... ] */ public interface TestExecutionExceptionHandler extends ExtensionPoint { /** * Handle the supplied throwable. * * Implementors must perform one of the following. * * - Swallow the supplied throwable, thereby preventing propagation * - Rethrow the incoming throwable as is * - Throw a new exception, potenially wrapping the supplied throwable * * [ ... ] */ void handleTestExecutionException( TestExtensionContext context, Throwable throwable) throws Throwable; }
讀完發現,咱們的任務就是實現 handleException
方法,存儲起接收到的異常並從新拋出。
你可能還記得我提過的關於擴展點和無狀態的一些結論:
引擎對擴展實例的初始化時間、實例的生存時間未做出任何規約和保證,所以,擴展必須是無狀態的。若是一個擴展須要維持任何狀態信息,那麼它必須使用 JUnit 提供的一個倉庫(store)來進行信息讀取和寫入。
看來咱們是必須使用這個 store 了。store 其實就是存放咱們但願保存的一些東西,一個可索引的資源集合。它能夠在擴展上下文對象中取得,後者會被傳給大多數擴展點接口方法做爲參數。不過須要注意的是,每一個不一樣的上下文對象都有本身一個獨立的 store,因此咱們還必須決定使用哪一個 store。
每一個測試方法有一個本身的上下文對象(TestExtensionContext
),同時,測試類也有一個本身的上下文對象(ContainerExtensionContext
)。還記得咱們的需求嗎?保存測試類中任何測試方法可能拋出的異常,僅此而已。也即,咱們不會保存其餘測試類中拋出的異常。這樣一來,容器級別的上下文 ContainerExtensionContext
恰好就是咱們須要的了。
接下來,咱們可使用這個容器上下文,經過它來存儲全部測試過程拋出的異常:
private static final Namespece NAMESPACE = Namespace .of("org", "codefx", "CollectExceptions"); private static final String THROWN_EXCEPTIONS_KEY = "THROWN_EXCEPTION_KEY"; @SuppressWarnings("unchecked") private static Set<Exception> getThrown(ExtensionContext context) { ExtensionContext containerContext = getAncestorContainerContext(context) .orElseThrow(IllegalStateException::new); retrun (Set<Exception>) containerContext .getStore(NAMESPACE) .getOrComputeIfAbsent( THROWN_EXCEPTIONS_KEY, ignoredKey -> new HashSet<>()); } private static Optional<ExtensionContext> getAncestorContainerContext( ExtensionContext context) { Optional<ExtensionContext> containerContext = Optional.of(context); while (containerContext.isPresent()) && !(containerContext.get() instanceof ContainerExtenstionContext)) containerContext = containerContext.get().getParent(); return containerContext; }
如今存儲一個異常就很是簡單了:
@Override public void handleException(TestExtensionContext context, Throwable throwable) throws Throwable { if (throwable instanceof Exception) { getThrown(context).add((Exception) throwable); throw throwable;
有意思的是,這個擴展仍是自擴展的,沒準還能夠用來作數據統計呢「譯者注:這句不太理解,原文 This is actually an interesting extension of its own. Maybe it could be used for analytics as well.」。無論怎樣,咱們須要一個public方法來拿到已拋出的異常列表:
public static Stream<Exception> getThrownExceptions( ExtensionContext context) { return getThrown(context).stream(); }
有了這個方法,其餘的擴展就能夠檢查至今爲止所拋出的異常列表了。
禁用測試的部分與前節所述十分相似,咱們能夠很快寫出代碼:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DisabledIfTestFailedCondition.class) public @interface DisabledIfTestFailedWith { Class <? extends Exception>[] value() default {}; }
注意,如今僅容許該註解被用在測試方法上。應用在測試類上也說得過去,不過咱們如今先不把它複雜化。所以咱們只須要實現接口TestExecutionCondition
便可。咱們先檢查註解是否存在,如果,再拿到用戶提供的異常類做爲參數,調用 disableIfExceptionWasThrown
。
private ConditionEvaluationResult disableIfExceptionWasThrown( TestExtensionContext context, Class<? extends Exception>[] exceptions) { return Arrays.stream(exceptions) .filter(ex -> wasThrown(context, ex)) .findAny(). .map(thrown -> ConditionEvaluationResult.disabled( thrown.getSimpleName() + "was thrown.")) .orElseGet(() -> ConditionEvaluationResult.enabled("")); } private static boolean wasThrown( TestExtensionContext context, Class<? extends Exception> exception) { return CollectExceptionExtension.getThrownExceptions(context) .map(Object::getClass) .anyMatch(exception::isAssignableFrom); }
至此爲止需求完成。如今咱們可使用這個註解,在某個特定類型的異常拋出時禁用測試了:
@CollectExceptions class DisabledIfFailsTest { private static boolean failedFirst = false; @Test void throwException() { System.out.println("I failed!"); failedFirst = true; throw new RuntimeException(); } @Test @DisabledIfTestFailedWith(RuntimeException.class) void disableIfOtherFailedFirst() { System.out.println("Nobody failed yet! (Right?)"); assertFalse(failedFirst); } }
哇哦,本篇的代碼還挺多的!不過相信到此你已經能徹底理解怎麼在 JUnit 5 中實現條件測試了:
建立一個註解,並使用 @ExtendWith
註解它,而後提供你本身實現的條件類
實現 ContainerExecutionCondition
或/和 TestExecutionCondition
檢查測試類上是否應用了你新建立的註解
檢查特定條件是否實現,並返回結果
除此之外,咱們還看到註解之間能夠組合,學到如何使用 JUnit 提供的 store 來保存數據,以及一個擴展的實現,如何經過自定義註解的加入變得更加優雅。
更多關於 旗幟 擴展點的故事「譯者注:原文爲more fun with flag extension points,more fun with flags 是生活大爆炸中謝耳朵講國旗的故事一部」,請參考下篇文章,咱們會探討關於參數注入的問題。