--- 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
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.