---
# System prepended metadata

title: 單元測試 - 概念
tags: [Test]

---

---
title: '單元測試 - 概念'
disqus: kyleAlien
---

單元測試 - 概念
===

## OverView of Content

在學習真正的單元測試之前最好先看過一遍單元測試基礎概念、定義，就如同你在學演算法時對於演算法 BIGO 的定義，OOP 中的六大設計原則


這些概念在腦袋中有印象後，你在撰寫程式時會不斷的去驗證你的想法（重複思考），最終你會越來越熟悉並熟捻於心，最終產生自己的想法

[TOC]

## 單元測試

單元測試是開發人員提昇程式品質、思考產品細節、深入理解類別 ＆ 方法的最佳方法之一，所以當然值得我們好好學習

### 單元測試的重要性 - 為何要學？保障了什麼？

* 在學習前我們總得知道為甚麼要學 (這樣比較有動力)，前面已經簡單說明了什麼是單元測試，那 **為甚麼要學單元測試** ?

    1. 對於自己撰寫的程式更有自信
    
    2. 減少程式出錯的機會 (並不是寫了單元測試就一定不會出錯 !)
    
    3. 不用花一堆時間在 Debug ! (去做點更有意義的研究 或 耍廢 ?)

    :::warning
    * 有時候你會覺得單元測試很蠢，這麼簡單還要測 ? 但很多時候我們就是犯了一堆很蠢的錯誤 !
    :::

