JUnit:別再用 main 方法測試了,好嗎?

01、前世今生

你好呀,我是 JUnit,一個開源的 Java 單元測試框架。在瞭解我之前,先來了解一下什麼是單元測試。單元測試,就是針對最小的功能單元編寫測試代碼。在 Java 中,最小的功能單元就是方法,因此,對 Java 程序員進行單元測試實際上就是對 Java 方法的測試。

爲什麼要進行單元測試呢?因爲單元測試可以確保你編寫的代碼是符合軟件需求和遵循開發規範的。單元測試是所有測試中最底層的一類測試,是第一個環節,也是最重要的一個環節,是唯一一次能夠達到代碼覆蓋率 100% 的測試,是整個軟件測試過程的基礎和前提。可以這麼說,單元測試的性價比是最好的。

微軟公司之前有這樣一個統計:bug 在單元測試階段被發現的平均耗時是 3.25 小時,如果遺漏到系統測試則需要 11.5 個小時。

經我這麼一說,你應該已經很清楚單元測試的重要性了。那在你最初編寫測試代碼的時候,是不是經常這麼做?就像下面這樣。

public class Factorial {
    public static long fact(long n) {
        long r = 1;
        for (long i = 1; i <= n; i++) {
            r = r * i;
        }
        return r;
    }

    public static void main(String[] args) {
        if (fact(3) == 6) {
            System.out.println("通過");
        } else {
            System.out.println("失敗");
        }
    }
}

要測試 fact() 方法正確性,你在 main() 方法中編寫了一段測試代碼。如果你這麼做過的話,我只能說你也曾經青澀天真過啊!使用 main() 方法來測試有很多壞處,比如說:

1)測試代碼沒有和源代碼分開。

2)不夠靈活,很難編寫一組通用的測試代碼。

3)無法自動打印出預期和實際的結果,沒辦法比對。

但如果學會使用我——JUnit 的話,就不會再有這種困擾了。我可以非常簡單地組織測試代碼,並隨時運行它們,還能給出準確的測試報告,讓你在最短的時間內發現自己編寫的代碼到底哪裏出了問題。

02、上手指南

好了,既然知道了我這麼優秀,那還等什麼,直接上手吧!我最新的版本是 JUnit 5,Intellij IDEA 中已經集成了,所以你可以直接在 IDEA 中編寫並運行我的測試用例。

第一步,直接在當前的代碼編輯器窗口中按下 Command+N 鍵(Mac 版),在彈出的菜單中選擇「Test...」。

勾選上要編寫測試用例的方法 fact(),然後點擊「OK」。

此時,IDEA 會自動在當前類所在的包下生成一個類名帶 Test(慣例)的測試類。如下圖所示。

如果你是第一次使用我的話,IDEA 會提示你導入我的依賴包。建議你選擇最新的 JUnit 5.4。

導入完畢後,你可以打開 pom.xml 文件確認一下,裏面多了對我的依賴。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>RELEASE</version>
    <scope>compile</scope>
</dependency>

第二步,在測試方法中添加一組斷言,如下所示。

@Test
void fact() {
    assertEquals(1, Factorial.fact(1));
    assertEquals(2, Factorial.fact(2));
    assertEquals(6, Factorial.fact(3));
    assertEquals(100, Factorial.fact(5));
}

@Test 註解是我要求的,我會把帶有 @Test 的方法識別爲測試方法。在測試方法內部,你可以使用 assertEquals() 對期望的值和實際的值進行比對。

第三步,你可以在郵件菜單中選擇「Run FactorialTest」來運行測試用例,結果如下所示。

測試失敗了,因爲第 20 行的預期結果和實際不符,預期是 100,實際是 120。此時,你要麼修正實現代碼,要麼修正測試代碼,直到測試通過爲止。

不難吧?單元測試可以確保單個方法按照正確的預期運行,如果你修改了某個方法的代碼,只需確保其對應的單元測試通過,即可認爲改動是沒有問題的。

03、瞻前顧後

在一個測試用例中,可能要對多個方法進行測試。在測試之前呢,需要準備一些條件,比如說創建對象;在測試完成後呢,需要把這些對象銷燬掉以釋放資源。如果在多個測試方法中重複這些樣板代碼又會顯得非常囉嗦。

這時候,該怎麼辦呢?

我爲你提供了 setUp()tearDown(),作爲一個文化人,我稱之爲“瞻前顧後”。來看要測試的代碼。

