一. 基本介紹
junit是Java用戶寫單元測試用到最多的一種技術,通過一些註解讓我們的多個測試用例跑起來,從而檢測代碼的正確性,這裏我們主要介紹一下junit5。
- 用途:Junit一般用來驗證獨立功能的業務邏輯,比如工具方法等
- 官網地址:【junit5官網】
- 官方文檔:【junit5官方文檔】
二. api
junit5提供了很多好用的註解,下面只列出最重要的幾個註解,如果想看所有註解的話,還是去官網比較好,這裏我們只對常用的一些做一下介紹。
1. 常用註解
- @Test:標識只是一個測試用例
- @BeforeEach:在每個測試方法執行執行,總會調用這個方法,一般用於初始化某些數據
- @AfterEach:在每個測試方法執行之後,總會調用這個方法,一般用於釋放資源
- @Disabled:忽略測試用例,讓相應的測試用例不運行,用在方法上或者類上
- @BeforeAll 和@AfterAll : 和上面的@BeforeEach和@AfterEach非常類似,區別在於,這兩個方法必須標註在靜態方法上面
2. 高級註解
- @Nested:內嵌測試註解,用於把一組測試歸納起來;
- @RepeatedTest:重複多次測試註解
- @ParameterizedTest:帶參數的註解
所有註解請猛戳這裏:【Junit5註解】,這些註解,都放在了源碼的org.junit.jupiter.api
包下面。
3. 斷言Assertions
準備好測試實例、執行了被測類的方法以後,我們需要判斷邏輯是否正確,斷言用於根據咱們的邏輯來斷定會發生什麼,確保你得到了想要的結果,Juint5給我們提供了很多的斷言方法,這些方法都在org.junit.jupiter.api.Assertions
類中,作用跟方法名一毛一樣,一眼就能看出來,如果你不知道咋用,請戳這裏:【Junit5斷言】
- assert關鍵字:可以用來斷定一些簡單的邏輯,如:
assert "hello".length()==5
;個人建議如果有別的可用的時候,先不要用這個,看上去不是很明白具體的意思; - assertEquals:斷言結果相等,如果不等,則不通過,有很多對的重載方法;
- assertNotNull:斷言不爲空,
- assertThrows:斷言拋出異常
- assertTimeout:斷言超時,如果方法運行的時間超過了指定的時間,就無法通過
- assertAll:進行一組斷言,如果前一個失敗了,後續不再執行。
4. 假設Assumptions
Assumptions用來做條件測試的,都在org.junit.jupiter.api.Assumptions
包下面,主要有以下幾個方法:
- assumeTrue:假設某個事情是正確的,[返回某個字符串]
- assumeFalse:假設某個情況是錯誤的,[返回某個字符串]
- assumingThat:假設某個表達式是正確時候,執行某個操作
5. 第三方包
junit5集成了一些第三方的包,如:AssertJ, Hamcrest, Truth等,有興趣的同學可以自行學習。
例如:在junit5中,移除了junit4中的assertThat
斷言,我們可以使用Hamcrest Matcher
來進行替代:
import static org.hamcrest.MatcherAssert.assertThat;
....
assertThat(....);
三、使用案例
(一)maven依賴
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
如果是springboot項目,如下引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
(二)基本使用
1. 創建測試類
比如我這裏有一個工具類如下:
package com.firewolf.busi.example;
/**
* Hello工具類
*/
public class HelloUtils {
/**
* 打招呼
*
* @param name 人名
* @return
*/
public String sayHello(String name) {
return "hello," + name;
}
/**
* 打招呼,自己傳入前綴
*
* @param name 姓名
* @param prefix 前綴
* @return
*/
public String sayHelloWithPrefix(String name, String prefix) {
return prefix + "," + name;
}
}
我們可以自行創建測試用例類,也可以利用Idea的工具來生成測試用例類,我們只需要在所在類的編輯窗口:右鍵->Generate->Test,就會出現下面的界面:
在這個界面,我們可以自己選擇使用的測試類庫,所在的包,已經要被測試的方法,我一般只會注意上面的類庫是否正確,其他的保持不變;
生成的測試類如下:
package com.firewolf.busi.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class HelloUtilsTest {
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}
@Test
void sayHello() {
}
@Test
void sayHelloWithPrefix() {
}
}
當然,這時候的測試類沒有任何測試邏輯
2. 編寫測試代碼
要測試我們的邏輯是否正確,我們需要自己進行編寫,編寫過程用到上面提到的api,例如:
package com.firewolf.busi.example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import java.util.Arrays;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.*;
class HelloUtilsTest {
private HelloUtils helloUtils;
@BeforeEach
void setUp() {
helloUtils = new HelloUtils();
}
@AfterEach
void tearDown() {
helloUtils = null;
}
/**************** 斷言 ***************/
@Test
void sayHello() {
assertEquals(helloUtils.sayHello("liuxing"), "hello,liuxing");
}
@Test
void exceptionTest() {
assertThrows(ArithmeticException.class, () -> {
int a = 1 / 0;
});
}
@Test
void timeOutTest() {
assertTimeout(Duration.ofSeconds(1), () -> Thread.sleep(2000));
}
@Test
void sayHelloWithPrefix() {
Exception ex = assertThrows(NullPointerException.class, () -> helloUtils.sayHelloWithPrefix(null, "welcome"));
assertNull(ex.getMessage());
assertAll("helloWithPrefix",
() -> assertEquals(helloUtils.sayHelloWithPrefix("liuxing", "hello"), "hello,liuxing"),
() -> assertThrows(NullPointerException.class, () -> helloUtils.sayHelloWithPrefix(null, "welcome"))
);
}
/************** 第三方jar *****************/
@Test
void testThirdLib() {
assertThat("hello".length(), is(5));
assertThat("hello", isA(String.class));
assertThat(Arrays.asList(1, 2, 3), hasItem(1));
}
/**************** 假設 ***************/
@Test
void testAssumptions() {
assumeTrue("hello".startsWith("h"));
assumeFalse(() -> "hello".endsWith("o"), () -> "hello end with o");
System.setProperty("env", "dev");
assumingThat(System.getProperty("env") != null, () -> {
System.out.println("exec test");
assertEquals(System.getProperty("env").length(), 4);
});
}
}
測試用例需要儘量多的覆蓋一些場景,不要只是傳入一些常規參數,需要多考慮邊界條件,比如,我在sayHelloWithPrefix的測試方法中,傳入了null,後面就發現了HelloUtil裏面缺少了工具處理。
3. 運行測試用例
有以及幾種情況:
- 跑單個測試方法:直接點擊方法前面的綠色三角、 右鍵方法名->debug/run
- 跑單個測試類:點擊類上面的綠色三角 、右鍵類的空白處->debug/run、 右鍵方法名->debug/run
- 運行某個包下面的測試用例:右鍵包名-> debug/run
- 運行所有測試用例:右鍵test下面的Java文件夾、點擊maven生命週期的test、進入項目根目錄->mvn test
run和debug的區別在於debug的話會進行調試,一般我們會在出錯之後這麼去找錯誤;
4. 查看測試結果
- 某個類或者某個方法的測試結果:我們可以通過idea的運行結果來查看我們的代碼是否達到了我們想要的目標,如:
只有方法前面標識了綠色對勾的時候,纔是正確的。 - 整個工程的測試通過情況
當我們使用mvn test 或者用maven插件執行的時候,可以明確看到那些測試用例報錯了
5. 跳過測試用例
我們的項目終究是要以jar或者其他的形式提供出去的,這個步驟對應着maven生命週期的deploy,而maven生命週期中,test位於depoy之前,也就是說,在我們deploy的時候,會先跑測試用例,如果測試用例耗時較多,那麼這個過程會比較慢,此時我們可以通過以下幾種方式跳過測試用例:
- . 跳過全部測試用例
- maven命令後面加上-Dmaven.test.skip=true,如:mvn clean install -Dmaven.test.skip=true
- maven插件添加如下配置:
<configuration> <skip>true</skip> </configuration>
- 跳過部分測試用例
- 在類或者非方法上面添加@Disabled
(三)重複執行
有時候我們需要一個方法多執行幾次才能達到我們測試的目的,比如定時任務等。
我們可以使用@RepeatedTest來完成這個功能
1. 註解屬性
- value:重複次數,必填
- name:這次執行的名字,裏面可以用到三個定義好的佔位符:
- {displayName}:測試的展示名,如果方法上面使用了@DisplayName註解的話,就會是這個註解裏面的值,否則顯示爲方法名
- {currentRepetition} :當前執行的次數
- {totalRepetitions}:共次數
2. 示例代碼
package com.firewolf.busi.example;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.TestInfo;
class RepeatTestDriver {
@DisplayName("repeat test")
@RepeatedTest(value = 4, name = "執行測試: {displayName}, 第 {currentRepetition} / {totalRepetitions} 次! ")
void testRepeat(TestInfo testInfo) {
assert !testInfo.getDisplayName().contains("3");
}
}
(四)傳遞參數
有時候我們希望給測試用例傳入我們需要的參數,這個時候,我們可以使用下面的一系列註解來完成這個事情註解來完成這個需求。
1. @ParameterizedTest
作用:標註這是一個帶參數的單元測試;
參數:
- name:每一個參數對應的測試方法名字,可以使用如下佔位符:{index}(第Index個參數)、{arguments}(當前這組參數),{0}/{1}/…(這組參數的第幾個參數)、{displayName}:方法名
這個註解需要配合下面的一堆註解一起使用
2. @ValueSource
作用:傳入一組參數
參數:
- ints:傳入一組整數
- …:這些參數和ints類似,可以傳入八種基本數據類型、java.lang.String類型、java.lang.Class類型。
方式爲:ints={},strings={} 等等;
示例:
@ParameterizedTest(name = "第 {index}個參數, 當前參數:{arguments}")
@ValueSource(ints = {1, 2, 3})
void testValueSourceIntParams(int param) {
assertTrue(param > 0 && param < 4);
}
3. @NullSource
作用:傳入空值null
4. @EmptySource
作用:傳入空數據
如:java.lang.String, java.util.List, java.util.Set, java.util.Map, primitive arrays (e.g., int[], char[][], etc.), object arrays (e.g.,String[], Integer[][], etc.)
.
5. @NullAndEmptySource
作用:@NullSource和@EmptySource這兩個註解的組合;
示例:
@ParameterizedTest
// 傳入三個字符串
@ValueSource(strings = {"haha", "hehe", "heihei"})
// 傳入null和""
@NullAndEmptySource
void testValueSourceStringParams(String str) {
assertEquals(str.length(), 4);
}
6. @EnumSource
作用:傳入枚舉中的值
參數:
- value:枚舉類型
- names:關心的枚舉集合。
- mode:對names的處理方式,默認是INCLUDE,也就是傳入names指定的枚舉,也可以使用EXCLUDE來排除指定的枚舉;
要求:
需要引入下面的依:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
示例:
@ParameterizedTest
@EnumSource(value = ChronoUnit.class, names = {"SECONDS", "DAYS"}, mode = EnumSource.Mode.INCLUDE)
void testEumSource(ChronoUnit unit) {
assertNotNull(unit);
}
7. @MethodSource
作用:通過方法來傳入參數
參數:
- value:提供參數方法名,如果方法是當前類的,就直接給方法名,如果是其他類的,需要使用
類的包名#方法名
形式傳入,如:com.firewolf.busi.example.ParamTestDriver#provider
要求:方法必須是靜態類型,且方法返回的必須是有個Stream類型,如:IntStream、Stream等等;
示例:
@ParameterizedTest
@MethodSource("com.firewolf.busi.example.ParamTestDriver#provider")
void testMethodParams(ChronoUnit chronoUnit) {
System.out.println(chronoUnit);
}
static Stream<ChronoUnit> provider() {
return Stream.of(ChronoUnit.HALF_DAYS, ChronoUnit.DAYS);
}
8. @CsvSource
作用:通過Csv格式傳入一組參數
參數:
- value:參數
- delimiter:各個值之間的分隔符,默認爲’,’
注意點:
- 多個單詞的參數:如果某項數據裏面包含了分隔符,那麼需要使用’'括起來;
- 傳入null:需要想傳入null,那麼這一項不寫即可;
- 如果數據項多於參數,那麼後面的會被丟棄
示例:
@ParameterizedTest
@CsvSource(value = {
"apple ; 2; heihei",
" ; 1; heihei",
"'lemon; lime'; 2; haha",
"nal; 0xF1; hehe"
}, delimiter = ';')
void testWithCsvSource(String fruit, int rank) {
System.out.println(fruit);
assertNotNull(fruit);
assertNotEquals(0, rank);
}
9. @CsvFileSource
作用:通過csv文件注入參數
參數:
- value:參數
- delimiter:各個值之間的分隔符,默認爲’,’
- resources:文件,這個文件需要放在
src/test/resources/
下面,然後文件路徑以/文件名
的形式 - lineSeparato:換行符,默認爲
\n
- encoding:文件編碼,默認爲utf-8
注意事項:如果文件中的某項數據包含了分隔符,那麼需要使用""來引用起來
示例:
@ParameterizedTest
@CsvFileSource(resources = "/test.csv", delimiter = ';')
void testWithCsvFileSource(String fruit, int rank) {
System.out.println(fruit);
assertNotNull(fruit);
assertNotEquals(0, rank);
}
10. ArgumentsAccessor
我們可以把傳入的參數轉成我們需要的類型,然後作爲測試用例的參數傳入方法。主要有兩種方式:
10.1 ArgumentsAccessor
給測試用例傳入一個ArgumentsAccessor類型的參數,然後在方法裏面進行封裝,
如:
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, String.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals("F", person.getGender());
} else {
assertEquals("M", person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
10.2 自定義 ArgumentsAccessor
我們可以實現自己的ArgumentsAccessor,這個類需要實現接口ArgumentsAggregator ;然後使用@AggregateWith註解來指定我們自己定義的轉換器,
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor2(@AggregateWith(PersonArgumentsAccessor.class) Person person) {
if (person.getFirstName().equals("Jane")) {
assertEquals("F", person.getGender());
} else {
assertEquals("M", person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
class PersonArgumentsAccessor implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext parameterContext) throws ArgumentsAggregationException {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, String.class),
arguments.get(3, LocalDate.class));
return person;
}
}