天天看點

測試驅動開發之初窺門徑

什麼是測試驅動開發

測試驅動開發是指在編寫實作代碼之前先寫測試代碼的開發方式。JUnit的作者Kent Beck說過:編寫測試驅動代碼的重要原因是消除開發中的恐懼和不确定性,因為編寫代碼時的恐懼會讓你小心試探,讓你回避溝通,讓你羞于得到回報,讓你變得焦躁不安,而TDD是消除恐懼、讓Java開發者更加自信更加樂于溝通的重要手段。TDD會帶來的好處可能不會馬上呈現,但是你在某個時候一定會發現,這些好處包括:

  1. 更清晰的代碼 — 隻寫需要的代碼
  2. 更好的設計
  3. 更出色的靈活性 — 鼓勵程式員面向接口程式設計
  4. 更快速的回報 — 不會到系統上線時才知道bug的存在

TDD可以在多個測試級别上使用,如下表所示:

測試級别 描述
單元測試 測試類中的代碼
內建測試 測試類之間的互動
系統測試 測試運作中的系統
系統內建測試 測試運作中的系統包括第三方元件

測試驅動開發的例子

現在我們需要一段代碼來計算某個電影放映廳的門票收入,目前的業務規則非常簡單,包括:

  • 每張票售價(單價)¥30
  • 收入=門票銷售數量*單價
  • 放映廳最多容納100人

這裡還有一個假設:目前因為沒有專業的裝置或系統來統計門票銷售的數量,在計算門票收入時,門票銷售數量是由使用者手動錄入的。

TDD的基本步驟是:紅色-綠色-重構。

  1. 紅色 - 編寫無法通過的測試
  2. 綠色 - 編寫實作代碼并盡快讓測試可以通過
  3. 重構 - 重構代碼并再次讓測試通過

接下來我們按照上述步驟完成門票收入計算的功能。

package com.lovo;

import java.math.BigDecimal;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class TicketRevenueTest {
    private TicketRevenue ticketRevenue;
    private BigDecimal expectedRevenue;

    @Before
    public void setUp() {
        ticketRevenue= new TicketRevenue();
    }

    @Test
    public void oneTicketSoldIsThirtyInRevenue() {
        expectedRevenue = new BigDecimal("30");
        Assert.assertEquals(expectedRevenue,   ticketRevenue.estimateTotalRevenue());
    }
}
           

上述測試代碼不僅不能通過測試,甚至連編譯都無法通過,因為TicketRevenue類還不存在呢。接下來我們可以利用IDE的代碼修複功能(Eclipse和IntelliJ都有這樣的功能)建立出TicketRevenue類以及該類中計算門票收入的estimateTotalRevenue方法。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {

    public BigDecimal estimateTotalRevenue(int i) {
        return BigDecimal.ZERO;
    }

}
           

現在可以運作你的單元測試用例了,但是由于我們還沒有實作真正的業務邏輯,這個測試是不可能通過的,如下圖所示。

測試驅動開發之初窺門徑

但是,迄今為止我們已經完成了“紅色“這個步驟。接下來我們修改TicketRevenue類的estimateTotalRevenue方法來讓測試通過。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) {
        BigDecimal totalRevenue = BigDecimal.ZERO;
        if(numberOfTicketsSold == ) {
            totalRevenue = new BigDecimal();
        }
        return totalRevenue;
    }

}
           

再次運作單元測試,結果如下圖所示。

測試驅動開發之初窺門徑

到這裡,第二個步驟”綠色“就完成了。

接下來我們開始重構TicketRevenue類的代碼。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = ;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) {
        BigDecimal totalRevenue = null;
        totalRevenue = new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
        return totalRevenue;
    }

}
           

重構後的代碼可以根據輸入的門票銷售數量計算出對應的收入,較之之前的硬代碼(hard code)它已經前進了一大步,但是很明顯它沒有考慮到輸入小于0或者大于100的情況。是以我們需要更多的測試例來模拟實際工作環境中可能的輸入,我們對剛才的測試代碼進行了如下改進。

