文章目錄
一.什麼是單元測試呢?
- 單元測試就是針對
最小的功能單元編寫測試代碼
。Java程序最小的功能單元是方法
,對Java程序進行單元測試就是針對單個Java方法的測試
。
二.測試驅動開發(TDD)
- 測試驅動開發就是指先編寫接口,緊接着編寫測試。編寫完測試後,我們纔開始真正編寫實現代碼。在編寫實現代碼的過程中,一邊寫,一邊測,什麼時候測試全部通過了,那就表示編寫的實現完成了.
這就是測試驅動開發TDD(Test-Driven Development)
。是敏捷開發
中的一項核心實踐和技術。
當然,這是一種理想情況
。大部分情況是我們已經編寫了實現代碼,需要對已有的代碼進行測試。
三.JUnit框架
1.爲什麼需要JUnit框架?
一般情況下我們是用一個main()方法
在Main方法裏面編寫測試代碼,但使用main()方法測試有很多缺點:
- 一是一個類只能有一個main()方法,
不能把測試代碼分離
是沒有打印出測試結果和期望結果
,例如,expected: 3628800, but actual: 123456是很難編寫一組通用的測試代碼。
因此我們可以使用JUnit框架進行單元測試
2.什麼是JUnit框架?
-
JUnit
是一個開源
的Java語言的單元測試標準框架
,專門針對Java設計
,使用最廣泛
。 -
使用JUnit編寫單元測試的好處在於: 可以非常簡單地組織測試代碼,隨時運行它們,JUnit就會給出
成功的測試和失敗的測試
,還可以生成測試報告
,不僅包含測試的成功率
,還可以統計測試的代碼覆蓋率
,即被測試的代碼本身有多少經過了測試 。對於高質量的代碼來說,測試覆蓋率應該在80%以上
。 -
此外,
幾乎所有的Java開發工具都集成了JUnit(如Eclipse,IDEA)
,這樣我們就可以直接在IDE中編寫並運行JUnit測試。JUnit目前最新版本是JUnit5
。
JUnit 5 這個版本,主要特性
- 提供全新的斷言和測試註解,支持測試類內嵌
- 更豐富的測試方式:支持動態測試,重複測試,參數化測試等
- 實現了模塊化,讓測試執行和測試發現等不同模塊解耦,減少依賴
- 提供對 Java 8 的支持,如 Lambda 表達式,Sream API等。
3,單元測試的好處
-
單元測試可以
確保單個方法按照正確預期運行
,如果修改了某個方法的代碼,只需確保其對應的單元測試通過,即可認爲改動正確。此外,測試代碼本身就可以作爲示例代碼,用來演示如何調用該方法。
-
使用JUnit進行單元測試,我們可以使用
斷言(Assert)
來測試期望結果,可以方便地組織和運行測試,並方便地查看測試結果。
在編寫單元測試的時候,我們要遵循一定的規範
:
-
單元測試代碼本身必須非常簡單,能一下看明白,決不能再爲
測試代碼編寫測試
; -
每個單元測試應當
互相獨立
,不依賴運行的順序
; -
測試時不但要覆蓋常用測試用例,還要特別注意測試邊界條件,例如
輸入爲0,null,空字符串""
等情況。
四.使用Junit5框架
引入Junit5框架
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
1.@Test/@DisplayName/@Tag
在方法上加上@Test註解,JUnit會把帶有@Test的方法識別爲測試方法
- @DisplayName: 測試類或方法的顯示名稱
- @Tag : 爲測試類或方法添加標籤
@Test
@DisplayName("測試方法")
@Tag("標籤")
public void testJunit() {
System.out.println("HelloWorld");
}
2. 斷言方法
Assert.assertEquals(expected, actual)是最常用的測試方法,它在Assertions類
中定義。Assertions
還定義了其他斷言方法,例如:
-
assertEquals(expected, actual):查看兩個對象是否相等。類似於字符串比較使用的equals()方法;
-
assertNotEquals(first, second):查看兩個對象是否不相等。
-
assertNull(object):查看對象是否爲空。
-
assertNotNull(object):查看對象是否不爲空
-
assertSame(expected, actual):查看兩個對象的引用是否相等,類似於使用“==”比較兩個對象;
-
assertNotSame(unexpected, actual):查看兩個對象的引用是否不相等,類似於使用“!=”比較兩個對象。
-
assertTrue(String message, boolean condition) 要求condition == true,查看運行的結果是否爲true;
-
assertFalse(String message, boolean condition) 要求condition == false,查看運行的結果是否爲false。
-
assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual),即查看兩個數組是否相等。
-
assertThat(String reason, T actual, Matcher matcher) :要求matcher.matches(actual) == true,使用Matcher做自定義的校驗。
-
fail:能使測試立即失敗,這種斷言通常用於標記某個不應該被到達的分支。通常用於測試在應該拋出異常的時候確實會拋出異常。
實例代碼
public class Factorial {
public static long fact(long n) {
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
@Test
public void testAssert() {
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(3628800, Factorial.fact(10));
assertEquals(2432902008176640000L, Factorial.fact(20));
}
}
測試通過
如果測試結果與預期不符,assertEquals()會拋出異常: 預計返回1111,實際返回1
3. 使用Fixture
3.1@BeforeEach/@AfterEach
-
在一個單元測試中,我們經常
編寫多個@Test方法
,來分組
、分類
對目標代碼進行測試。 -
在測試的時候,我們經常遇到·
一個對象需要初始化,測試完可能還需要清理的情況
。· 如果每個@Test方法都寫一遍這樣的重複代碼,顯然比較麻煩。
-
JUnit提供處理測試前準備,和測試後清理的公共代碼,我們稱之爲
Fixture
。
使用當前這個類必須先實例化Calculator 對象,才能調用相關的方法,我們不必在每個測試方法中都創建Calculator 對象
通過@BeforeEach來初始化Calculator ,通過@AfterEach來回收Calculator
方法 | 描述 |
---|---|
@BeforeEach | 執行測試方法前調用 |
@AfterEach | 執行測試方法後調用 |
public class Calculator {
private long n = 0;
public long add(long x) {
n = n + x;
return n;
}
public long sub(long x) {
n = n - x;
return n;
}
}
修改後的代碼
public class CalculatorTest {
Calculator calculator;
//執行測試方法前調用
@BeforeEach
public void setUp() {
this.calculator = new Calculator();
}
//執行測試方法後調用
@AfterEach
public void tearDown() {
this.calculator = null;
}
@Test
void testAdd() {
assertEquals(100, this.calculator.add(100));
assertEquals(150, this.calculator.add(50));
assertEquals(130, this.calculator.add(-20));
}
@Test
void testSub() {
assertEquals(-100, this.calculator.sub(100));
assertEquals(-150, this.calculator.sub(50));
assertEquals(-130, this.calculator.sub(-20));
}
}
3.2 @BeforeAll/@AfterAll
方法 | 描述 |
---|---|
@BeforeAll | 執行所有@Test測試方法前調用一次,只能標註在靜態方法 上面 |
@AfterAll | 執行所有@Test測試方法後調用 一次,只能標註在靜態方法 上面 |
某些資源初始化和清理會會耗費較長的時間,全局只需要初始化和清理一次即可時
,例如初始化數據庫。JUnit還提供了@BeforeAll和@AfterAll,它們在運行所有@Test前後運行
:
public class DatabaseTest {
static Database db;
//初始化數據庫
@BeforeAll
public static void initDatabase() {
db = createDb(...);
}
//關閉數據庫
@AfterAll
public static void closeDatabase() {
//...
}
}
3.3.執行順序
@DisplayName("我的第一個測試用例")
public class MyFirstTestCaseTest {
@BeforeAll
public static void init() {
System.out.println("初始化數據");
}
@AfterAll
public static void cleanup() {
System.out.println("清理數據");
}
@BeforeEach
public void tearup() {
System.out.println("當前測試方法開始");
}
@AfterEach
public void tearDown() {
System.out.println("當前測試方法結束");
}
@DisplayName("我的第一個測試")
@Test
void testFirstTest() {
System.out.println("我的第一個測試開始測試");
}
@DisplayName("我的第二個測試")
@Test
void testSecondTest() {
System.out.println("我的第二個測試開始測試");
}
}
3.4使用Fixture小結
因此,我們總結出編寫Fixture的套路如下:
-
對於
實例變量
,在@BeforeEach中初始化
,在@AfterEach中清理
,它們在各個@Test方法中互不影響,因爲是不同的實例 -
對於
靜態變量
,在@BeforeAll中初始化
,在@AfterAll中清理
,它們在各個@Test方法中均是唯一實例,會影響各個@Test方法
大多數情況下,使用@BeforeEach和@AfterEach就足夠了。
只有某些測試資源初始化耗費時間太長,以至於我們不得不盡量“複用”
時纔會用到@BeforeAll和@AfterAll。
- 實際上每次運行一個@Test方法前,
JUnit都會將當前方法創建一個XxxTest實例
(方法名+Test) - 因此,
每個@Test方法內部的成員變量都是獨立的
,一個@Test方法不能調用另一個@Test方法的變量。
4. 異常測試: assertThrows
我們代碼中對於帶有異常的方法通常都是使用 try-catch 方式捕獲處理,針對測試這樣帶有異常拋出的代碼,而 JUnit 5 提供方法 Assertions#assertThrows(Class, Executable) 來進行測試,第一個參數爲異常類型,第二個爲函數式接口參數,跟 Runnable 接口相似,不需要參數,也沒有返回,並且支持 Lambda表達式方式使用,具體使用方式可參考下方代碼:
@Test
void testNegative() {
assertThrows(IllegalArgumentException.class, new Executable() {
@Override
public void execute() throws Throwable {
System.out.println(1/0);
}
});
}
測試不通過
5.禁用/啓用執行測試
測試類
public class Config {
public static String getConfigFile(String filename) {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
return "C:\\" + filename;
}
if (os.contains("mac") || os.contains("linux") || os.contains("unix")) {
return "/usr/local/" + filename;
}
throw new UnsupportedOperationException();
}
}
@Disabled
:禁用當前標註單元測試
@Test
void testWindows1() {
System.out.println(Config.getConfigFile("test.ini"));
}
@Test
@Disabled
void testWindows2() {
System.out.println(Config.getConfigFile("test1111.ini"));
}
@EnabledOnOs
: 根據不同的系統啓動當前標註單元測試
@Test
@EnabledOnOs(OS.WINDOWS)
void testWindows() {
assertEquals("C:\\test.ini", Config.getConfigFile("test.ini"));
}
@Test
@EnabledOnOs({ OS.LINUX, OS.MAC })
void testLinuxAndMac() {
assertEquals("/usr/local/test.cfg", Config.getConfigFile("test.cfg"));
}
- @DisabledOnJre() :根據jre運行環境來禁用當前標註單元測試
@Test
void testWindows1() {
System.out.println(Config.getConfigFile("test.ini"));
}
@Test
@DisabledOnJre(JRE.JAVA_8)
void testWindows2() {
System.out.println(Config.getConfigFile("test1111.ini"));
}
-
@EnabledIfSystemProperty
根據操作系統判斷當前標註單元測試是否啓用 -
@EnableIf
: 可以執行任意Java語句並根據返回的boolean決定當前標註方法是否執行測試
@Test
@EnabledIf("java.time.LocalDate.now().getDayOfWeek()==java.time.DayOfWeek.SUNDAY")
void testOnlyOnSunday() {
// TODO: this test is only run on Sunday
}
當我們在JUnit中運行所有測試的時候,JUnit會給出執行的結果。在IDE中,我們能很容易地看到沒有執行的測試
6.參數化測試
-
如果待測試的方法
輸入和輸出
是一組數據: 可以把測試數據組織起來 用不同的測試數據調用相同的測試方法 -
參數化測試和普通測試稍微不同的地方在於,測試方法需要傳入至少一個參數,然後,傳入一組參數反覆運行。
-
@ValueSource
是 JUnit 5 提供的最簡單的數據參數源,支持Java 的八大基本類型和字符串
,Class,使用時賦值給註解上對應類型屬性,以數組方式傳遞
-
接收單個參數
@ParameterizedTest
@ParameterizedTest
@ValueSource(strings = { "張三","李四","王五" })
void testEquals(String str) {
System.out.println("張三".equals(str));
}
@MethodSource
:接收多個參數@MethodSource
註解,它允許我們編寫一個同名的靜態方法來提供測試參數- 如果靜態方法和測試方法的名稱不同,@MethodSource也允許指定方法名。但使用默認同名方法最方便
@ParameterizedTest
@MethodSource
void testCapitalize(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
static List<Arguments> testCapitalize() {
return List.of( // arguments:
Arguments.arguments("abc", "Abc"), //
Arguments.arguments("APPLE", "Apple"), //
Arguments.arguments("gooD", "Good"));
}
@CsvSource
: 傳入多個參數
@CsvSource
,它的每一個字符串表示一行
,一行包含的若干參數用,
分隔
@ParameterizedTest
@CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" })
void testCapitalizeCsv(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
@CsvFileSource
如果有成百上千的測試輸入,那麼,直接寫@CsvSourc
e就很不方便。這個時候,我們可以把測試數據提到一個獨立的CSV
文件中,然後標註上@CsvFileSource
@ParameterizedTest
@CsvFileSource(resources = { "/test-capitalize.csv" })
void testCapitalizeUsingCsvFile(String input, String result) {
assertEquals(result, StringUtils.capitalize(input));
}
JUnit只在classpath
中查找指定的CSV文件,因此,test-capitalize.csv
這個文件要放到test
目錄下,內容如下
7.重複性測試
@RepeatedTest : 在 JUnit 5 裏新增了對測試方法設置運行次數的支持
,允許讓測試方法進行重複運行。當要運行一個測試方法 N次時,可以使用 @RepeatedTest 標記它
@DisplayName("重複測試")
@RepeatedTest(value = 3)
public void i_am_a_repeated_test() {
System.out.println("執行測試");
}
我們還可以對重複運行的測試方法名稱進行修改,利用 @RepeatedTest 提供的內置變量,以佔位符方式在其name
屬性上使用,
@DisplayName("自定義名稱重複測試")
@RepeatedTest(value = 3, name = "{displayName} 第 {currentRepetition} 次")
public void i_am_a_repeated_test_2() {
System.out.println("執行測試");
}
@RepeatedTes
t 註解內用currentRepetition
變量表示已經重複的次數
,totalRepetitions
變量表示總共要重複的次數
,displayName
變量表示測試方法顯示名稱
,我們直接就可以使用這些內置的變量來重新定義測試方法重複運行時的名稱。
8.超時操作的測試:assertTimeoutPreemptively
當我們希望測試耗時方法的執行時間,並不想讓測試方法無限地等待時,就可以對測試方法進行超時測試,JUnit 5
對此推出了斷言方法 assertTimeout
,提供了對超時的廣泛支持。
假設我們希望測試代碼在一秒內執行完畢,可以寫如下測試用例
@Test
@DisplayName("超時方法測試")
void test_should_complete_in_one_second() {
Assertions.assertTimeoutPreemptively(Duration.of(1, ChronoUnit.SECONDS), () -> Thread.sleep(2000));
}
這個測試運行失敗,因爲代碼執行將休眠兩秒鐘,而我們期望測試用例在一秒鐘之內成功。但是如果我們把休眠時間設置一秒鐘,測試仍然會出現偶爾失敗的情況,這是因爲測試方法執行過程中除了目標代碼還有額外的代碼和指令執行會耗時,所以在超時限制上無法做到對時間參數的完全精確匹配
9.內嵌測試類
@Nested :
- 當我們編寫的類和代碼逐漸增多,隨之而來的需要測試的對應測試類也會越來越多。
- 爲了解決測試類數量爆炸的問題,JUnit 5提供了
@Nested 註解
,能夠以靜態內部成員類的形式
對測試用例類進行邏輯分組
。 並且每個靜態內部類
都可以有自己的生命週期
方法, 這些方法將按從外到內層次順序執
行。 - 此外,嵌套的類也可以用@DisplayName 標記,這樣我們就可以使用正確的測試名稱。
@DisplayName("內嵌測試類")
public class NestUnitTest {
@BeforeEach
void init() {
System.out.println("測試方法執行前準備");
}
@Nested
@DisplayName("第一個內嵌測試類")
class FirstNestTest {
@Test
void test() {
System.out.println("第一個內嵌測試類執行測試");
}
}
@Nested
@DisplayName("第二個內嵌測試類")
class SecondNestTest {
@Test
void test() {
System.out.println("第二個內嵌測試類執行測試");
}
}
}