TDD(測試驅動開發)死了嗎?

0一、前言

很早以前,曾在網絡上見到過 TDD 這 3 個大寫的英文字母,它是 Test Driven Development 這三個單詞的縮寫,也就是「測試驅動開發」的意思——聽起來很不錯的一種理念。java

其理念主要是確保兩件事:程序員

  • 確保全部的需求都能被照顧到
  • 在代碼不斷增長和重構的過程當中,能夠檢查全部的功能是否正確

但後來很長一段時間裏,都沒再聽過 TDD 的消息。有人說,TDD 已經死了,給出的意見以下:小程序

1)一般來講,開發人員不該該在沒有失敗的測試用例下編寫代碼——這彷佛是合理的,可是它可能致使過分測試。例如,爲了保證一行生產代碼的正確性,你不禁得寫了 4 行測試代碼,這意味着一旦這一行生產代碼須要修改,你也得修改那 4 行測試代碼。網絡

2)爲了遵循 TDD 而寫的代碼,容易進入一個誤區:代碼是爲了知足測試用的,而忽略了實際需求。性能

0二、TDD 究竟是什麼?

無論 TDD 到底死了沒有,先讓咱們來回顧一下 TDD 究竟是什麼。單元測試

TDD 的基本思想就是在開發功能代碼以前,先編寫測試代碼。也就是說在明確要開發某個功能後,首先思考如何對這個功能進行測試,並完成測試代碼的編寫,而後編寫相關的代碼知足這些測試用例。而後循環進行添加其餘功能,直到完成所有功能的開發測試

TDD 的基本過程能夠拆解爲如下 6 個步驟:優化

1) 分析需求,把需求拆分爲具體的任務。spa

2) 從任務列表中取出一個任務,並對其編寫測試用例。3d

3) 因爲沒有實際的功能代碼,測試代碼不大可能會經過(紅)。

4) 編寫對應的功能代碼,儘快讓測試代碼經過(綠)。

5) 對代碼進行重構,並保證測試經過(重構)。

6) 重複以上步驟。

能夠用下圖來表示上述過程。

0三、TDD 的實踐過程

一般狀況下,咱們都習慣在需求分析完成以後,儘快地投入功能代碼的編寫工做中,以後再去調用和測試。

而 TDD 則不一樣,它假設咱們已經有了一個「測試用戶」了,它是功能代碼的第一個使用者,儘管功能代碼還不太完善。

當咱們站在「測試用戶」的角度去寫測試代碼的時候,咱們要考慮的是,這個「測試用戶」該如何使用功能代碼呢?是經過一個類直接調用方法呢(靜態方法),仍是構建類的實例去調用方法呢(實例方法)?這個方法如何傳參呢?方法如何命名呢?方法有返回值嗎?

有了測試代碼後,咱們開始編寫功能代碼,而且要以最快地速度讓測試由「紅」變爲「綠」,可能此時的功能代碼很不優雅,不過不要緊

當測試經過之後,咱們就能夠放心大膽的對功能代碼進行「重構」了——優化原來比較醜陋、臃腫、性能誤差的代碼。

接下來,假設咱們接到了一個開發需求:

汪汪隊要到小鎮冒險島進行表演,門票爲 99 元,冒險島上惟一的一個程序員王二須要開發一款能夠計算門票收入的小程序。

按照 TDD 的流程,王二須要先使用 Junit 編寫一個簡單的測試用例,測試預期是:銷售一張門票的收入是 99 元。

public class TicketTest {
	
	private Ticket ticket;

	@Before
	public void setUp() throws Exception {
		ticket = new Ticket();
	}

	@Test
	public void test() {
		BigDecimal total = new BigDecimal("99");
		
		assertEquals(total, ticket.sale(1));
	}

}
複製代碼

爲了便於編譯可以順利經過,王二須要一個簡單的 Ticket 類:

public class Ticket {

	public BigDecimal sale(int count) {
		return BigDecimal.ZERO;
	}

}
複製代碼

