什么是junit5 ?
先看來個公式:
junit 5 = junit platform + junit jupiter + junit vintage
這看上去比junit4 復雜,實際上在導入包時也會復雜一些。
junit platform是在jvm上啟動測試框架的基礎。
junit jupiter是junit5擴展的新的編程模型和擴展模型,用來編寫測試用例。jupiter子項目為在平臺上運行jupiter的測試提供了一個testengine (測試引擎)。
junit vintage提供了一個在平臺上運行junit 3和junit 4的testengine 。
關鍵要點
- junit 5是一個模塊化和可擴展的測試框架,支持java 8及更高版本。
- junit 5由三個部分組成——一個基礎平臺、一個新的編程和擴展模型jupiter,以及一個名為vintage的向后兼容的測試引擎。
- junit 5 jupiter的擴展模型可用于向junit中添加自定義功能。
- 擴展模型api測試生命周期提供了鉤子和注入自定義參數的方法(即依賴注入)。
junit是最受歡迎的基于jvm的測試框架,在第5個主要版本中進行了徹底的改造。junit 5提供了豐富的功能——從改進的注解、標簽和過濾器到條件執行和對斷言消息的惰性求值。這讓基于tdd編寫單元測試變得輕而易舉。新框架還帶來了一個強大的擴展模型。擴展開發人員可以使用這個新模型向junit 5中添加自定義功能。本文將指導你完成自定義擴展的設計和實現。這種自定義擴展機制為java程序員提供了一種創建和執行故事和行為(即bdd規范測試)的方法。
我們首先使用junit 5和我們的自定義擴展(稱為“storyextension”)來編寫一個示例故事和行為(測試方法)。這個示例使用了兩個新的自定義注解“@story”和“@scenario”,以及“scene”類,用以支持我們的自定義storyextension:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import org.junit.jupiter.api.extension.extendwith; import ud.junit.bdd.ext.scenario; import ud.junit.bdd.ext.scene; import ud.junit.bdd.ext.story; import ud.junit.bdd.ext.storyextension; @extendwith (storyextension. class ) @story (name=“returns go back to the stockpile”, description=“...“) public class storefronttest { @scenario (“refunded items should be returned to the stockpile”) public void refundeditemsshouldberestocked(scene scene) { scene .given(“customer bought a blue sweater”, () -> buysweater(scene, “blue”)) .and(“i have three blue sweaters in stock”, () -> assertequals( 3 , sweatercount(scene, “blue”), “store should carry 3 blue sweaters”)) .when(“the customer returns the blue sweater for a refund”, () -> refund(scene, 1 , “blue”)) .then(“i should have four blue sweaters in stock”, () -> assertequals( 4 , sweatercount(scene, “blue”), “store should carry 4 blue sweaters”)) .run(); } } |
從代碼片段中我們可以看到,jupiter的擴展模型非常強大。我們還可以看到,我們的自定義擴展及其相應的注解為測試用例編寫者提供了簡單而干凈的方法來編寫bdd規范。
作為額外的獎勵,當使用我們的自定義擴展程序執行測試時,會生成如下所示的文本報告:
story: returns go back to the stockpile
as a store owner, in order to keep track of stock, i want to add items back to stock when they're returned.
scenario: refunded items should be returned to stock
given that a customer previously bought a blue sweater from me
and i have three blue sweaters in stock
when the customer returns the blue sweater for a refund
then i should have four blue sweaters in stock
這些報告可以作為應用程序功能集的文檔。
自定義擴展storyextension能夠借助以下核心概念來支持和執行故事和行為:
- 用于裝飾測試類和測試方法的注解
- junit 5 jupiter的生命周期回調
- 動態參數解析
注解
示例中的“@extendwith”注解是由jupiter提供的標記接口。這是在測試類或方法上注冊自定義擴展的方法,目的是讓jupiter測試引擎調用給定類或方法的自定義擴展。或者,測試用例編寫者可以通過編程的方式注冊自定義擴展,或者通過服務加載器機制進行自動注冊。
我們的自定義擴展需要一種識別故事的方法。為此,我們定義了一個名為“story”的自定義注解類,如下所示:
1
2
3
4
|
import org.junit.platform.commons.annotation.testable; @testable public @interface story {...} |
測試用例編寫者應該使用這個自定義注解將測試類標記為故事。請注意,這個注解本身使用了junit 5內置的“@testable”注解。這個注解為ide和其他工具提供了一種識別可測試的類和方法的方式——也就是說,帶有這個注解的類或方法可以通過junit 5 jupiter測試引擎來執行。
我們的自定義擴展還需要一種方法來識別故事中的行為或場景。為此,我們定義一個名為“scenario”的自定義注解類,看起來像這樣:
1
2
3
4
|
import org.junit.jupiter.api.test; @test public @interface scenario {...} |
測試用例編寫者應使用這個自定義注解將測試方法標記為場景。這個注解本身使用了junit 5 jupiter的內置“@test”注解。當ide和測試引擎掃描給定的一組測試類并在公共實例方法上找到@scenario注解時,就會將這些方法標記為可執行的測試方法。
請注意,與junit 4的@test注解不同,jupiter的@test注解不支持可選的“預期”異常和“超時”參數。jupiter的@test注解是從頭開始設計的,并考慮到了可擴展性。
生命周期
junit 5 jupiter提供了擴展回調,可用于訪問測試生命周期事件。擴展模型提供了幾個接口,用于在測試執行生命周期的各個時間點對測試進行擴展:
??
擴展開發者可以自由地實現所有或部分生命周期接口。
“beforeallcallback”接口提供了一種方法用于初始化擴展并在調用junit測試容器中的測試用例之前添加自定義邏輯。我們的storyextension類將實現這個接口,以確保給定的測試類使用了“@story”注解。
1
2
3
4
5
6
7
8
9
10
11
12
|
import org.junit.jupiter.api.extension.beforeallcallback; public class storyextension implements beforeallcallback { @override public void beforeall(extensioncontext context) throws exception { if (!annotationsupport .isannotated(context.getrequiredtestclass(), story. class )) { throw new exception(“use @story annotation...“); } } } |
jupiter引擎將提供一個用于運行擴展的執行上下文。我們使用這個上下文來確定正在執行的測試類是否使用了“@story”注解。我們使用junit平臺提供的annotationsupport輔助類來檢查是否存在這個注解。
回想一下,我們的自定義擴展在執行測試后會生成bdd報告。這些報告的某些部分是從“@store”注解的元素中提取的。我們使用beforeall回調來保存這些字符串。稍后,在執行生命周期結束時,再基于這些字符串生成報告。我們使用了一個簡單的pojo。我們將這個類命名為“storyde??tails”。以下代碼片段演示了創建這個類實例的過程,并將注解元素保存到實例中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class storyextension implements beforeallcallback { @override public void beforeall(extensioncontext context) throws exception { class <?> clazz = context.getrequiredtestclass(); story story = clazz.getannotation(story. class ); storydetails storydetails = new storydetails() .setname(story.name()) .setdescription(story.description()) .setclassname(clazz.getname()); context.getstore(namespace).put(clazz.getname(), storydetails); } } |
我們需要解釋一下方法的最后一個語句。我們實際上是從執行上下文中獲取一個帶有名字的存儲,并將新創建的“storyde??tails”實例保存到這個存儲中。
自定義擴展可以使用存儲來保存和獲取任意數據——基本上就是一個存在于內存中的map。為了避免多個擴展之間出現意外的key沖突,junit引入了命名空間的概念。命名空間是一種對不同擴展保存的數據進行隔離的方法。用于隔離擴展數據的一種常用方法是使用自定義擴展類名:
1
2
|
private static final namespace namespace = namespace .create(storyextension. class ); |
我們的擴展需要用到的另一個自定義注解是“@scenario”注解。這個注解用于將測試方法標記為故事中的場景或行為。我們的擴展將解析這些場景,以便將它們作為junit測試用例來執行并生成報告。回想一下我們之前看到的生命周期圖中的“beforeeachcallback”接口,在調用每個測試方法之前,我們將使用回調來添加附加邏輯:
1
2
3
4
5
6
7
8
9
10
11
|
import org.junit.jupiter.api.extension.beforeeachcallback; public class storyextension implements beforeeachcallback { @override public void beforeeach(extensioncontext context) throws exception { if (!annotationsupport. isannotated(context.getrequiredtestmethod(), scenario. class )) { throw new exception(“use @scenario annotation...“); } } } |
如前所述,jupiter引擎將提供一個用于運行擴展的執行上下文。我們使用上下文來確定正在執行的測試方法是否使用了“@scenario”注解。
回到本文的開頭,我們提供了一個故事的示例代碼,我們的自定義擴展負責將“scene”類的實例注入到每個測試方法中。scene類讓測試用例編寫者能夠使用“given”、“then”和“when”等步驟來定義場景(行為)。scene類是我們自定義擴展的中心單元,它包含了特定于測試方法的狀態信息。狀態信息可以在場景的各個步驟之間傳遞。我們使用“beforeeachcallback”接口在調用測試方法之前準備一個scene實例:如前所述,jupiter引擎將提供一個用于運行擴展執行上下文。我們使用上下文來確定正在執行的測試方法是否使用了“@scenario”注解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class storyextension implements beforeeachcallback { @override public void beforeeach(extensioncontext context) throws exception { scene scene = new scene() .setdescription(getvalue(context, scenario. class )); class <?> clazz = context.getrequiredtestclass(); storydetails details = context.getstore(namespace) .get(clazz.getname(), storydetails. class ); details.put(scene.getmethodname(), scene); } } |
上面的代碼與我們在“beforeallcallback”接口方法中所做的非常相似。
動態參數解析
現在我們還缺少一個東西,即如何將場景實例注入到測試方法中。jupiter的擴展模型為我們提供了一個“parameterresolver”接口。這個接口為測試引擎提供了一種方法,用于識別希望在測試執行期間動態注入參數的擴展。我們需要實現這個接口的兩個方法,以便注入我們的場景實例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import org.junit.jupiter.api.extension.parameterresolver; public class storyextension implements parameterresolver { @override public boolean supportsparameter(parametercontext parametercontext, extensioncontext extensioncontext) { parameter parameter = parametercontext.getparameter(); return scene. class .equals(parameter.gettype()); } @override public object resolveparameter(parametercontext parametercontext, extensioncontext extensioncontext) { class <?> clazz = extensioncontext.getrequiredtestclass(); storydetails details = extensioncontext.getstore(namespace) .get(clazz.getname(), storydetails. class ); return details.get(extensioncontext .getrequiredtestmethod().getname()); } } |
上面的第一個方法告訴jupiter我們的自定義擴展是否可以注入測試方法所需的參數。
在第二個方法“resolveparameter()”中,我們從執行上下文的存儲中獲取storyde??tails實例,然后從storydetails實例中獲取先前為給定測試方法創建的場景實例,并將其傳給測試引擎。測試引擎將這個場景實例注入到測試方法中并執行測試。請注意,僅當“supportsparameter()”方法返回true值時才會調用“resolveparameter()”方法。
最后,為了在執行完所有故事和場景后生成報告,自定義擴展實現了“afterallcallback”接口:
1
2
3
4
5
6
7
8
9
|
import org.junit.jupiter.api.extension.afterallcallback; public class storyextension implements afterallcallback { @override public void afterall(extensioncontext context) throws exception { new storywriter(getstorydetails(context)).write(); } } |
“storywriter”是一個自定義類,可生成報告并將其保存到json或文本文件中。
現在,讓我們看看如何使用這個自定義擴展來編寫bdd風格的測試用例。gradle 4.6及更高版本支持使用junit 5運行單元測試。你可以使用build.gradle文件來配置junit 5。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
dependencies { testcompile group: “ud.junit.bdd”, name: “bdd-junit”, version: “ 0.0 . 1 -snapshot” testcompile group: “org.junit.jupiter”, name: “junit-jupiter-api”, version: “ 5.2 . 0 " testruntime group: “org.junit.jupiter”, name: “junit-jupiter-engine”, version: “ 5.2 . 0 ” } test { usejunitplatform() } |
如你所見,我們通過“usejunitplatform()”方法要求gradle使用junit 5。然后我們就可以使用storyextension類來編寫測試用例。這是本文開頭給出的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import org.junit.jupiter.api.extension.extendwith; import ud.junit.bdd.ext.scenario; import ud.junit.bdd.ext.story; import ud.junit.bdd.ext.storyextension; @extendwith (storyextension. class ) @story (name=“returns go back to the stockpile”, description=“...“) public class storefronttest { @scenario (“refunded items should be returned to the stockpile”) public void refundeditemsshouldberestocked(scene scene) { scene .given(“customer bought a blue sweater”, () -> buysweater(scene, “blue”)) .and(“i have three blue sweaters in stock”, () -> assertequals( 3 , sweatercount(scene, “blue”), “store should carry 3 blue sweaters”)) .when(“the customer returns the blue sweater for a refund”, () -> refund(scene, 1 , “blue”)) .then(“i should have four blue sweaters in stock”, () -> assertequals( 4 , sweatercount(scene, “blue”), “store should carry 4 blue sweaters”)) .run(); } } |
我們可以通過“gradle testclasses”來運行測試,或者使用其他支持junit 5的ide。除了常規的測試報告外,自定義擴展還為所有測試類生成bdd文檔。
結論
我們描述了junit 5擴展模型以及如何利用它來創建自定義擴展。我們設計并實現了一個自定義擴展,測試用例編寫者可以使用它來創建和執行故事。讀者可以從github上獲取代碼,并研究如何使用jupiter擴展模型及其api來實現自定義擴展。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:http://www.infoq.com/cn/articles/deep-dive-junit5-extensions