package com.lovo;

import java.math.BigDecimal;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class TicketRevenueTest {
    private TicketRevenue ticketRevenue;
    private BigDecimal expectedRevenue;

    @Before
    public void setUp() {
        ticketRevenue = new TicketRevenue();
    }

    @Test(expected = IllegalArgumentException.class)
    public void failIfLessThanZeroTicketsAreSold() {
        ticketRevenue.estimateTotalRevenue(-);
    }

    @Test
    public void zeroSalesEqualsZeroRevenue() {
        Assert.assertEquals(BigDecimal.ZERO, ticketRevenue.estimateTotalRevenue());
    }

    @Test
    public void oneTicketSoldIsThirtyInRevenue() {
        expectedRevenue = new BigDecimal("30");
        Assert.assertEquals(expectedRevenue, ticketRevenue.estimateTotalRevenue());
    }

    @Test
    public void tenTicketsSoldIsThreeHundredInRevenue() {
        expectedRevenue = new BigDecimal("300");
        Assert.assertEquals(expectedRevenue, ticketRevenue.estimateTotalRevenue());
    }

    @Test(expected = IllegalArgumentException.class)
    public void failIfMoreThanOneHundredTicketsAreSold() {
        ticketRevenue.estimateTotalRevenue();
    }
}
           

再次運作測試會發現5個測試中有兩個無效輸入的測試沒有通過(銷售數量為-1和101的測試),原因很簡單,我們的代碼中還沒有處理無效輸入的代碼。接下來繼續重構我們的代碼。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = ;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) 
            throws IllegalArgumentException {
        BigDecimal totalRevenue = null;
        if(numberOfTicketsSold < ) {
            throw new IllegalArgumentException("門票銷售數量必須大于等于0");
        }
        else if(numberOfTicketsSold > ) {
            throw new IllegalArgumentException("門票銷售數量必須小于等于100");
        }
        else {
            totalRevenue = new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
        }
        return totalRevenue;
    }

}
           

再次運作剛才的測試代碼,檢查一下你的bar是不是綠色的(JUnit的名言是:“Keep your bar green”)。當然,對于有代碼潔癖的人來說,上述代碼仍然稍顯臃腫,沒關系,再來一次重構吧。

package com.lovo;

import java.math.BigDecimal;

public class TicketRevenue {
    private final static int TICKET_PRICE = ;

    public BigDecimal estimateTotalRevenue(int numberOfTicketsSold) 
            throws IllegalArgumentException {
        if(numberOfTicketsSold <  || numberOfTicketsSold > ) {
            throw new IllegalArgumentException("門票銷售數量必須在0到100之間");
        }

        return  new BigDecimal(TICKET_PRICE * numberOfTicketsSold);
    }

}
           

當你完成對代碼的修改後,永遠都不要忘記再來一次剛才的測試,仍然需要Keep your bar green。

如果我們使用面向對象的程式設計範式,那麼對代碼的重構應當遵循面向對象的設計原則。大神Robert Matin将這些原則總結為SOLID原則。

原則 英文 描述
單一職責原則(S) Single Responsibility Principle 每個對象隻做自己該做的事情
開閉原則(O) Open-Closed Principle 接受擴充但不接受修改
裡氏替換原則(L) Liskov Substitution Principle 可以用子類型替換父類型
接口隔離原則(I) Interface Segregation Principle 接口要小而專
依賴倒轉原則(D) Dependency Inversion Principle 依賴接口而不依賴實作
說明:上面的例子來自The Well-Grounded Java Developer一書(中文名《Java程式員修煉之道》),這本書覆寫了Java開發中很多實用的技術以及Java新的語言特性,有興趣的可以閱讀此書,相信你會從中得到很多收獲。