--- title: 'TDD 測試驅動開發' disqus: kyleAlien --- TDD 測試驅動開發 === ## Overview of Content TDD (Test-driven development) 測試驅動開發,簡單來說就是 **先寫測試再寫程式 (可以說是測試為主體,程式是為了滿足測試);這樣先寫測試有甚麼優點呢 ?** [TOC] ## TDD 概述 我們也知道 **測試案例需要從失敗案例開始撰寫**;TDD 也是遵循這個概念,所以 TDD 在撰寫時有以下固定的步驟 1. **紅燈**:依照需求,先撰寫失敗案例 2. **綠燈**:撰寫剛好可以通過測試的程式(滿足以測試為主體) 3. **重構**:在不更改需求的情況下,重構程式 > 這裡的重構是指優化程式,而不修改邏輯 (例如可以讓程式看起來更有可讀性... ) >  ### TDD 關注重點 * 在使用 TDD 開發程式時我們可以發現以下特點,我們也要在撰寫程式時同時去思考、顧及以下這幾個點 (如同 OOP 的六大基礎概念) | TDD 特點 | 優點 | 說明 | | -------- | -------- | - | | 小步快跑 | **一次只關注一件事** | TDD 的開發方式非常滿足敏捷開發的概念,小量快速迭代,有問題時可以快速解決 (大量會導致不易追蹤) | | 維持綠燈 | 確保程式的穩定性 | | | 先分析再測試 | **仔細思考需求** | 由於我們在撰寫單元測試時,會將方法切分才可以測試,如果這時 **先寫測試就可以先去思考如何拆分需求 (函數)** | | 僅在測試失敗時撰寫新的程式 | 如同寫測試時,先寫錯誤案例一樣 | | | 重構 | 讓程式碼更簡潔、或更有可讀性 | 重構看似可有可無,不過這對於優質程式是相當重要的 | ### TDD 無法作到的事情 * TDD 簡單來說就是測試先行,但僅僅是測試先行我們也不能保證以下的事情 1. 無法保證測試是可維護、可讀、可靠的 2. 如果你作到了測試的可維護、可讀、可靠,也不能保證你能獲得測試先行的好處 3. 如果你作到了測試的可維護、可讀、可靠,並不能保證你設計出一個可靠、穩定的系統 :::success 簡單來說,測試能力並不等同於你的程式設計能力 ::: ## TDD 開發 接著我們會以一個簡單的案例來示範 TDD 要如何去開發 :::info * 需求案例 **目標**:計算吃火鍋花費的價格 **條件**:內用 300 元,外帶打 9 折 ::: ### 撰寫失敗案例 - 紅燈 * 首先思考目標、條件 1. 目標是要驗證價格 2. 由於這裡有一個條件,所以們我需要寫兩個單元測試,而這個條件我選用 DI 注入接口 * 驗證內用的價格 1. **使用 `mockk` 來創建假物件**:這裡我們就需要一個 `IUseType` interface (DI 注入這個 ) ```kotlin= @Test fun test_HotPotCost_ForHere() { val stubForHere = mockk<IUseType>() every { stubForHere.forHere() }.returns(true) // 目前案例是寫內用,所以返回 true } ``` :::success * 可以使用 Android Studio 的快捷鍵 (`Alt` + `Enter`) 來快速創建類 1. 創建 interface >  2. 指定檔案路徑 >  3. 選擇指定路徑 >  4. 用同樣的快捷鍵創建 `forHere` 函數 >  ::: 2. **創建 HotPot 類、靜態成員、待測方法**:也就是 SUT 類 & 方法 (待測目標) > 同樣使用上面的快捷鍵方法創建 ```kotlin= @Test fun test_HotPotCost_ForHere() { val stubForHere = mockk<IUseType>() every { stubForHere.forHere() }.returns(true) val exceptCost = HotPot.DEFAULT_COST_300 // 靜態成員 val sut = HotPot() // 創建 SUT 類 val realCost = sut.getCost(stubForHere) // 待測方法 assertEquals(exceptCost, realCost) } ``` 關於 SUT 的待測方法,先不去實做,在之後的綠燈通行才去實作它 ```kotlin= // SUT 對象 class HotPot { fun getCost(stubForHere: IUseType) : Int { TODO("Not yet implemented") } companion object { const val DEFAULT_COST_300: Int = 300 } } ``` 3. 運行測試:預計得到一個錯誤結果 "Not yet implemented" >  * 驗證外帶的價格:TDD 運行方式如上,只需要修改 `forHere`,返回 false 即可;同樣要驗證出錯誤 ```kotlin= @Test fun test_HotPotCost_TakeOut() { val stubForHere = mockk<IUseType>() every { stubForHere.forHere() }.returns(false) val exceptCost = HotPot.DEFAULT_COST_300 val sut = HotPot() val realCost = sut.getCost(stubForHere) assertEquals((exceptCost* 0.9).toInt(), realCost) } ``` >  ### 通過測試案例 - 綠燈 * 當測試案例寫完後 (目前是錯誤 **不通過的狀態**),我們就要來修改程式,讓其恰好可以通過測試案例 ```kotlin= class HotPot { // 準備修改這個函數,讓其通過測試 fun getCost(stubForHere: IUseType): Int { TODO("Not yet implemented") } companion object { const val DEFAULT_COST_300: Int = 300 } } ``` * **撰寫滿足測試的程式**:`getCost` 函數修改邏輯後如下 ```kotlin= class HotPot { fun getCost(stubForHere: IUseType): Int { // 撰寫滿足測試的程式 if (stubForHere.forHere()) { return DEFAULT_COST_300 } return (DEFAULT_COST_300 * 0.9).toInt() } companion object { const val DEFAULT_COST_300: Int = 300 } } ``` * 運行兩個測試案例,這時可以發現測試都通過;我們也就以最小的步伐去滿足了需求 (小步快跑) >  :::warning 如果測試錯誤的話就必須回頭去觀察,是需求的測試出了問題,還是實現的程式出了問題 ::: ### 程式優化 - 重構 * 最後當測試通過、程式也完成後,可以在針對程式做優化 ```kotlin= // 以下修正讓程式更簡潔 fun getCost(stubForHere: IUseType): Int { return if (stubForHere.forHere()) { DEFAULT_COST_300 } else { (DEFAULT_COST_300 * 0.9).toInt() } } ``` **優化完後記得在運行一次測試,確保沒有修改到程式邏輯** (有可能會不小心改錯) >  ## Appendix & FAQ :::info ::: ###### tags: `Test`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up