測試用例運行結果以下圖所示,紅色表示測試沒有經過:預期結果是 99,實際結果是 0。

那接下來,王二須要快速讓測試經過,Ticket.sale() 方法修改後的結果以下:

public class Ticket {

	public BigDecimal sale(int count) {
		if (count == 1) {
			return new BigDecimal("99");
		}
		return BigDecimal.ZERO;
	}

}
複製代碼

再運行一下測試用例,結果以下圖所示,綠色表示測試經過了:預期結果是 99,實際結果是 99。

綠了,綠了,測試經過了,到了該重構功能代碼的時候了。99 元是個魔法數字,至少應該聲明成常量,對吧?

public class Ticket {
	private final static int PRICE = 99;
		
	public BigDecimal sale(int count) {
		if (count == 1) {
			return new BigDecimal(PRICE);
		}
		return BigDecimal.ZERO;
	}

}
複製代碼

重構完後再運行一下測試用例,確保測試經過的狀況下,再增長几個測試用例,好比說門票銷量爲負數、零甚至一千的狀況。

public class TicketTest {
	
	private Ticket ticket;

	@Before
	public void setUp() throws Exception {
		ticket = new Ticket();
	}

	@Test
	public void testOne() {
		BigDecimal total = new BigDecimal("99");
		
		assertEquals(total, ticket.sale(1));
	}
	
	@Test(expected=IllegalArgumentException.class)
	public void testNegative() {
		ticket.sale(-1);
	}
	
	@Test
	public void testZero() {
		assertEquals(BigDecimal.ZERO, ticket.sale(0));
	}
	
	@Test
	public void test1000() {
		assertEquals(new BigDecimal(99000), ticket.sale(1000));
	}

}
複製代碼

銷量爲負數的時候,王二但願功能代碼可以拋出異常;銷量爲零的時候,功能代碼的計算結果應該爲零;銷量爲一千的時候,計算結果應該爲 99000。

從新運行一下測試用例,結果以下圖所示:

有兩個測試用例沒有經過,那麼王二須要繼續修改功能代碼,調整以下:

public class Ticket {
	private final static int PRICE = 99;

	public BigDecimal sale(int count) {
		if (count < 0) {
			throw new IllegalArgumentException("銷量不能爲負數");
		}
		
		if (count == 0) {
			return BigDecimal.ZERO;
		}

		if (count == 1) {
			return new BigDecimal(PRICE);
		}

		return new BigDecimal(PRICE * count);
	}

}
複製代碼

再運行一下測試用例,發現都經過了。又到了重構的時候了,銷量爲零、或者大於等於一的時候,代碼能夠合併,因而重構結果以下:

public class Ticket {
	private final static int PRICE = 99;

	public BigDecimal sale(int count) {
		if (count < 0) {
			throw new IllegalArgumentException("銷量不能爲負數");
		}
		
		return new BigDecimal(PRICE * count);
	}

}
複製代碼

重構結束後,再運行測試用例,確保重構後的代碼依然可用。

0四、最後

從上面的實踐過程能夠得出以下結論:

TDD 想要作的就是讓咱們對本身的代碼充滿信心,由於咱們能夠經過測試代碼來判斷這段代碼是否正確無誤。

也就是說,TDD 流程比較關鍵的一環在於如何寫出有效的測試代碼,這裏有 4 個原則能夠參考:

1)測試過程應該儘可能模擬正常使用的過程。

2)應該儘可能作到分支覆蓋。

3)測試數據應該儘可能包括真實數據,以及邊界數據。

4)測試語句和測試數據應該儘可能簡單,容易理解。

注意,這 4 個原則不只適用於 TDD,一樣適用於任何流程下的單元測試。

最後,我想說的是,無論 TDD 有沒有死,TDD 都不是銀彈,不可能適合全部的場景,但這不該該成爲咱們拒絕它的理由。

相關文章
相關標籤/搜索