public class Calculator {
    public int sub(int a, int b) {
        return a - b;
    }
    public int add(int a, int b) {
        return a + b;
    }
}

新建測試用例的時候記得勾選setUptearDown

生成後的代碼如下所示。

class CalculatorTest {
    Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @AfterEach
    void tearDown() {
        calculator = null;
    }


    @Test
    void sub() {
        assertEquals(0,calculator.sub(1,1));
    }

    @Test
    void add() {
        assertEquals(2,calculator.add(1,1));
    }
}

@BeforeEachsetUp() 方法會在運行每個 @Test 方法之前運行;@AfterEachtearDown() 方法會在運行每個 @Test 方法之後運行。

與之對應的還有 @BeforeAll@AfterAll,與 @BeforeEach@AfterEach 不同的是,All 通常用來初始化和銷燬靜態變量。

public class DatabaseTest {
    static Database db;

    @BeforeAll
    public static void init() {
        db = createDb(...);
    }
    
    @AfterAll
    public static void drop() {
        ...
    }
}

03、異常測試

對於 Java 程序來說,異常處理也非常的重要。對於可能拋出的異常進行測試,本身也是測試的一個重要環節。

還拿之前的 Factorial 類來進行說明。在 fact() 方法的一開始,對參數 n 進行了校驗,如果小於 0,則拋出 IllegalArgumentException 異常。

public class Factorial {
    public static long fact(long n) {
        if (n < 0) {
            throw new IllegalArgumentException("參數不能小於 0");
        }
        long r = 1;
        for (long i = 1; i <= n; i++) {
            r = r * i;
        }
        return r;
    }
}

在 FactorialTest 中追加一個測試方法 factIllegalArgument()

@Test
void factIllegalArgument() {
    assertThrows(IllegalArgumentException.class, new Executable() {
        @Override
        public void execute() throws Throwable {
            Factorial.fact(-2);
        }
    });
}

我爲你提供了一個 assertThrows() 的方法,第一個參數是異常的類型,第二個參數 Executable,可以封裝產生異常的代碼。如果覺得匿名內部類寫起來比較複雜的話,可以使用 Lambda 表達式。

@Test
void factIllegalArgumentLambda() {
    assertThrows(IllegalArgumentException.class, () -> {
        Factorial.fact(-2);
    });
}

04、忽略測試

有時候,由於某些原因,某些方法產生了 bug,需要一段時間去修復,在修復之前,該方法對應的測試用例一直是以失敗告終的,爲了避免這種情況,我爲你提供了 @Disabled 註解。

class DisabledTestsDemo {

    @Disabled("該測試用例不再執行,直到編號爲 43 的 bug 修復掉")
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }

}

@Disabled 註解也可以不需要說明,但我建議你還是提供一下,簡單地說明一下爲什麼這個測試方法要忽略。在上例中,如果團隊的其他成員看到說明就會明白,當編號 43 的 bug 修復後,該測試方法會重新啓用的。即便是爲了提醒自己,也很有必要,因爲時間長了你可能自己就忘了,當初是爲什麼要忽略這個測試方法的。

05、條件測試

有時候,你可能需要在某些條件下運行測試方法,有些條件下不運行測試方法。針對這場使用場景,我爲你提供了條件測試。

1)不同的操作系統,可能需要不同的測試用例,比如說 Linux 和 Windows 的路徑名是不一樣的,通過 @EnabledOnOs 註解就可以針對不同的操作系統啓用不同的測試用例。

@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
    // ...
}

@TestOnMac
void testOnMac() {
    // ...
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
    // ...
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
    // ...
}

2)不同的 Java 運行環境,可能也需要不同的測試用例。@EnabledOnJre@EnabledForJreRange 註解就可以滿足這個需求。

@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
    // ...
}

@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
    // ...
}

@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
    // ...
}

06、尾聲

最後,給你說三句心裏話吧。在編寫單元測試的時候,你最好這樣做:

1)單元測試的代碼本身必須非常名單明瞭,能一下看明白,決不能再爲測試代碼編寫測試代碼。

2)每個單元測試應該互相獨立,不依賴運行時的順序。

3)測試時要特別注意邊界條件,比如說 0,null,空字符串"" 等情況。

希望我能儘早的替你發現代碼中的 bug,畢竟越早的發現,造成的損失就會越小。see you!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章