什麼是測試驅動開發
測試驅動開發是指在編寫實作代碼之前先寫測試代碼的開發方式。JUnit的作者Kent Beck說過:編寫測試驅動代碼的重要原因是消除開發中的恐懼和不确定性,因為編寫代碼時的恐懼會讓你小心試探,讓你回避溝通,讓你羞于得到回報,讓你變得焦躁不安,而TDD是消除恐懼、讓Java開發者更加自信更加樂于溝通的重要手段。TDD會帶來的好處可能不會馬上呈現,但是你在某個時候一定會發現,這些好處包括:
- 更清晰的代碼 — 隻寫需要的代碼
- 更好的設計
- 更出色的靈活性 — 鼓勵程式員面向接口程式設計
- 更快速的回報 — 不會到系統上線時才知道bug的存在
TDD可以在多個測試級别上使用,如下表所示:
測試級别 | 描述 |
---|---|
單元測試 | 測試類中的代碼 |
內建測試 | 測試類之間的互動 |
系統測試 | 測試運作中的系統 |
系統內建測試 | 測試運作中的系統包括第三方元件 |
測試驅動開發的例子
現在我們需要一段代碼來計算某個電影放映廳的門票收入,目前的業務規則非常簡單,包括:
- 每張票售價(單價)¥30
- 收入=門票銷售數量*單價
- 放映廳最多容納100人
這裡還有一個假設:目前因為沒有專業的裝置或系統來統計門票銷售的數量,在計算門票收入時,門票銷售數量是由使用者手動錄入的。
TDD的基本步驟是:紅色-綠色-重構。
- 紅色 - 編寫無法通過的測試
- 綠色 - 編寫實作代碼并盡快讓測試可以通過
- 重構 - 重構代碼并再次讓測試通過
接下來我們按照上述步驟完成門票收入計算的功能。
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新的語言特性,有興趣的可以閱讀此書,相信你會從中得到很多收獲。