JUnit:別再用 main 方法測(cè)試了,好嗎?
01、前世今生
你好呀,我是 JUnit,一個(gè)開源的 Java 單元測(cè)試框架。在了解我之前,先來了解一下什么是單元測(cè)試。單元測(cè)試,就是針對(duì)最小的功能單元編寫測(cè)試代碼。在 Java 中,最小的功能單元就是方法,因此,對(duì) Java 程序員進(jìn)行單元測(cè)試實(shí)際上就是對(duì) Java 方法的測(cè)試。
為什么要進(jìn)行單元測(cè)試呢?因?yàn)閱卧獪y(cè)試可以確保你編寫的代碼是符合軟件需求和遵循開發(fā)規(guī)范的。單元測(cè)試是所有測(cè)試中最底層的一類測(cè)試,是第一個(gè)環(huán)節(jié),也是最重要的一個(gè)環(huán)節(jié),是唯一一次能夠達(dá)到代碼覆蓋率 100% 的測(cè)試,是整個(gè)軟件測(cè)試過程的基礎(chǔ)和前提??梢赃@么說,單元測(cè)試的性價(jià)比是最好的。
微軟公司之前有這樣一個(gè)統(tǒng)計(jì):bug 在單元測(cè)試階段被發(fā)現(xiàn)的平均耗時(shí)是 3.25 小時(shí),如果遺漏到系統(tǒng)測(cè)試則需要 11.5 個(gè)小時(shí)。
經(jīng)我這么一說,你應(yīng)該已經(jīng)很清楚單元測(cè)試的重要性了。那在你最初編寫測(cè)試代碼的時(shí)候,是不是經(jīng)常這么做?就像下面這樣。
- 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("失敗");
- }
- }
- }
要測(cè)試 fact() 方法正確性,你在 main() 方法中編寫了一段測(cè)試代碼。如果你這么做過的話,我只能說你也曾經(jīng)青澀天真過啊!使用 main() 方法來測(cè)試有很多壞處,比如說:
1)測(cè)試代碼沒有和源代碼分開。
2)不夠靈活,很難編寫一組通用的測(cè)試代碼。
3)無法自動(dòng)打印出預(yù)期和實(shí)際的結(jié)果,沒辦法比對(duì)。
但如果學(xué)會(huì)使用我——JUnit 的話,就不會(huì)再有這種困擾了。我可以非常簡(jiǎn)單地組織測(cè)試代碼,并隨時(shí)運(yùn)行它們,還能給出準(zhǔn)確的測(cè)試報(bào)告,讓你在最短的時(shí)間內(nèi)發(fā)現(xiàn)自己編寫的代碼到底哪里出了問題。
02、上手指南
好了,既然知道了我這么優(yōu)秀,那還等什么,直接上手吧!我最新的版本是 JUnit 5,Intellij IDEA 中已經(jīng)集成了,所以你可以直接在 IDEA 中編寫并運(yùn)行我的測(cè)試用例。
第一步,直接在當(dāng)前的代碼編輯器窗口中按下 Command+N 鍵(Mac 版),在彈出的菜單中選擇「Test...」。
勾選上要編寫測(cè)試用例的方法 fact(),然后點(diǎn)擊「OK」。
此時(shí),IDEA 會(huì)自動(dòng)在當(dāng)前類所在的包下生成一個(gè)類名帶 Test(慣例)的測(cè)試類。如下圖所示。
如果你是第一次使用我的話,IDEA 會(huì)提示你導(dǎo)入我的依賴包。建議你選擇最新的 JUnit 5.4。
導(dǎo)入完畢后,你可以打開 pom.xml 文件確認(rèn)一下,里面多了對(duì)我的依賴。
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter</artifactId>
- <version>RELEASE</version>
- <scope>compile</scope>
- </dependency>
第二步,在測(cè)試方法中添加一組斷言,如下所示。
- @Test
- void fact() {
- assertEquals(1, Factorial.fact(1));
- assertEquals(2, Factorial.fact(2));
- assertEquals(6, Factorial.fact(3));
- assertEquals(100, Factorial.fact(5));
- }
@Test 注解是我要求的,我會(huì)把帶有 @Test 的方法識(shí)別為測(cè)試方法。在測(cè)試方法內(nèi)部,你可以使用 assertEquals() 對(duì)期望的值和實(shí)際的值進(jìn)行比對(duì)。
第三步,你可以在郵件菜單中選擇「Run FactorialTest」來運(yùn)行測(cè)試用例,結(jié)果如下所示。
測(cè)試失敗了,因?yàn)榈?20 行的預(yù)期結(jié)果和實(shí)際不符,預(yù)期是 100,實(shí)際是 120。此時(shí),你要么修正實(shí)現(xiàn)代碼,要么修正測(cè)試代碼,直到測(cè)試通過為止。
不難吧?單元測(cè)試可以確保單個(gè)方法按照正確的預(yù)期運(yùn)行,如果你修改了某個(gè)方法的代碼,只需確保其對(duì)應(yīng)的單元測(cè)試通過,即可認(rèn)為改動(dòng)是沒有問題的。
03、瞻前顧后
在一個(gè)測(cè)試用例中,可能要對(duì)多個(gè)方法進(jìn)行測(cè)試。在測(cè)試之前呢,需要準(zhǔn)備一些條件,比如說創(chuàng)建對(duì)象;在測(cè)試完成后呢,需要把這些對(duì)象銷毀掉以釋放資源。如果在多個(gè)測(cè)試方法中重復(fù)這些樣板代碼又會(huì)顯得非常啰嗦。
這時(shí)候,該怎么辦呢?
我為你提供了 setUp() 和 tearDown(),作為一個(gè)文化人,我稱之為“瞻前顧后”。來看要測(cè)試的代碼。
- public class Calculator {
- public int sub(int a, int b) {
- return a - b;
- }
- public int add(int a, int b) {
- return a + b;
- }
- }
新建測(cè)試用例的時(shí)候記得勾選setUp 和 tearDown。
生成后的代碼如下所示。
- 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));
- }
- }
@BeforeEach 的 setUp() 方法會(huì)在運(yùn)行每個(gè) @Test 方法之前運(yùn)行;@AfterEach 的tearDown() 方法會(huì)在運(yùn)行每個(gè) @Test 方法之后運(yùn)行。
與之對(duì)應(yīng)的還有 @BeforeAll 和 @AfterAll,與 @BeforeEach 和 @AfterEach 不同的是,All 通常用來初始化和銷毀靜態(tài)變量。
- public class DatabaseTest {
- static Database db;
- @BeforeAll
- public static void init() {
- db = createDb(...);
- }
- @AfterAll
- public static void drop() {
- ...
- }
- }
03、異常測(cè)試
對(duì)于 Java 程序來說,異常處理也非常的重要。對(duì)于可能拋出的異常進(jìn)行測(cè)試,本身也是測(cè)試的一個(gè)重要環(huán)節(jié)。
還拿之前的 Factorial 類來進(jìn)行說明。在 fact() 方法的一開始,對(duì)參數(shù) n 進(jìn)行了校驗(yàn),如果小于 0,則拋出 IllegalArgumentException 異常。
- public class Factorial {
- public static long fact(long n) {
- if (n < 0) {
- throw new IllegalArgumentException("參數(shù)不能小于 0");
- }
- long r = 1;
- for (long i = 1; i <= n; i++) {
- r = r * i;
- }
- return r;
- }
- }
在 FactorialTest 中追加一個(gè)測(cè)試方法 factIllegalArgument()。
- @Test
- void factIllegalArgument() {
- assertThrows(IllegalArgumentException.class, new Executable() {
- @Override
- public void execute() throws Throwable {
- Factorial.fact(-2);
- }
- });
- }
我為你提供了一個(gè) assertThrows() 的方法,第一個(gè)參數(shù)是異常的類型,第二個(gè)參數(shù) Executable,可以封裝產(chǎn)生異常的代碼。如果覺得匿名內(nèi)部類寫起來比較復(fù)雜的話,可以使用 Lambda 表達(dá)式。
- @Test
- void factIllegalArgumentLambda() {
- assertThrows(IllegalArgumentException.class, () -> {
- Factorial.fact(-2);
- });
- }
04、忽略測(cè)試
有時(shí)候,由于某些原因,某些方法產(chǎn)生了 bug,需要一段時(shí)間去修復(fù),在修復(fù)之前,該方法對(duì)應(yīng)的測(cè)試用例一直是以失敗告終的,為了避免這種情況,我為你提供了 @Disabled 注解。
- class DisabledTestsDemo {
- @Disabled("該測(cè)試用例不再執(zhí)行,直到編號(hào)為 43 的 bug 修復(fù)掉")
- @Test
- void testWillBeSkipped() {
- }
- @Test
- void testWillBeExecuted() {
- }
- }
@Disabled 注解也可以不需要說明,但我建議你還是提供一下,簡(jiǎn)單地說明一下為什么這個(gè)測(cè)試方法要忽略。在上例中,如果團(tuán)隊(duì)的其他成員看到說明就會(huì)明白,當(dāng)編號(hào) 43 的 bug 修復(fù)后,該測(cè)試方法會(huì)重新啟用的。即便是為了提醒自己,也很有必要,因?yàn)闀r(shí)間長了你可能自己就忘了,當(dāng)初是為什么要忽略這個(gè)測(cè)試方法的。
05、條件測(cè)試
有時(shí)候,你可能需要在某些條件下運(yùn)行測(cè)試方法,有些條件下不運(yùn)行測(cè)試方法。針對(duì)這場(chǎng)使用場(chǎng)景,我為你提供了條件測(cè)試。
1)不同的操作系統(tǒng),可能需要不同的測(cè)試用例,比如說 Linux 和 Windows 的路徑名是不一樣的,通過 @EnabledOnOs 注解就可以針對(duì)不同的操作系統(tǒng)啟用不同的測(cè)試用例。
- @Test
- @EnabledOnOs(MAC)
- void onlyOnMacOs() {
- // ...
- }
- @TestOnMac
- void testOnMac() {
- // ...
- }
- @Test
- @EnabledOnOs({ LINUX, MAC })
- void onLinuxOrMac() {
- // ...
- }
- @Test
- @DisabledOnOs(WINDOWS)
- void notOnWindows() {
- // ...
- }
2)不同的 Java 運(yùn)行環(huán)境,可能也需要不同的測(cè)試用例。@EnabledOnJre 和 @EnabledForJreRange 注解就可以滿足這個(gè)需求。
- @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、尾聲
最后,給你說三句心里話吧。在編寫單元測(cè)試的時(shí)候,你最好這樣做:
1)單元測(cè)試的代碼本身必須非常名單明了,能一下看明白,決不能再為測(cè)試代碼編寫測(cè)試代碼。
2)每個(gè)單元測(cè)試應(yīng)該互相獨(dú)立,不依賴運(yùn)行時(shí)的順序。
3)測(cè)試時(shí)要特別注意邊界條件,比如說 0,null,空字符串"" 等情況。
希望我能盡早的替你發(fā)現(xiàn)代碼中的 bug,畢竟越早的發(fā)現(xiàn),造成的損失就會(huì)越小。see you!
本文轉(zhuǎn)載自微信公眾號(hào)「沉默王二」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系沉默王二公眾號(hào)。