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

0一、前世此生

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

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

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

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

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)不夠靈活,很難編寫一組通用的測試代碼。ide

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

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

0二、上手指南

好了,既然知道了我這麼優秀,那還等什麼,直接上手吧!我最新的版本是 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。此時,你要麼修正實現代碼,要麼修正測試代碼,直到測試經過爲止。

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

0三、瞻前顧後

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

這時候,該怎麼辦呢?

我爲你提供了 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() {
        ...
    }
}
複製代碼

0三、異常測試

對於 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);
    });
}
複製代碼

0四、忽略測試

有時候,因爲某些緣由,某些方法產生了 bug,須要一段時間去修復,在修復以前,該方法對應的測試用例一直是以失敗了結的,爲了不這種狀況,我爲你提供了 @Disabled 註解。

class DisabledTestsDemo {

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

    @Test
    void testWillBeExecuted() {
    }

}
複製代碼

@Disabled 註解也能夠不須要說明,但我建議你仍是提供一下,簡單地說明一下爲何這個測試方法要忽略。在上例中,若是團隊的其餘成員看到說明就會明白,當編號 43 的 bug 修復後,該測試方法會從新啓用的。即使是爲了提醒本身,也頗有必要,由於時間長了你可能本身就忘了,當初是爲何要忽略這個測試方法的。

0五、條件測試

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

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() {
    // ...
}
複製代碼

0六、尾聲

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

1)單元測試的代碼自己必須很是名單明瞭,能一下看明白,決不能再爲測試代碼編寫測試代碼。

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

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

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

推薦閱讀

帶妹入坑,她該怎樣提升本身的編程能力?

太讚了,GitHub 上標星 115k+ 的 Java 教程!

GitHub上最勵志的計算機自學教程(重製版)

最後的一點點請求,若是這篇文章的確幫助到了你,就順手點個贊吧,讓 2021 年第一次更文的我,快樂那麼一下下,謝謝你的鼓勵呀,新年咱們一塊兒加油!

相關文章
相關標籤/搜索