前言
很久很久很久很久....沒有寫技術向的文章了,今天呢就來寫一篇關於單元測試的文章把。寫這篇文章的原因呢是因為幾個月前看到隔壁組的同事寫的單元測試,當場就被驚艷了。第一次發現原來單元測試還可以這么寫,相比之下我以前寫的那堆測試簡直是惡心自己用的。於是好好研究了一番他們項目組使用的JUnit5,總結了一些新特性,希望有天自己也能寫出讓人驚艷的代碼吧。
JUnit5介紹
JUnit作為目前Java領域內最為流行的單元測試框架已經走過了數十年。而JUnit5在JUnit4停止更新版本的3年后終於也於2017年發布了。
作為最新版本的JUnit框架,JUnit5與之前版本的Junit框架有很大的不同。首先Junit5由來自三個不同子項目的幾個不同模塊組成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: Junit Platform是在JVM上啟動測試框架的基礎,不僅支持Junit自制的測試引擎,其他測試引擎也都可以接入。
JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的編程模型,是JUnit5新特性的核心。內部 包含了一個測試引擎,用於在Junit Platform上運行。
JUnit Vintage: 由於JUint已經發展多年,為了照顧老的項目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的測試引擎。
通過上述的介紹,不知道有沒有發現JUint5似乎已經不再滿足於安安靜靜做一個單元測試框架了,它的野心很大,想通過接入不同測試引擎,來支持各類測試框架的使用,成為一個單元測試的平台。因此它也采用了分層的架構,分成了平台層,引擎層,框架層。下圖可以很清晰的體現出來:
只要實現了JUnit的測試引擎接口,任何測試框架都可以在JUnit Platform上運行,這代表着JUnit5將會有着很強的拓展性。
開啟一個JUnit5項目吧!
引入JUnit5相比之前的測試框架要相對復雜,需要引入3個模塊的jar包。由於目前項目都是由gradle構建的,因此以下的配置皆為gradle配置。
testCompile("org.junit.platform:junit-platform-launcher:1.6.0") testCompile("org.junit.jupiter:junit-jupiter-engine:5.6.0") testCompile("org.junit.vintage:junit-vintage-engine:5.6.0")
當然,你可能覺得引那么多包太復雜了,沒關系,最新版本的Junit5已經考慮到這點了。現在只需要引入以下一個包即可
testCompile("org.junit.jupiter:junit-jupiter:5.6.0")
哦,對了。別忘了我們的JUnit5是運行在JUnit Platform上的,所以還需要在build.gradle中加上這個。(都是踩過的坑....)
test { useJUnitPlatform() }
引入JUnit5后即可開啟第一個單元測試了。注意@Test注解使用的是org.junit.jupiter.api.Test包下的,不要再用成Junit4版本的了。
import org.junit.jupiter.api.Test; //注意這里使用的是jupiter的Test注解!! public class TestDemo { @Test @DisplayName("第一次測試") public void firstTest() { System.out.println("hello world"); }
基本注解
JUnit5的注解與JUnit4的注解有所變化,以下列出的注解為部分我覺得常用的注解
@Test :表示方法是測試方法。但是與JUnit4的@Test不同,他的職責非常單一不能聲明任何屬性,拓展的測試將會由Jupiter提供額外測試
@ParameterizedTest :表示方法是參數化測試,下方會有詳細介紹
@RepeatedTest :表示方法可重復執行,下方會有詳細介紹
@DisplayName :為測試類或者測試方法設置展示名稱
@BeforeEach :表示在每個單元測試之前執行
@AfterEach :表示在每個單元測試之后執行
@BeforeAll :表示在所有單元測試之前執行
@AfterAll :表示在所有單元測試之后執行
@Tag :表示單元測試類別,類似於JUnit4中的@Categories
@Disabled :表示測試類或測試方法不執行,類似於JUnit4中的@Ignore
@Timeout :表示測試方法運行如果超過了指定時間將會返回錯誤
@ExtendWith :為測試類或測試方法提供擴展類引用
新的特性
更強大的斷言
JUnit5使用了新的斷言類:org.junit.jupiter.api.Assertions。相比之前的Assert斷言類多了許多新的功能,並且大量方法支持Java8的Lambda表達式。
以下為兩個與JUnit4不太一樣的斷言方式:
1. 異常斷言
在JUnit4時期,想要測試方法的異常情況時,需要用@Rule注解的ExpectedException變量還是比較麻煩的。而JUnit5提供了一種新的斷言方式Assertions.assertThrows() ,配合函數式編程就可以進行使用。
@Test @DisplayName("異常測試") public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出斷言異常 ArithmeticException.class, () -> System.out.println(1 % 0)); }
2. 超時斷言
Junit5還提供了Assertions.assertTimeout() 為測試方法設置了超時時間
@Test @DisplayName("超時測試") public void timeoutTest() { //如果測試方法時間超過1s將會異常 Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500)); }
參數化測試
參數化測試是JUnit5很重要的一個新特性,也是我認為JUnit5最驚艷到我的一個功能。它使得用不同的參數多次運行測試成為了可能,也為我們的單元測試帶來許多便利。
基礎用法
利用@ValueSource等注解,指定入參,我們將可以使用不同的參數進行多次單元測試,而不需要每新增一個參數就新增一個單元測試,省去了很多冗余代碼。
@ValueSource: 為參數化測試指定入參來源,支持八大基礎類以及String類型,Class類型
@NullSource: 表示為參數化測試提供一個null的入參
@EnumSource: 表示為參數化測試提供一個枚舉入參
@ParameterizedTest @ValueSource(strings = {"one", "two", "three"}) @DisplayName("參數化測試1") public void parameterizedTest1(String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); }
進階用法
當然如果參數化測試僅僅只能做到指定普通的入參還達不到讓我覺得驚艷的地步。讓我真正感到他的強大之處的地方在於他可以支持外部的各類入參。如:CSV,YML,JSON 文件甚至方法的返回值也可以作為入參。只需要去實現ArgumentsProvider接口,任何外部文件都可以作為它的入參。
@CsvFileSource:表示讀取指定CSV文件內容作為參數化測試入參
@MethodSource:表示讀取指定方法的返回值作為參數化測試入參(注意方法返回需要是一個流)
/** * csv文件內容: * shawn,24 * uzi,50 */ @ParameterizedTest @CsvFileSource(resources = "/test.csv") //指定csv文件位置 @DisplayName("參數化測試-csv文件") public void parameterizedTest2(String name, Integer age) { System.out.println("name:" + name + ",age:" + age); Assertions.assertNotNull(name); Assertions.assertNotNull(age); } @ParameterizedTest @MethodSource("method") //指定方法名 @DisplayName("方法來源參數") public void testWithExplicitLocalMethodSource(String name) { System.out.println(name); Assertions.assertNotNull(name); } static Stream<String> method() { return Stream.of("apple", "banana"); }
為什么要用參數化測試?
介紹完參數化測試,可能會覺得這個功能確實強大但是似乎應用場景並不多呀,畢竟我們寫的大部分單元測試都不會去復用。其實不然,從外部文件讀取入參有一個很大的好處。沒錯,就是解耦,使得測試邏輯與測試參數解耦,未來如果測試的參數改變了,我們不需要去修改測試代碼,只需要修改對應的文件。讓我們的測試邏輯不會被大量構造測試參數的代碼干擾,能更專注於寫好測試邏輯。
目前我們公司也有封裝自己的@FileSource注解,用於從外部Json或Yaml文件中讀取入參。單元測試代碼的邏輯會非常清晰。相比於之前要自己用代碼mock許多測試參數,使用這種方式可以讓測試代碼邏輯更加清晰。
@DisplayName("創建咨詢來源:成功創建") @ParameterizedTest @FileSource(resources = "testData/consultconfig_app_service.yaml") public void testSaveConsultSource_SUCCESS(ConsultConfigAppServiceDTO dto) { //獲取入參 ConsultSourceSaveDTO saveDTO = dto.getSaveConsultSourceSuccess(); //調用測試方法 ConsultSourceBO consultSourceBO = consultConfigAppService.saveConsultSource(saveDTO); //驗證 Assertions.assertEquals("testConsultSourceCmd", consultSourceBO.getConsultChannelName()); }
內嵌單元測試
JUnit5提供了嵌套單元測試用於更好表示各個單元測試類之間的關系。平時我們寫單元測試時一般都是一個類對應一個單元測試類。不過有些互相之間有業務關系的類,他們的單元測試完全是可以寫在一起,使用內嵌的方式表示,減少測試類的數量防止類爆炸。
JUnit 5提供了@Nested 注解,能夠以靜態內部成員類的形式對測試用例類進行邏輯分組
public class NestedTestDemo { @Test @DisplayName("Nested") void isInstantiatedWithNew() { System.out.println("最一層--內嵌單元測試"); } @Nested @DisplayName("Nested2") class Nested2 { @BeforeEach void Nested2_init() { System.out.println("Nested2_init"); } @Test void Nested2_test() { System.out.println("第二層-內嵌單元測試"); } @Nested @DisplayName("Nested3") class Nested3 { @BeforeEach void Nested3_init() { System.out.println("Nested3_init"); } @Test void Nested3_test() { System.out.println("第三層-內嵌單元測試"); } } } }
重復測試
JUnit5為提供了@RepeatedTest注解,允許某個單元測試執行多次。其實現在我也並不是很理解為什么要將一個單元測試運行多遍。目前我個人理解是因為單元測試是需要有可重復執行性的,而多次運行單元測試可以更加保證測試的准確性,防止一些隨機性。
@RepeatedTest(10) //表示重復執行10次 @DisplayName("重復測試") public void testRepeated() { Assertions.assertTrue(1 == 1); }
動態測試
JUnit5允許我們動態的創建單元測試,通過@TestFactory注解,會在運行時生成單元測試。需要注意的是@TestFactory修飾的方法本身並不是單元測試,他只是負責生成單元測試。我們只需要返回 DynamicTest的迭代器甚至是流即可生成不同的單元測試。
(動態測試的應用場景我感覺比較少,各位如果有想到的話,望補充)
@TestFactory @DisplayName("動態測試") Iterator<DynamicTest> dynamicTests() { return Arrays.asList( dynamicTest("第一個動態測試", () -> assertTrue(true)), dynamicTest("第二個動態測試", () -> assertEquals(4, 2 * 2)) ).iterator(); }
結語
以上就是對JUnit5的基本介紹。單元測試是軟件開發中一個非常重要的環節,好的單元測試可以讓系統的質量可維護性大大增加。而JUnit5提供了很多新特性方便我們對於單元測試的編寫,如果你的項目現在還在用JUnit4,不妨嘗試一下JUnit5,說不定就能打開一個新世界的大門呢?
了解更多資料,微信搜索「摩卡技術站」關注我吧~更多干貨持續更新!