什么是單元測試
什么是軟件測試
軟件測試(英語:Software Testing),描述一種用來促進鑒定軟件的正確性、完整性、安全性和質量的過程。換句話說,軟件測試是一種實際輸出與預期輸出之間的審核或者比較過程。軟件測試的經典定義是:在規定的條件下對程序進行操作,以發現程序錯誤,衡量軟件質量,並對其是否能滿足設計要求進行評估的過程。
單元測試
什么是單元測試
單元測試(英語:Unit Testing)又稱為模塊測試,是針對程序模塊(軟件設計的最小單位)來進行正確性檢驗的測試工作。程序單元是應用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數、過程等;對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。 通常來說,程序員每修改一次程序就會進行最少一次單元測試,在編寫程序的過程中前后很可能要進行多次單元測試,以證實程序達到軟件規格書要求的工作目標,沒有程序錯誤;雖然單元測試不是什么必須的,但也不壞,這牽涉到項目管理的政策決定。
單元測試的優點
- 適應變更
單元測試允許程序員在未來重構代碼,並且確保模塊依然工作正確(復合測試)。這個過程就是為所有函數和方法編寫單元測試,一旦變更導致錯誤發生,借助於單元測試可以快速定位並修復錯誤。
- 簡化集成
單元測試消除程序單元的不可靠,采用自底向上的測試路徑。通過先測試程序部件再測試部件組裝,使集成測試變得更加簡單。
- 文檔記錄
單元測試提供了系統的一種文檔記錄。借助於查看單元測試提供的功能和單元測試中如何使用程序單元,開發人員可以直觀的理解程序單元的基礎 API。
- 表達設計
在測試驅動開發的軟件實踐中,單元測試可以取代正式的設計。每一個單元測試案例均可以視為一項類、方法和待觀察行為等設計元素。
JUnit 測試框架
JUnit 是一個開源的 Java 編程語言的單元測試框架。通過 JUnit,可以提高測試代碼編寫的速度與質量,而且 JUnit 測試可以自動運行,檢查自身結果並提供即時反饋,無需人工整理測試結果。JUnit 憑借它的優勢,在 Java 單元測試中得到廣泛使用。
JUnit 集成在許多 IDE 當中,如 Eclipse。目前 JUnit 最新版本為 JUnit5,因此本課程使用 Eclipse 中的 JUnit5 作為主要的實戰環境。
第一個單元測試
手動測試
編寫好方法后一般會測試這個方法能不能行得通,在不使用 JUnit 的情況下,一般使用如下的方法進行測試。
public class Add {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
Add add = new Add();
if (add.add(1, 1) == 2) {
System.out.println("Test pass");
} else {
System.out.println("Test fail");
}
}
}
手動測試需要新建一個實例,並且調用對應的方法,然后對結果進行比較判斷,最后輸出測試結果。
使用 JUnit 進行測試
創建一個 JUnit 測試類 AddTest.java,具體操作為:首先選擇 src 目錄,在 Eclipse 頂部菜單選擇 File->New->JUnit Test Case。
選擇 Junit Test Case,創建一個測試用例 AddTest。
Eclipse 會提示是否添加 JUnit 5 的 jar 到項目中,選擇 ok。
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class AddTest {
public static Add add;
@BeforeAll //在所有測試方法運行前運行,並且只能修飾靜態方法(除非修改測試實例生命周期)
public static void setUp() throws Exception {
add = new Add();
}
@Test //表示這是個測試方法
void add() {
//斷言相等,比較2和add.add(1,1)的返回值是否相等
assertEquals(2,add.add(1,1));
}
}
接着運行測試方法,測試結果如圖所示。
可以看到 Eclipse 的控制台自動打印出了相關信息,包括運行的測試用例的總數,測試成功數量,測試失敗數量,並且可以快速的定位到測試方法,方便進行修改。可以看到單元測試和手動測試相比要簡單快捷不少。
JUnit注解
JUnit 5 注解
注解 |
描述 |
@Test |
表示方法是一種測試方法。與 JUnit 4 的@Test 注解不同,此注釋不會聲明任何屬性 |
@ParameterizedTest |
表示方法是參數化測試 |
@RepeatedTest |
表示方法是重復測試模板 |
@TestFactory |
表示方法是動態測試的測試工程 |
@TestInstance |
用於配置測試實例生命周期 |
@TestTemplate |
表示方法是為多次調用的測試用例的模板 |
@DisplayName |
為測試類或者測試方法自定義一個名稱 |
@BeforeEach |
表示方法在每個測試方法運行前都會運行 |
@AfterEach |
表示方法在每個測試方法運行之后都會運行 |
@BeforeAll |
表示方法在所有測試方法之前運行,注意使用該注解的方法必須返回 void、訪問級別不允許為 private,且必須聲明為靜態 (static) 方法 |
@AfterAll |
表示方法在所有測試方法之后運行,而且該注解的使用限制與 @BeforeAll 一致 |
@Nested |
表示帶注解的類是嵌套的非靜態測試類,@BeforeAll 和 @AfterAll 方法不能直接在 @Nested 測試類中使用,除非修改測試實例生命周期 |
@Tag |
用於在類或方法級別聲明用於過濾測試的標記 |
@Disabled |
用於禁用測試類或測試方法 |
@ExtendWith |
用於注冊自定義擴展,該注解可以繼承 |
JUnit 常用注解的使用
首先創建一個項目 JunitTest,接着在src目錄下創建一個類Add.java。
public class Add {
public int add(int a, int b) {
return a + b;
}
}
接下來 右鍵->new->other->Junit Test Case,創建一個測試用例 AnnotationsTest.java。
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.Test;
//常用注解測試
@DisplayName("Common annotation test")
public class AnnotationsTest {
private static Add add;
@BeforeAll
public static void beforeAll() {
add=new Add();
//在所有測試方法運行前運行
System.out.println("Run before all test methods run");
}
@BeforeEach
public void beforeEach() {
//每個測試方法運行前運行
System.out.println("Run before each test method runs");
}
@AfterEach
public void afterEach() {
//每個測試方法運行完畢后運行
System.out.println("Run after each test method finishes running");
}
@AfterAll
public static void afterAll() {
//在所有測試方法運行完畢后運行
System.out.println("Run after all test methods have finished running");
}
@Disabled
@Test
@DisplayName("Ignore the test")
public void disabledTest() {
//這個測試不會運行
System.out.println("This test will not run");
}
@Test
@DisplayName("Test Methods 1+1")
public void testAdd1() {
System.out.println("Running test method1+1");
Assertions.assertEquals(2,add.add(1,1));
}
@Test
@DisplayName("Test Methods 2+2")
@RepeatedTest(1)
public void testAdd2() {
// 這個測試將重復一次
System.out.println("Running test method2+2");
Assertions.assertEquals(4,add.add(2,2));
}
}
運行測試類查看結果。
JUnit斷言
什么是斷言
編寫代碼時,我們總是會做出一些假設,斷言就是用於在代碼中捕捉這些假設。斷言表示為一些布爾表達式,程序員相信在程序中的某個特定點該表達式值為真,可以在任何時候啟用和禁用斷言驗證,因此可以在測試時啟用斷言而在部署時禁用斷言。同樣,程序投入運行后,最終用戶在遇到問題時可以重新啟用斷言。 使用斷言可以創建更穩定、品質更好且不易於出錯的代碼。當需要在一個值為 FALSE 時中斷當前操作的話,可以使用斷言。
JUnit 5 常用斷言
下表提供了一些常用的 JUnit 5 斷言方法。
斷言 |
描述 |
assertAll |
分組斷言,執行其中包含的所有斷言 |
assertEquals |
判斷斷言預期值和實際值是否相等 |
assertNotEquals |
判斷斷言預期值和實際值是否不相等 |
assertArrayEquals |
判斷斷言預期數組和實際數組相等 |
assertTrue |
判斷斷言條件是否為真 |
assertFalse |
判斷斷言條件是否為假 |
assertNull |
判斷斷言條件是否為空 |
assertNotNull |
判斷斷言條件是否不為空 |
assertSame |
判斷兩個對象引用是否指向同一個對象 |
assertTimeout |
斷言超時 |
fail |
使單元測試失敗 |
常用斷言的使用
新建一個 Java 項目 JunitTest。
在 src 目錄下,執行操作:右鍵 -> new -> JUnit Test Case。
選擇 Junit Test Case,創建一個測試用例 Assert。
Eclipse 會提示是否添加 JUnit 5 的 jar 到項目中,選擇 ok。
項目代碼如下:
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class Assert {
@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "error message");
assertTrue(2 == 2, () -> "error message");
}
@Test
void groupedAssertions() {
//分組斷言,執行分組中所有斷言,分組中任何一個斷言錯誤都會一起報告
assertAll("person",
() -> assertEquals("John", "John"),
() -> assertEquals("Doe", "Doe")
);
}
@Test
void dependentAssertions() {
//分組斷言
assertAll("properties",
() -> {
// 在代碼塊中,如果斷言失敗,后面的代碼將不會運行
String firstName = "John";
assertNotNull(firstName);
// 只有前一個斷言通過才會運行
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
},
() -> {
// 分組斷言,不會受到first Name代碼塊的影響,所以即使上面的斷言執行失敗,這里的依舊會執行
String lastName = "Doe";
assertNotNull(lastName);
// 只有前一個斷言通過才會運行
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
//斷言異常,拋出指定的異常,測試才會通過
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// 斷言超時
assertTimeout(ofMinutes(2), () -> {
// 完成任務小於2分鍾時,測試通過。
});
}
@Test
void timeoutNotExceededWithResult() {
// 斷言成功並返回結果
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "result";
});
assertEquals("result", actualResult);
}
@Test
void timeoutExceeded() {
// 斷言超時,會在任務執行完畢后才返回,也就是1000毫秒后返回結果
assertTimeout(ofMillis(10), () -> {
// 執行任務花費時間1000毫秒
Thread.sleep(1000);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// 斷言超時,如果在10毫秒內任務沒有執行完畢,會立即返回斷言失敗,不會等到1000毫秒后
assertTimeoutPreemptively(ofMillis(10), () -> {
Thread.sleep(1000);
});
}
}
運行項目后可以得到如下結果:
可以看到其中有 2 個測試沒有通過,這個是我們 Thread.sleep() 方法設置的時間超時導致,通過查看這兩個測試方法的執行時間,我們可以很輕易的對比 assertTimeoutPreemptively() 和 assertTimeout() 的區別。
JUnit假設和條件測試
什么是假設
JUnit 5 中的假設可以用於控制測試用例是否執行,相當於條件,只有條件滿足才會執行,如果條件不滿足,那么將不會執行后續測試。
JUnit 5 中的假設主要有如下內容:
方法 |
描述 |
assumeFalse |
假設為 false 時才會執行,如果為 true,那么將會直接停止執行 |
assumeTrue |
假設為 true 時才會執行,如果為 false,那么將會直接停止執行 |
assumingThat |
assumingThat 接受一個函數式接口 Executable,假設為 true 時執行,將會執行 Executable,否則不會執行 Executable。 |
JUnit 5 假設示例
新建一個 java 項目 JunitTest,接着創建一個 Junit Test Case (可以參照前面的章節) Assumption.java。
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import org.junit.jupiter.api.Test;
class Assumption {
@Test
void assumeTrueTest() {
//如果假設傳入的值為True,那么就會執行后面測試,否則直接停止執行
assumeTrue(false);
System.out.println("This will not be implemented.");
}
@Test
void assumeFalseTest() {
//如果假設傳入的值為false,那么就會執行后面測試,否則直接停止執行
assumeFalse(true);
System.out.println("This will not be implemented.");
}
@Test
void assumingThatTest() {
// assumingThat(boolean assumption, Executable executable)
// assumingThat接受一個boolean值assumption,如果assumption為true,那么將會執行executable,否則不會執行,
// 但是assumingThat即使為false也不會影響后續代碼的執行,他和assumeFalse和assumeTrue不同,assumingThat只
// 決定Executable是否執行,Executable是一個函數式接口,接受一個沒有參數和返回值的方法。
assumingThat(false,
() -> {
System.out.println("This will not be implemented.");
});
//下面的輸出將會執行
System.out.println("This will be implemented.");
}
}
執行測試。
從測試結果中可以看到 Runs:3/3(2 skipped),因為 assumeFalse 和 assumeTrue 的條件都不滿足,所以執行被中止了,而 assumingThat 不會影響到后續代碼,所以 System.out.println("This will be implemented."); 被執行,我們可以在控制台中看到輸出。
JUnit 5 條件測試
JUnit 中的條件測試也可以控制測試用例的執行,其中主要有以下幾種條件。
操作系統條件
通過 @EnabledOnOs 和 @DisabledOnOs 注解來在指定的操作系統上運行或者關閉測試,這兩個注解常用的參數有 LINUX、WINDOWS 、 MAC 和 OTHER 等表示操作系統的常量。
Java 運行環境條件
通過 @EnabledOnJre 和 @DisabledOnJre 注解 ava 在指定的 Java 環境 (JRE) 下運行或者關閉測試,這兩個注解常用的參數有 JAVA_8、JAVA_9、JAVA_10 和 OTHER 等表示不同版本 JRE 的常量。
系統屬性條件
根據 JVM 系統屬性來開啟或者關閉測試,通過 @EnabledIfSystemProperty 和 @DisabledIfSystemProperty 注解來實現。這兩個注解都擁有 named 和 matches 兩個參數。named 為 JVM 系統參數名稱,matches 接受一個正則表達式,用於匹配指定參數的值。
環境變量條件
根據系統環境變量來開啟或者關閉測試,通 @EnabledIfEnvironmentVariable 和 @DisabledIfEnvironmentVariable 注解來實現,都擁有 named 和 matches 兩個參數。named 為環境變量參數名稱,matches 接受一個正則表達式,用於匹配指定參數的值。
代碼示例
在src目錄下新建一個 Junit Test Case ,命名為 Condition.java。
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnJre;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.junit.jupiter.api.condition.EnabledOnJre;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
class Condition {
@Test
@EnabledOnJre(JRE.JAVA_8) // java8環境下運行
void javaRuntimeConditions() {
System.out.println("JAVA 8");
}
@Test
@DisabledOnJre(JRE.JAVA_8) //除了java8其他的環境都會運行
void notONJava8() {
// 這段代碼將不會運行在java8中
System.out.println("It will not run on Java8.");
}
@Test
@EnabledOnOs(OS.LINUX) //Linux系統下運行
void operatingSystemConditions() {
System.out.println("Running under Linux");
}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void systemPropertyConditions() {
//在64位虛擬機下運行
System.out.println("Running on a 64 bit system");
//輸出JVM參數列表
System.out.println(System.getProperties());
}
@Test
@EnabledIfEnvironmentVariable(named = "USER", matches = "shiyanlou")
void environmentVariableConditions() {
//輸出環境變量參數列表
System.out.println(System.getenv());
}
}
運行單元測試,得到如下結果:
本次實驗使用雲主機的測試環境為 Linux 64 位系統、Java 8 版本,從截圖中可以看見,除了方法 notONJava8() 未被執行,其他用例均符合條件。另外在方法 environmentVariableConditions() 中,根據注解參數打印了對應的環境變量信息,我們可以在控制台中看到輸出。
JUnit禁用測試
禁用測試
在測試過程中,可能有一些測試暫時還不需要運行,比如功能還沒有完成,或者 Bug 短時間無法處理,我們希望這一段測試不會運行,那么可以采用 @Disabled 注解的方式。
@Disabled 注解可以注解在方法上或者注解在類上,注解在方法上時禁用對應的方法,注解在類上的時候禁用該類中所有的測試。
首先新建一個項目 JunitTest,在 src 目錄下新建一個 JUnit Test Case:DisabledTest
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTest {
@Test
//使用@Disabled注解關閉
@Disabled
void disabled() {
System.out.println("Not running");
}
@Test
void open() {
System.out.println("running");
}
}
運行測試,結果如下所示。
可以看到注解了 @Disabled 的方法並沒有運行。
過濾測試
除了使用 JUnit 的 @Disabled 注解之外,我們也可以使用 @Tag 標簽來過濾測試,只運行我們需要的測試。
@Tag 標記語法規則:
- 標記不能為 null 或空。
- 不能包含空格。
- 不能包含 ISO 控制字符。
- 不能包含以下保留字符。
- ,(逗號)
- ( ) (左括號和右括號)
- &
- |(豎線)
- ! (感嘆號)
在 src 目錄下新建一個 JUnit Test Case:TagTest。
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class TagTest {
@Test
@Tag("tag1")
void tag1() {
System.out.println("Tag1 Test");
}
@Test
@Tag("tag2")
void tag2() {
System.out.println("Tag2 test");
}
}
先運行一次看看:
可以看到 2 個測試都有運行。
接着在 TagTest 文件上執行操作: 單擊右鍵 -> run as -> run configurations。
點擊 configure。
勾選 Include Tags 選項,在輸入框中填入需要運行的測試方法的標簽名。
點擊 OK,接着點擊 Run。
從運行結果可以看出 Eclipse 過濾掉了 tag2 標簽,只運行了 tag1 標簽的測試用例。
JUnit復測試
@RepeatedTest 注解
通過 @RepeatedTest 注解可以完成重復測試的工作,@RepeatedTest 中的 value 屬性可以設置重復的次數,name 屬性可以自定義重復測試的顯示名,顯示名可以由占位符和靜態文本組合,目前支持下面幾種占位符:
- {displayName}:顯示名;
- {currentRepetition}:當前重復次數;
- {totalRepetitions}:總重復次數。
JUnit 重復測試提供了默認的顯示名的模式:repetition {currentRepetition} of {totalRepetitions}。
如果沒有自定義的話,顯示名就會是這種形式:repetition 1 of 10。
JUnit 還提供了另外一種顯示名 RepeatedTest.LONG_DISPLAY_NAME,它顯示的形式為:{displayName} :: repetition {currentRepetition} of {totalRepetitions},只需要將 @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) 注解到方法上就可以使用這種顯示方式。如果想要用編程的方式獲取當前循環測試的相關詳細,可以將 RepetitionInfo 實例注入到相關的方法中。
代碼示例
打開 Eclipse,新建一個項目 JunitTest,在 src 目錄下新建一個 JUnit Test Case:Repeated。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.BeforeAll;
class Repeated {
@BeforeAll
public static void beforeAll() {
System.out.println("Before All");
}
@BeforeEach
void beforeEach() {
System.out.println("Before Each");
}
//自定義重復測試的顯示名稱
@RepeatedTest(value=5,name="{displayName}-->{currentRepetition}/{totalRepetitions}")
@DisplayName("repeatTest")
void repeatedTest(TestInfo testInfo,RepetitionInfo repetitionInfo) {
//我們可以通過TestInfo在測試中獲取測試的相關信息,比如輸出自定義的測試名
System.out.println(testInfo.getDisplayName());
//輸出當前重復次數
System.out.println("currentRepetition:"+repetitionInfo.getCurrentRepetition());
}
}
運行測試,測試結果如下所示。
可以看見 repeatedTest() 方法被執行了 5 次,且每次測試的結果都是成功的。
控制台輸出如下所示。
從截圖可見,在每次重復測試中,利用 getDisplayName() 方法將自定義的測試名打印了出來,且測試名根據 currentRepetition 當前重復次數的值在逐漸遞增。另外通過 getCurrentRepetition() 方法再次打印當前重復次數的值,與前面測試名中的值是一致的。
另外應當特別注意 @BeforeAll 和 @BeforeEach 這類注解在重復測試中的表現。從輸出結果可以看到,beforeAll() 方法仍然僅在測試開始前執行了一次,而 beforeEach() 方法在每次重復前都執行一次。這種執行結果與非重復測試的情況仍然一致。實際上,重復測試的每次調用的行為就像一個常規 @Test 方法的執行,和常規的測試方法在聲明周期和擴展上是一致的。
JUnit參數化測試
參數化測試
參數化測試可以使用不同的參數進行多次測試,相當於重復測試的增強版,只是參數化測試使用@ParameterizedTest 注解聲明,而且參數化測試必須聲明至少一個參數源,比如 @ValueSource 注解。另外要使用參數化測試必須引入 junit-jupiter-params 依賴,如果使用 Eclipse 創建 JUnit 測試則不需要引入,因為 Eclipse 在引入 JUnit 依賴的時候會引入 JUnit 的所有依賴。
下面寫一個簡單的參數化測試,在 Eclipse 中新建一個 Java 項目 JunitTest,在 src 目錄下新建一個 Junit Test Case:ParameterTest。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class ParameterTest {
@ParameterizedTest
@ValueSource(strings= {"Java","C++","Python"})
void parameter(String args) {
System.out.println(args);
}
}
運行測試結果如下所示。
控制台輸出結果如下所示。
參數源
測試方法數據的來源就是參數源,JUnit 提供了多種參數源。
參數源介紹
@ValueSource
@ValueSource 允許指定原生類型的數組,並且只能用於為每個參數化測試調用提供單個參數。@ValueSource 支持下面幾種類型:
- int[]
- long[]
- short[]
- double[]
- float[]
- char[]
- byte[]
- boolean[]
- java.lang.String[]
- java.lang.Class<?>[]
@EnumSource
@EnumSource 提供了一種使用 Enum 常量的便捷方法。該注釋提供了一個可選 names 參數,允許指定應使用哪些常量。如果省略,將使用所有常量。
@MethodSource
@MethodSource 允許引用測試類或外部類的一個或多個工廠方法。這個方法必須是 static 的(除非修改測試類生命周期為@TestInstance(Lifecycle.PER_CLASS)),返回值必須是一個 Stream、Iterable、Iterator 或者參數數組。並且這些方法不能有參數。外部類的方法則必須是 static 的。
@CsvSource
@CsvSource 允許將參數列表定義為以逗號分隔的值(即 String 類型)。@CsvSource 使用單引號 ' 作為引號字符。空的引用值 '' 會被解釋成空的的 String 類型,而完全空值被解釋為 null。
JUnit 還支持從 CSV 格式文件中讀取數據作為數據源,使用的注解為 @CsvFileSource ,其本質與 @CsvSource 一樣,都是對 CSV 格式的數據進行處理,主要區別在於文件路徑的引入和數據讀取的細節上,具體使用可參考文檔 CsvFileSource。
輸入 |
輸出 |
@CsvSource({ "a, b" }) |
"a","b" |
@CsvSource({ "a, 'b,c'" }) |
"a","b,c" |
@CsvSource({ "a, ''" }) |
"a","" |
@CsvSource({ "a, " }) |
"a",null |
@ArgumentsSource
@ArgumentsSource 可用於指定自定義,可重用 ArgumentsProvider。自定義時需要實現 ArgumentsProvider 接口,並且必須將實現聲明為 public 類或 static 嵌套類。
代碼示例
修改 ParameterTest.java。
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
class ParameterTest {
@ParameterizedTest
@ValueSource(strings= {"Java","C++","Python"})
void parameter(String args) {
System.out.println(args);
}
@ParameterizedTest
//使用names制定需要的枚舉常量
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void enumSource(TimeUnit timeUnit) {
System.out.println(timeUnit.toString());
}
@ParameterizedTest
@CsvSource({ "Java, 1", "C++, 2", "'Python, Lisp', 3" })
void csvSource(String first, int second) {
System.out.println(first+"---"+second);
}
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void argumentsSource(String argument) {
System.out.print(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("Java-", "C-", "Python\n").map(Arguments::of);
}
}
}
運行測試,結果如圖所示。
由圖可見,在實驗例子中每個參數均通過了測試。
注意:由於例子中的測試都使用 @ParameterizedTest 注解,使用相同的注解的多個方法,它們的執行順序是不確定的。
控制台輸出結果如圖所示。
JUnit測試實踐
項目結構
首先展示本次實驗的項目結構。注意創建 Java 項目 JunitTest 后,應當將實驗給出的測試樣例程序 SaleMachine.java 導入到項目和包中。
接下來講解測試實驗的具體內容。
本次單元測試實驗主要包含兩部分:路徑測試和功能測試。
路徑測試屬於白盒測試,主要是檢查該程序的路徑分支是否可完整覆蓋執行、是否存在編程邏輯錯誤,如錯誤的計算、不正確的比較等原因導致不正常的控制流程缺陷。
功能測試屬於黑盒測試,着重測試軟件的功能需求,不需要考慮內部結構及代碼,是在程序接口上進行的測試,檢查產品是否達到用戶要求的功能。
因此,在進行程序測試前,首先需要理解程序的功能需求,並且對被測程序進行流程分析,畫出程序流程圖,分析程序的執行路徑,以便設計相應的測試用例。
程序分析
被測程序 SaleMachine.java 代碼如下。
package test;
public class SaleMachine {
private int countOfBeer, countOfFiveJiao, countOfOneYuan;// 售貨機中 3 個資源變量,分別代表啤酒的數量、5角硬幣的數量、1元硬幣的數量
private String resultOfDeal;// 銷售結果
public SaleMachine()// 默認構造函數
{
initial();// 初始化
}
public void initial()// 將各類資源的數量清0
{
countOfBeer = 0; // 售貨機啤酒數量清零
countOfFiveJiao = 0;// 售貨機5角硬幣數量清零
countOfOneYuan = 0;// 售貨機1元硬幣數量清零
}
public SaleMachine(int fiveJiao, int oneYuan, int numOfBeer)
// 有參數的構造函數,將實際參數傳遞給形參,對類中屬性變量初始化
{
countOfFiveJiao = fiveJiao;
countOfOneYuan = oneYuan;
countOfBeer = numOfBeer;
}
public String currentState()// 獲取售貨機當前四種資源變量數量值
{
String state = "Current State\n" + "Beer: " + countOfBeer + "\n" + "5 Jiao: " + countOfFiveJiao + "\n"
+ "1 Yuan: " + countOfOneYuan;
return state;
}
public String operation(String money)// 售貨機操作控制程序
// type參數代表客戶選擇的購買商品類型,money參數代表客戶投幣類型
{
if (money.equalsIgnoreCase("5J")) // 如果客戶投入5角錢
{
if (countOfBeer > 0) // 如果還有啤酒,進行交易,修改資源數量
{
// 路徑S1輸出信息
countOfBeer--;
countOfFiveJiao++;
resultOfDeal = "Input Information\n" + "Money: 5 Jiao; Change: 0";
return resultOfDeal;
} else // 沒有啤酒,輸出啤酒短缺的信息
{
// 路徑S2輸出信息
resultOfDeal = "Failure Information\n" + "Beer Shortage";
return resultOfDeal;
}
} else if (money.equalsIgnoreCase("1Y")) // 如果客戶投入一元錢
{
if (countOfFiveJiao > 0) // 如果售貨機有5角零錢
{
// 路徑S3輸出信息,還有啤酒
if (countOfBeer >= 0) {
countOfBeer--;
countOfFiveJiao++;
countOfOneYuan++;
resultOfDeal = "Input Information\n" + "Money: 1 Yuan; Change: 5 Jiao";
return resultOfDeal;
} else {
// 路徑S4,沒有啤酒,輸出啤酒短缺信息
resultOfDeal = "Failure Information\n" + "Beer Shortage";
return resultOfDeal;
}
} else // 售貨機沒有5角零錢,輸出零錢短缺錯誤信息
{
// 路徑S5輸出信息
resultOfDeal = "Failure Information\n" + "Change Shortage";
return resultOfDeal;
}
} else // 客戶輸入不是5J和1Y,輸出投幣類型錯誤信息
{
// 路徑S6輸出信息
resultOfDeal = "Failure Information\n" + "Money Error";
return resultOfDeal;
}
}
}
SaleMachine 程序功能是模擬一台簡單的啤酒售賣機,它遵守以下的銷售規則:
- 啤酒銷售價格為 5 角。
- 機器僅接受 1 元和 5 角兩種貨幣,如果投入其他類型貨幣,將會提示錯誤信息。
- 當收到 5 角貨幣時,機器檢查啤酒庫存,庫存短缺則提示錯誤信息。反之銷售成功,記錄庫存和貨幣數量變化信息。
- 當收到 1 元貨幣時,機器首先檢測是否有 5 角貨幣進行找零,若缺少零錢則提示錯誤信息;有零錢的前提下,接着檢查啤酒庫存,庫存短缺則提示錯誤信息。反之銷售成功,記錄庫存和貨幣數量變化信息。
SaleMachine 程序的流程圖如下:
可以看見程序中一共有六個路徑,根據用戶不同的輸入值,程序應當執行用戶期望的路徑,這是程序路徑測試是否成功的關鍵。
在了解程序的功能和工作流程后,應當進行測試用例的設計。
測試用例設計
在 SaleMachine 程序中,其 operation(String Money) 方法函數將根據不同輸入參數執行不同路徑。Money 參數是客戶投入的硬幣金額,如 5J 和 1Y。在自動售貨機程序內,使用變量 CountOfBeer 記錄啤酒的數量、使用變量 CountOfFiveJiao 記錄 5 角硬幣數量,使用 CountOfOneYuan 記錄 1 元硬幣的數量。各個變量的初值在程序初始化中設置。
在設計的測試用例表中,每行對應一個路徑的測試用例,路徑測試用例表如下所示:
用例編號 |
輸入值 |
資源變量狀態 |
期望路徑 |
判斷准則 |
1 |
5J |
CountOfFiveJiao=5 |
S1 |
執行路徑與期望路徑一致 |
2 |
5J |
CountOfFiveJiao=5 |
S2 |
執行路徑與期望路徑一致 |
3 |
1Y |
CountOfFiveJiao=5 |
S3 |
執行路徑與期望路徑一致 |
4 |
1Y |
CountOfFiveJiao=5 |
S4 |
執行路徑與期望路徑一致 |
5 |
1Y |
CountOfFiveJiao=0 |
S5 |
執行路徑與期望路徑一致 |
6 |
1J |
CountOfFiveJiao=5 |
S6 |
執行路徑與期望路徑一致 |
當樣本程序 SaleMachine 的功能函數 Operation(String Money) 在傳入不同的參數時,跳轉不同分支路徑進行功能處理,並對各個資源變量進行修改。資源變量變化后的值,與程序設計期望的輸出值是否一致,決定了程序功能是否正常。根據這點設計功能測試用例表,如下所示:
用例編號 |
輸入值 |
資源變量 |
期望輸出值 |
判斷准則 |
1 |
5J |
CountOfFiveJiao=5 |
Current State |
是否測試通過 |
2 |
1Y |
CountOfFiveJiao=5 |
Current State |
是否測試通過 |
3 |
1J |
CountOfFiveJiao=5 |
Current State |
是否測試通過 |
4 |
null |
CountOfFiveJiao=5 |
Current State |
是否測試通過 |
編寫測試用例類
- 編寫路徑測試用例類 SaleMachinePathTest.java。
package test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.Test;
public class SaleMachinePathTest {
@Test
public void testOperationS1() { // 路徑S1:有零錢 有啤酒 投5角
SaleMachine saleMachine1 = new SaleMachine(5, 5, 5);
String expectedResult = "Input Information\n" + "Money: 5 Jiao; Change: 0";
assertEquals(expectedResult, saleMachine1.operation("5J"));
}
@Test
public void testOperationS2() { // 路徑S2:有零錢 無啤酒 投5角
SaleMachine saleMachine2 = new SaleMachine(5, 5, 0);
String expectedResult = "Failure Information\n" + "Beer Shortage";
assertEquals(expectedResult, saleMachine2.operation("5J"));
}
@Test
public void testOperationS3() { // 路徑S3:有零錢 有啤酒 投1元
SaleMachine saleMachine3 = new SaleMachine(5, 5, 5);
String expectedResult = "Input Information\n" + "Money: 1 Yuan; Change: 5 Jiao";
assertEquals(expectedResult, saleMachine3.operation("1Y"));
}
@Test
public void testOperationS4() { // 路徑S4:有零錢 無啤酒 投1元
SaleMachine saleMachine4 = new SaleMachine(5, 5, 0);
String expectedResult = "Failure Information\n" + "Beer Shortage";
assertEquals(expectedResult, saleMachine4.operation("1Y"));
}
@Test
public void testOperationS5() { // 路徑S5:無零錢 有啤酒 投1元
SaleMachine saleMachine5 = new SaleMachine(0, 5, 5);
String expectedResult = "Failure Information\n" + "Change Shortage";
assertEquals(expectedResult, saleMachine5.operation("1Y"));
}
@Test
public void testOperationS6() { // 路徑S6:有零錢 有啤酒 投1角
SaleMachine saleMachine6 = new SaleMachine(5, 5, 5);
String expectedResult = "Failure Information\n" + "Money Error";
assertEquals(expectedResult, saleMachine6.operation("1J"));
}
}
在測試用例類的編寫中,每個測試方法遵循相同的步驟:根據測試用例輸入不同的參數初始化對象,使用斷言判斷對象調用方法的結果與期望結果是否相同。為了避免格式問題導致不必要的測試失敗,期望輸出變量 expectedResult 的內容可直接在被測程序中復制。用例類的編寫關鍵實際上在於前面測試用例的設計上,測試用例決定了每個方法使用什么參數,測試結果的判斷依據等。
- 編寫功能測試用例類 SaleMachineFunctionTest.java。
package test;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.Before;
import org.junit.Test;
public class SaleMachineFunctionTest {
private SaleMachine saleMachine;
@Before
public void setUp() throws Exception {
saleMachine = new SaleMachine(5, 5, 5);
}
@Test
public void testOperation1() {
saleMachine.operation("5J");
String expectedResult = "Current State\n" + "Beer: 4\n" + "5 Jiao: 6\n" + "1 Yuan: 5";
assertEquals(expectedResult, saleMachine.currentState());
}
@Test
public void testOperation2() {
saleMachine.operation("1Y");
String expectedResult = "Current State\n" + "Beer: 4\n" + "5 Jiao: 4\n" + "1 Yuan: 6";
assertEquals(expectedResult, saleMachine.currentState());
}
@Test
public void testOperation3() {
saleMachine.operation("1J");
String expectedResult = "Current State\n" + "Beer: 5\n" + "5 Jiao: 5\n" + "1 Yuan: 5";
assertEquals(expectedResult, saleMachine.currentState());
}
@Test
public void testOperation4() {
saleMachine.operation(null);
String expectedResult = "Current State\n" + "Beer: 5\n" + "5 Jiao: 5\n" + "1 Yuan: 5";
assertEquals(expectedResult, saleMachine.currentState());
}
}
功能測試用例類的編寫與路經測試用例類相似,區別在於功能測試不關注程序內部的具體執行路徑,而是從用戶的角度對程序功能進行測試。因此本次測試中事先將對象實例化,並將資源數量均初始化為 5。通過對比不同參數下資源數量變化和期望輸出是否一致,判斷功能是否正常。
測試運行和結果分析
- 編譯執行 SaleMachinePathTest.java
如圖所示,在項目結構中右鍵點擊 SaleMachinePathTest.java,選擇 Run As -> JUnit Test 運行測試。
從 JUnit 執行結果可見,路徑 S4 未通過測試。左鍵點擊 testOperationS4 ,在 Failure Trace 選項中點擊鼠標右鍵呼出菜單,選擇 Compare Result 查看具體結果對比,如圖所示。
可見期望輸出信息是啤酒缺貨,而實際輸出為交易信息,因此路徑 S4 存在錯誤。
- 編譯執行 SaleMachineFunctionTest.java
運行方法參考路徑測試,測試結果如圖所示。
從測試結果可見,方法 testOperation2 測試失敗,方法 testOperation4 出現錯誤。
首先查看 testOperation2 的失敗原因。
由圖可見,測試失敗的原因是實際資源變化和期望的資源變化不同。其中 5 角的數量在找零后應當變為 4,而實際變為了 6。
接着查看 testOperation4 的失敗原因。
錯誤信息為 NullPointerException ,這是常見的空指針錯誤。
程序改進
首先根據路徑測試的結果,對路徑 S4 進行改進。根據代碼注釋,查看 SaleMachine.java 中路徑 S4 附近相關代碼,如圖所示。
可看見路徑 S4 要在 S3 不符合條件的情況才能執行,然而路徑 S3 的判斷條件為 countOfBeer >= 0 即啤酒數量為 0 時,售貨機程序仍然賣出啤酒,而不是執行期望的 S4 路徑,即輸出 Failure Information Beer Shortage 啤酒缺貨信息。因此只要將 S3 中 if(countOfBeer >= 0) 語句改為 if(countOfBeer > 0) 即可。
再次運行測試用例類 SaleMachinePathTest.java,結果如下所示,所有路徑均通過測試,路徑測試完成。
接着根據功能測試的結果,對程序進行進一步的改進。
根據上一節的測試結果分析,程序存在資源變化方面的錯誤,即找零的情況下,5 角的數量本應減 1,實際上卻增 1,不符合程序的功能需求。因此要定位錯誤代碼的位置,應當從涉及找零的部分入手。
檢查代碼,可發現錯誤位於路徑 S3 附近,如下所示。
路徑 S3 用於處理客戶投入 1 元貨幣,找零 5 角的情況,此時變量 countOfFiveJiao 應當自減 1,但原程序中的語句為 countOfFiveJiao++,顯然不符合實際需求,改為 countOfFiveJiao-- 即可。
最后的錯誤為上節提到的 NullPonterException,該錯誤對應的用例為參數 Money 為 null 的情況,說明程序中缺乏對於輸入為空的情況的判斷,其中一種解決方法為增加對輸入的判斷。
如圖所示,向 if (money.equalsIgnoreCase("5J")) 和 else if (money.equalsIgnoreCase("1Y")) 這兩個判斷語句中添加對 money 的判斷:money != null 即可。即改為 if (money != null && money.equalsIgnoreCase("5J")) 和 else if (money != null && money.equalsIgnoreCase("1Y"))。
保存以上修改,重新運行測試類,所有測試通過,如下所示。
出處:https://www.cnblogs.com/yyyyfly1/p/15958047.html