--- title: '設計 & 可測試性' disqus: kyleAlien --- 設計 & 可測試性 === ## Overview of Content [TOC] ## 可測試性 對於 設計 首先我們要知道寫出一個有測試性的程式並不代表這是一個好的設計,設計與測試是兩種不同的技能,但是可以保證 **一個好的測試,對於設計來說事件好事(大多數)** :::info 好的測試會遵循到設計的六大原則之 **依賴倒置**,它物件導向的一大原則,利於設計、測試,將其作到極致就成了 TDD 開發模式 ::: ### 可測試性的設計 - 特色 * 可測試性的設計它容易替換程式碼中的邏輯,並快速完成一個單元測試,並且這些單元測試有以下特點 > 了解特色可以幫助我們判定當前的設計是否符合可測試性,並且 **往往有好的可測試性會代表這是一個靈活性佳的設計** * 執行速度快 * 測試之間相互隔離(不會有關連性) * 不需要額外進行外部設定,並且不依賴外部狀態(可自己做出假物件模擬狀況) * 測試結果穩定(失敗就是失敗,成功就是成功,不會有所謂的機率性) :::success * **FICC 特性**? FCCI 分別代表 4 個特性:F (`Fast`)、I (`Isolated`)、C (`Configuration free`)、C (`Consistent`),好的測試會包含這幾點 ::: ### 可測試性的設計 - 目標 * 知道可測試性的特色後,我們就要了解如何結合你所知的設計做出一個可測試性的程式;**設計要有可測試性,就需要有 ++接縫++** :::info **接縫** 的概念常與 `Open-Closed Principle` 一起討論 ::: 下表為可測試的設計指南(可以連同 OOP 6 大設計準則思考),與其優點 | 可測試的設計指南 | 對於測試的優點 | | - | - | | 將方法設定為抽象方法 | 對於界面來說,可以透過對外的契約進行溝通,而不是依賴於實做方案;對於抽象來說方便我們去繼承並複寫假物件 | | 使用界面導向設計 | 方便模擬、替換假物件 | | 預設情況下,類可被繼承 | **這點有些許的爭議(有關於安全性)**,這我們之後會提及;但是對於設計來說就可以大大加強了可測試性 | | 避免在方法執行中直接創建物件,或靜態物件 | 在方法中創建物件會阻礙我們注入假物件測試,這可以透過多種方法來避免 | | 避免直接使用靜態方法 | 同上,它不便於替換 | | 避免在建構函數中插入過多的邏輯 | 因為建構函數很難被覆寫,也就不方便我們做出假行為 | | 把單例的行為界面、單例實做者分離 | 同樣我們依賴於單例的行為界面,更方便我們做測試行為 | 接下來我們會針對這幾點提出幾個例子來說明 * 可測試的設計指南 1. 預設為抽象方法 抽象方法方便我們去替換待測試方(SUT)依賴的某個物件,以下有兩種常見的方案,這兩個方案都可協助待測試方(SUT)替換物件 1. **擷取、複寫** * 待測試(SUT)類 ```kotlin= open class SUTClazz { open fun getUtils() : Utils { return Utils() } fun login(passwd: String) : Boolean { if (getUtils().isValid(passwd)) { return true } throw Exception("Invalid passwd") } } ``` * 相依物件 ```kotlin= class Utils { fun isValid(str: String) : Boolean { return str.length > 10 } } ``` * 測試:**透過繼承替換相依物件,做出假物件來模擬行為** ```kotlin= @Test fun login_ValidLogin_ReturnTrue() { val stubUtils = mockk<Utils>(relaxed = true).apply { every { isValid(any()) }.returns(true) } val sut = object : SUTClazz() { override fun getUtils(): Utils { return stubUtils } } assertTrue { sut.login("No matter") } } ``` 2. **代理(委派)** * 待測試(SUT)類 ```kotlin= open class SUTClazz2 { // 可替換的判斷 var isValid : (String) -> Boolean = { Utils().isValid(it) } fun login(passwd: String) : Boolean { if (isValid(passwd)) { return true } throw Exception("Invalid passwd") } } ``` * 測試:**透過替換關鍵判斷(可能是代理、委派),做出假物件來模擬行為** ```kotlin= @Test fun login_ValidLogin_ReturnTrue() { val sut = SUTClazz2().apply { isValid = { true } } assertTrue { sut.login("No matter") } } ``` 2. **使用界面導向** 除了資料物件(`data class`)以外,**類與類之間的依賴關係應該依賴於界面,這符合 OOP 設計中的依賴倒置(DIP**) 3. **預設情況下將類別可設計** 考慮到將類別不可繼承會可能是 **基於想要完全控制這個類的發展**(安全性),但是讓這個類別不方便去測試 4. **避免在方法執行中直接創建物件,或靜態物件** 在有邏輯的方法中去創建一個新物件來使用是非常普片的事情,不過對於測試來說就相當不友善,我們無法很好的操控該方法中的物件來模擬狀況 > 這有很多種方案可以避免,改用注入、繼承複寫、建構函式、工廠類提供... 等等 ```kotlin= fun bad_login(passwd: String) : Boolean { // 盡量避免這種狀況 if (Utils().isValid(passwd)) { return true } throw Exception("Invalid passwd") } ``` 5. **避免直接使用靜態方法** 大多數的抽象方法,不方便於替換(不非所有框架都支持),就算替換了其代價也較高(不易於維護的測試),所以建議盡量不要直接呼叫靜態方法 6. **避免在建構函數中插入過多的邏輯** 在測試中很難去模擬,注意建構函數中的邏輯,這不便一個可維護性的測試 7. **把單例的行為界面、單例實做者分離** 這裡也可以說成依賴於界面,而不是單例容器,範例如下 * 單例界面 ```kotlin= interface IUtils { fun isValid(str: String) : Boolean } ``` > 對於使用者,同樣依賴於單例的抽象,而不是實體 * 單例邏輯 ```kotlin= class UtilsReal: IUtils { override fun isValid(str: String) : Boolean { return str.length > 10 } } ``` > 對於單例責任真正的邏輯專門寫測試,**不必混和單例的功能測試** * 單例持有者 ```kotlin= class SingleUtilsHolder private constructor() { companion object { private val utils = UtilsReal() fun getUtils() : UtilsReal { return utils } } } ``` > 對於單例的邏輯可以另外寫測試 :::warning * 關於這種寫法並非每一個設計者都同意,同樣是基於安全性,**要使用這樣的寫法必須是同團隊內的使用者共同遵守這個開發規範!** ::: ## 可測試性的利弊 * **可測試性程式 - 優點** * **對 API 的深度考量**:**站在使用者角度來設計程式 API,對於使用者體驗來說更加好,也更加全面** > 同時也會讓你對於設計出的 API 思考更深(你要避免重複功能、不斷修改、不明確的 API 特徵、甚至是取名... 等等問題) * **可拓展**:基於抽象化方便測試的角度(才可以插入間隙),程式自然好拓展 * **隔離性**:由於要建立各種假物件,你在撰寫程式時自然會有隔離度 * **可測試性程式 - 缺點** * **工作量**:測試是開發者站在使用者立場測試自己開發的程式,這必定會提高工作量 > 但相對來講,你對於你的程式會更有自信(當然~ 因為你自己測試了一次,也嘗試站在使用者角度使用自己的開發模型) * **複雜度**:為了測試我們必須抽象化物件,而 **抽象化物件對於程式本身是加深了複雜度** * **敏感資訊**:有時候我們必須隱藏商業機密(這個機密包括不對外暴露 API 特性),這時就得另外想辦法繞過這塊程式 ### 可測試性 & 設計 - 關聯性 * **==好的可測試程式,不代表是一個好的設計==!** 兩者有一定的關聯性,**好的測試會有好的拓展性、可讀性,這是設計的幾個特點,但並不代表它就是一個符合商業邏輯的好設計**! > 如何設計? > > 接收到需求後,進行分析、了解,最終設計出一個符合商業邏輯又有限度的擴展,才是一個好的設計 * **個人總結**:**設計先行,遵循設計的 6 大原則,可以得到一個可測試性的程式**(**可測試程式是遵守設計原則後的副作用,並非設計的本質目的**!!) :::info * 不受限測試框架,是否還需要考慮可測試性? 對於測試來說是不需要特別考慮抽象化;但是對於設計來說就並非這樣說,**設計仍有抽象化的需求,好讓程式容易維護、拓展** ::: ## Appendix & FAQ :::info ::: ###### tags: `Test`