* 重要性

    * **回憶初衷**：因為我們不可能一直記得當初設計這個類得初衷，藉由測試可以快速回憶

    * **純粹性、完備性**

    * **保證程式的可行性**：單元測可以保證幾個月前的程式可以正常運作
        
        :::danger
        * **單元測試保障了函數的契約**
            
            契約是該函數當初許下的承諾，這份「承諾契約」無法透過編譯器強制驗證使用者實現，所以這時可以透過「單元測試」來測定函數的契約
        :::
        
        :::success

        * 往往我們在撰寫一個功能時可以保證該功能的可行性，但是類與類之間多多少少存在著耦合關係，這會導致 **我們可能會為了完成這個功能去修改令一個類，但類 B 也相同依賴類 A，最終導致功能確實做完了，但類 B 壞掉了**

            > ![](https://i.imgur.com/fCRpOhX.png)
        :::

### SUT - System Under Test

* SUT 通常就是用來指被測試的目標（系統），所以如果在單元測試中看到 SUT 關鍵字，就可以理解是測試目標

    :::info
    有些人會用 CUT (Code Under Test, Code Under Test) 來命名被測試目標，跟 SUT 是同樣意思
    :::


### 何謂單元？

* 所謂的單元並沒有一定的定義，它是相對的定義，當你站在不同角度來看代同一見事物會有不同的定義；

    :::warning
    * **相對的定義**：
    
        對於醫生來說人類的內臟可能就可以稱為一個單元，對於生物學家來說一個細胞才是一個單元
    :::
    
* 以程式來說，完整的提供一系列功能的稱為 **類** (多個相關功能)，而在類中更小一點的單元稱之為 **函數**（一個函數代表了一個功能）；因此 **對於程式來說，一個函數就是一個單元**
    
    > 不糾結於函數內細節，功能就代表了一個單元，**也就是我們在驗證一個具體結果**
    

### 單元測試 ＆ 整合測試

* 看看下表可以很清楚的比較出兩種測試的差異

    |    | 依賴外部實體 | 速度 |特色 |
    | -------- | ------- | - | -------- |
    | 單元測試 | No    | 快 | 一個改變情況 測試一個結果 |
    | 整合測試 | Yes   | 不一定 | 多個改變情況（多個測試目標） 測試一個結果 (無法清楚定位任何一種狀況下的失敗) |
    
    > 整合測試有不可控的可能性
    
    :::info
    * **依賴外部實體**？
    
        也就是具體的實體資料，可能像是資料庫實時資料，當前時間... 等等，就算是依賴外部實體 （不能作假物件測試）
    :::


### 怎樣的單元測試才算優秀？

* 根據 **單元測試的藝術** 這本書，優秀的單元測試應該要有以下特質 （我合併了幾個點）

    1. 可自動化
    
    2. 容易被實現：你看完程式後應該可以滿快的寫出一個測試
    
    3. 可重複執行：每次執行結果應該都要一致
    
    4. 它的存在是有意義的：這個程式值得被重複執行，來驗證程式的可靠度
    
    5. 任何人都可執行並得到相同的結果
    
    6. 可模擬的物件
    
    7. 獨立性：不依賴外部或是不穩定的來源
    
    8. 失敗時清楚的知道失敗原因


### 單元測試原則

* 在撰寫單元測試時我們一般會依照以下原則，如同我們在學習 OOP 時的六大原則一樣；這些原則並非一定要遵守，不過它可以用來提醒我們

    | 原則 | 說明 | 補充 |
    | -------- | -------- | - |
    | Fast | 單元測試的執行盡量不要拖延時間，如果執行速度慢，極有可能之後就 (懶) 不去執行 |
    | Independent | 測試案例之間的相依性，每個測試案例不應該相互依賴，**一個測試一個責任** | 其中也不與外部相依 (網路、資料庫... 等等) |
    | Repeatable | 每個測式都是可被重複執行的，並且 **每次執行的結果都不須相同** |  |
    | Self-Validating | 每個測式結果都能清楚知道失敗原因 ! | 保持錯誤的清晰度，不可模糊的驗證 |
    | Timely | 寫測試要如同寫程式，要即時快速不拖延 | 如同 TDD 開發，最少也要在 commit 之前寫完測試 |

* 另外還有要注意以下事項

    1. 單元測試中不應該存在判斷，否則就不能稱為單元測試
    
    2. 測試應該要簡單、清晰，一出錯甚至不用 debug 就知道問題點，也盡量不要有 for 迴圈... 等等複雜邏輯


### 單元測試的時機

* 通常單元是的時機就是在判斷的時候，像是 **if/else, switch, loop ... 等等都是單元測試可以測試的時機**；如下範例

    ```kotlin=
    fun isValid(str: String) : Boolean {

        if(str.length > 10) {        // 測試點
            return false
        }

        if(str.startsWith("A")) {        // 測試點
            return false
        }

        return true        // 測試點
    }
    ```
    
* 其中除了判斷以外還有一些點需要依據業務邏輯去測試，像是測試 **設定值** (雖然他不是判斷，但是對於進入該函數後會對傳入的類進行修改，這就可以測試)

    ```kotlin=
    fun downloadFirmware(setting: Setting) {

        setting.apply {
            handleWay = AUTO_DOWNLOAD        // 測試點
        }

    }
    ```

## 簡單的單元測試

一般我們在撰寫 單元測試 時，常常會使用到測試框架，這裡我們跳脫框架，看看 **如果沒有測試框架，那又該如何達成單元測試**


### 單元測試 Sample - Without 框架

```kotlin=
// 測試以下程式

class Login {
    
    fun isValid(str: String) : Boolean {

        if(str.length > 10) {
            return false
        }

        if(str.startsWith("A")) {
            return false
        }

        return true
    }
    
}
```
* 接著不使用單元測試框架，直接在開發資料夾內來撰寫測試；以下如果有無法通過的測試就往外拋出 Exception 來代表錯誤

    ```kotlin=
    class LoginTests {

        companion object {
            fun isValid_StrLenMoreThan10_False() {

                if (Login().isValid("Hello World Tests.")) {
                    throw Exception("Str length more than 10 should return false")
                }
            }

            fun isValid_StrStartsWithA_False() {

                if (Login().isValid("AHello")) {
                    throw Exception("When str start with `A` should return false")
                }
            }

            fun isValid_StrLenLessThan10_NotStartWithA_False() {

                if (Login().isValid("Hello")) {
                    return
                }
                throw Exception("Str length len less than 10 and not start with `A`, it should return true.")
            }
        }

    }
    ```

    使用 main 開始測試

    ```kotlin=
    fun main() {
        LoginTests.isValid_StrLenMoreThan10_False()
        LoginTests.isValid_StrStartsWithA_False()
        LoginTests.isValid_StrLenLessThan10_NotStartWithA_False()
    }
    ```
    > 測試通過


* 從沒有框架的測試中我們可以發現幾個缺點

    1. 每個測試之間無法隔離，測試失敗時可不方便尋找，錯誤不夠清晰！
        
        ```kotlin=
        fun main() {
            // 可能是它失敗？
            LoginTests.isValid_StrLenMoreThan10_False()
            
            // 可能是它失敗？
            LoginTests.isValid_StrStartsWithA_False()
            
            // 可能是它失敗？
            LoginTests.isValid_StrLenLessThan10_NotStartWithA_False()
        }
        ```
    
    2. 每次都必須全部執行，測試無法單獨執行！
        
        ```kotlin=
        fun main() {
            // 必須一起執行，無法分開
            LoginTests.isValid_StrLenMoreThan10_False()
            
            LoginTests.isValid_StrStartsWithA_False()
            
            LoginTests.isValid_StrLenLessThan10_NotStartWithA_False()
        }
        ```


## 其他

### 回歸 regression

* 回歸 regression：代表了以前正常運作但是現在無法正常運作的程式

    :::info
    * 回歸 regression 測試就是在運作跟當前要測試的目標無關的測試；而單元測試就可以做到這點（保證了其他代碼的正確邏輯性）    
        
        > 可以 **用來驗證其他單元沒有被這次修改的程式影響**
    :::


### Legacy Code

* **Legacy Code 意思是 ++遺留程式碼++**，也可以稱為歷史程式（不在使用的程式碼）
    
    > 但通常 Legacy Code 很難跟目前的程式做切割


    :::info
    也有人稱把 `只是能動的程式碼` 稱為 Legacy Code
    :::

### 過度指定 over-specification

* 我們在測試時指需要指定該測試需要的內容即可，若與該測試無關的指定（假設）我們應該要忽略，避免混淆測試的環境（也不方便之後閱讀）

    
    ```kotlin=
    interface ICost {
        fun getCost() : Int

    }
    
    // 該測試指需要　ICost 的假設，其他的則不須在意
    fun camBooking(cost : ICost) : Boolean {
        val userCost = cost.getCost()

        return userCost <= 1000
    }
    ```
    
## Appendix & FAQ

:::info
:::

###### tags: `Test`
