--- title: '好的單元測試' disqus: kyleAlien --- 好的單元測試 === ## Overview of Content [TOC] ## 好的單元測試 - 標準 * 如何定義一個好的單元測試,可以用以下幾個特色來檢視這個單元測試是否優秀(一個優秀的單元測試必須包含所有的特色) 1. **可信賴(Trustworthiness)**: 對於跑過完的單元測試結果有信心!你可以 **完全信賴這個單元測試(也就是說這個測試本身沒有 Bug)** 2. **可維護性**: 無法維護的測試可能包含,閱讀性差,測試之間關聯性重,或是由於業務邏輯導致測試頻繁的被修改(並且修改的代價重!) > 基本上沒有可維護性,漸漸的該測試程式就不會有人想要繼續維護、修復 3. **可讀性**: 可讀性是基本的基本,失去可讀性人們很容易會覺得這個程式難以維護,因為連看都很難看懂(排除對測試框架不熟悉的人),自然這些測試就會讓人覺得不可信任 ## 創建可信賴的測試 要創建一個可信賴的測試程式(這裡指 Unit Test),有一些基礎原則、技術,如下: * 刪除、修改測試時機點,修改後又該如何驗證 * 測試中不攜帶邏輯概念 * 一次測試指關注一個點 * 區分單元測試(綠色區域)、整合測試 > 詳細請看 [**測試分類**](https://hackmd.io/9XopWYSbQC-7eTEDTb9Jsw?view#%E6%B8%AC%E8%A9%A6%E5%88%86%E9%A1%9E) * 測試的審查(`Code review`、`Pair coding`) ### 修改測試時機 * 這首先要先了解你 ^1.^為何需要修改程式? 再了 ^2.^解修改了測試後會造成啥問題? 最後修改之後我們要 ^3.^如何去驗證? * 我們從 **為何需要修改程式? 開始看**,以下是我們可能去修改測試的原因 1. **產品程式 Bug**: 修改一個產品程式碼,導致原有的測試項目失敗,代表了你的測試找到了修改產品後的 Bug(好事 ~),這時 **要修改的不是測試,而是修改程式,讓程式符合測試** > 修改程式讓其符合測試,這個動作類似於 TDD 的技術 2. **測試程式本身 Bug**: **測試本身存在 Bug 這是很難被發現的問題**(有可能你收到 crash report 時,去驗證測試沒問題並有包裹到,這時你才會發現 測試程式本身 Bug) :::info * 修改步驟如下 1. 修復產品程式 Bug:修復成正確邏輯 2. 第一關,撰寫失敗的測試:驗證失敗動作是否真的能失敗(如果你連失敗都做不到?那代表這個測試問題很大!必須重新 回到第一點 修改) 3. 第二關,撰寫成功的測試:最後驗證正確的商業邏輯 ::: 3. **API 界面修改,但測試目標沒變**: 這個狀況很 **常發生在我們重構程式後**(有可能是 API 修改了參數、或是類的使用方式修改),再次運行單元測試導致單元測試一系列的失敗; 這時可以在程式中設計一個隔離層(重複利用的部份),隔離測試程式本身、產品程式,當改動時你只需要跳整隔離層;如下圖 > ![](https://i.imgur.com/f5Oi7kv.png) 4. **商業邏輯修改導致 矛盾、無效 的測試**: 對於同一段程式碼的測試,新寫的測試通過,但舊的程式失敗,這時就產生了商業邏輯上的矛盾! :::warning * 這的問題的出現,可以很好的反應出修改的以前的(修改前)商業邏輯跟後來的(修改後)商業邏輯有衝突、產生了矛盾! > 這時就可以需求向反應並再次確認 ::: 5. **重複的測試**:有時候我們會不小心對同一段程式進程測試(有可能是不同人撰寫、或是測試的手法不同),這時就可以 **考慮** 刪除重複的測試 > 這並非必須 * 重複測試的優點: * 不同的測試手法,越有可能發現 Bug * 閱讀測試程可以看到同一個測試目標有不同的設計、語意 * 重複測試的缺點: * 維護成本變高 * 測試品質參差不齊,不易閱讀 * 對於測試失敗時的定位會被混淆 * 取名不易 鑑於缺點稍多一些,所以還是建議刪除重複的測試(留下更優良、全面的單元測試,多餘但同樣測試目標的測試就可以刪除) ### 測試 - 不帶邏輯 * **有了邏輯,這個就不再是單元測試,而是整合測試**;單元測試簡單並且容易定位錯誤,而整合測試充滿了不確定因素、無法經確快速定位錯誤 * 單元測試中有以下語句都是包含了邏輯 1. 迴圈:`while`、`for`、`for-in` 2. 判斷:`switch`、`if-else` :::warning * 當然除了以上語句,我們這再來看一個隱藏的特殊案例 ```kotlin= // 待測試程式 class NameWithRule { fun enName(firstName: String, lastName: String) : String { return "$firstName $lastName" } } ``` 驗證單元測試如下,它的邏輯在那呢?請看註解 ```kotlin= @Test fun enName_PairRule_True() { val stubFirstName = "Kyle" val stubLastName = "Pan" val sut = NameWithRule() assertEquals( // 邏輯藏在這!手動拼接即是一個邏輯! "$stubFirstName $stubLastName", sut.enName(stubFirstName, stubLastName)) } ``` 修改手動拼接的邏輯,我們應該寫死一個 100% 正確的值,來給簡化這個測試給人帶來的邏輯壓力 ```kotlin= @Test fun enName_PairRule_True2() { val expectName = "Kyle Pan" val stubFirstName = "Kyle" val stubLastName = "Pan" val sut = NameWithRule() assertEquals( expectName, sut.enName(stubFirstName, stubLastName)) } ``` ::: ### 一次測試一個驗證 * 再次強調,單元測試一次的測試只測試一個驗證,如果單元測試存在多種輸出的可能性,那可能導致我們無法快速定位錯誤!如下範例 ```kotlin= // 待測試程式 class NameWithRule { fun enName(firstName: String, lastName: String) : String { return "$firstName $lastName" } fun cnName(firstName: String, lastName: String) : String { return "$lastName $firstName" } } ``` 單元測試超過一個驗證會如何?出錯時你沒辦法快速定位錯誤是哪個驗證出錯,維護也不好維護 ```kotlin= @Test fun enName_PairRule_True3() { val expectName = "Kyle Pan" val expectName2 = "Pan Kyle" val stubFirstName = "Kyle" val stubLastName = "Pan" val sut = NameWithRule() // 驗證 1 assertEquals( expectName, sut.enName(stubFirstName, stubLastName)) // 驗證 2 assertEquals( expectName2, sut.cnName(stubFirstName, stubLastName)) } ``` :::warning * 可以用 Message 判斷呀 當然可以,但是你有時間寫 Message (並且 Message 要寫的好),不如把兩個測試分開,這樣反而可以快速定位錯誤,並方便維護 ::: ### 測試的審查 - Code coverage * 首先 Code coverage 是檢視你的程式被測試覆蓋的程度,這相當重要(可能會有關於你對於程式的信心),但 **Code coverage 不等於優良的測試** :::danger 甚至為了滿足 Code coverage 直接來個空(假)測試 ::: * 測試的審查是相當重要的,其中我們有兩種基礎方案 ^1.^ `Code review` (後置檢查)、^2.^ `Pair coding` (寫程式時就討論、檢查) 滿推薦 `Pair coding`,可以一起討論、相互提出意見 ## 可維護的測試 我們在程式中會有許多的方法(包括私有、保護... 等等),在這裡我們要討論撰寫單元測試時的技術,如何透握這些技術實現可維護的測試 > 單元測試如果不去維護,隨著時間的推移,它會變得難以維護甚至是難以理解 ### 私有、保護方法 - 方案 * 首先我們要先了解 私有、保護方法的意義,為何不公開所有方法? 這有許多有力的原因像是,設計該模塊不希望使用者用到設計契約之外的方法、或是出於安全考量不讓使用者調用、亦或是為了整理程式而區分出不同函數 ... 等等 :::info * **私有、保護方法** v.s **公開方法(契約)** 私有、保護方法,非常有機會會因為重構而改變,而公開方法則不會如此輕易更動(否則會對使用者造成很大的修改困擾) ::: * 所以我們應該要 **透過 公開方法,間接性的測試私有方法**!畢竟私有方法一定會有公開方法呼叫 ```kotlin= class Login { // 私有方法 private fun isAccountValid(account: String) : Boolean { return account.length < 10 } // 私有方法 private fun isPasswordValid(passwd: String) : Boolean { return passwd.startsWith("A") } // 公開方法(契約) // 單元測試透過 login 傳入不同參數來測試 private 方法 fun login(account: String, passwd: String) { if (!isAccountValid(account)) { throw Exception("Account invalid.") } if (!isPasswordValid(passwd)) { throw Exception("Password invalid.") } } } ``` :::success * 直接測試私有方法有用嘛 ? 以商業邏輯來看,注重的是公開的方法,而如果公開方法都沒有全面的測試,反而選私有方法來測試,那這測試反而沒有任何意義 ::: * 以下有幾個對於私有、保護方法的測試方案(取用哪個不一定,你必須考量) 1. **讓私有、保護方法 轉為 公開方法**:這裡應該思考兩者對於使用者、開發者的意義,如果這個類的責任中可以添加公開方法,那就可以將私有轉為公開 * 私有、保護方法 開發者可以修改方法的實做方案,而不閉擔心使用者會在未知的地方使用 * 公開方法 對使用者來說它是作為一種行為、契約的存在 2. **把私有、保護方法 抽取到令一個類中**:如果一個類中有許多私有方法,那可以考慮將這些私有方法全出抽出到令一個類中,我們就可以單獨測試這個類 > 而原先類就轉為依賴這個獨立類別 ```kotlin= // 將私有方法抽出,獨立一個類 class IsValidInput { fun isAccountValid(account: String) : Boolean { return account.length < 10 } fun isPasswordValid(passwd: String) : Boolean { return passwd.startsWith("A") } } class Login2 { // 依賴獨立出來的類 private val utils = IsValidInput() fun login(account: String, passwd: String) { if (!utils.isAccountValid(account)) { throw Exception("Account invalid.") } if (!utils.isPasswordValid(passwd)) { throw Exception("Password invalid.") } } } ``` 3. **將方法改為靜態方法**:如果該方法不依賴任何原來的變數、狀態,那就可以將其抽為一個靜態方法 > 當然這靜態方法也算對外公開的契約 4. **將方法改為 internal 方法**:這要看語言是否支持(像 Java 不支持,但 Kotlin 支持),`internal` 的特性會將方法限制為該模組才可使用,這樣的特性會讓外部無法使用該方法、類 :::info * 但要有共識,internal 方法是不可隨意修改的,它雖然不是對外的契約,但它是對一個模組的公開契約 ::: :::warning * 用反射測試私有、方法? 呵呵?建議不要這樣做,這很不容易顯示出測試的錯誤點,並且也難以閱讀(你必須參照原來的程式的部份參數才能知道這是在測試啥) ::: ### 去除重複程式碼 * 在單元測試中,我們很常對於一個方法有多個測試狀況,這容易導致我們會分散不同方法作假物件,這在程式異動時,會讓我們付出較大的代價來重寫測試; 以下有幾中方案去除重複的測試 ```kotlin= // 待測程式 class Login3 { var init = false fun login(account: String, passwd: String) { if (!init) { throw Exception("Init first.") } if (account.length < 10) { throw Exception("Account invalid.") } if (!passwd.startsWith("A")) { throw Exception("Password invalid.") } } } ``` 1. **輔助方法**:當多個測試有需要重複創建(使用)相同假物件時,我們可以考慮在單元測試中創建一個輔助方法,該輔助方法就是來執行重複動作的 ```kotlin= class Login3Tests { @Test fun login_InvalidAccount_Throw() { assertFails { getLoginInstanceWithInit().login("Baby", "") } } @Test fun login_InvalidPasswd_Throw() { assertFails { getLoginInstanceWithInit().login("BabyBabyBaby", "Baby") } } // 輔助方法 private fun getLoginInstanceWithInit() : Login3 { return Login3().apply { init = true } } } ``` 2. **setup 方法**:如果多個測試方法,需要同一個物件,那可以考慮將其分配到 `@BeforeTest` 的方法中(就是測試開始之前一定會執行的程式碼) ```kotlin= class Login3Tests_2 { private lateinit var instance : Login3 @BeforeTest fun setup() { instance = Login3().apply { init = true } } @Test fun login_InvalidAccount_Throw() { assertFails { instance.login("Baby", "") } } @Test fun login_InvalidPasswd_Throw() { assertFails { instance.login("BabyBabyBaby", "Baby") } } } ``` :::success * setup 方法建議使用在全部測試都使用一個物件,或是一個設定時,否則它會造成測試的可讀性降低;以下為使用 setup 的時機 * 初始化工廠、全部測試的共用類 ::: :::warning * **setup 該避免的狀況**;以下這些狀況都會讓測試變得複雜、難以維護、降低可讀性 * 過多設定 * 準備假物件(Fake & Mock) * 初始化部份測試才會使用到的設定(可能該測試類有 50 個測試而 setup 方法就為了 5 個測試,而呼叫某個設定) ::: ### 測試隔離 - 4 種狀況 * **每個測試應該活在自己的小宇宙中**,甚至不知道其他測試的存在(最少知識),這樣可以避免測試的相互干擾,當測試出錯時可以清晰的看出錯誤點在哪; 以下有幾種測試隔離不清的案例 1. **有順序限制的測試** 2. **呼叫其他測試** 3. **共享物件狀態** 4. **外部共享狀態** > 如果出現以上這幾狀況,那可以將其稱為 **反模式** 1. **反模式 - 限制順序性** ```kotlin= // 以下有兩個待測試方法 class OrderLimit { var isInit = false fun getHello() : String { if (!isInit) { throw Exception("Init first") } return "Hello" } fun getWorld() : String { if (!isInit) { throw Exception("Init first") } return "World" } } ``` * 如果你的測試是一種流程測試的話,那極有可能那是個整合測試而不是單元測試;這種流程限制有以下幾個缺點 * 測試笨拙、不易修改 * 不論新增、修改、刪除… 都可能造成其他測試程式失敗 * 由於息息相關,測試的取名也不易 * 當測試出錯時難以追朔(錯誤不清晰,可能每次錯誤的點都不同) :::success * 你應該區分區單元測試、整合測試 ::: * 如果不同測試之間有規定要先執行哪個測試,另外一個測試才能通過,那這就是有限制順序的測試,**它的測試結果不穩定,無法作為單元測試** > 以下呈現順序性測試,這種測試相當不穩定 ```kotlin= class OrderLimitTest { private lateinit var orderLimit : OrderLimit @Test fun getHello_getString() { orderLimit = OrderLimit() orderLimit.isInit = true assertEquals("Hello", orderLimit.getHello()) } @Test fun getWorld_getString() { // 該測試依賴 `getHello_getString` 測試 assertEquals("Hello", orderLimit.getWorld()) } } ``` :::danger * 大部分測試框架的測試是併發測試,並沒有順序性 ::: 2. **反模式 - 呼叫內部測試** ```kotlin= // 以下有兩個待測試方法 class OrderLimit { var isInit = false fun getHello() : String { if (!isInit) { throw Exception("Init first") } return "Hello" } } ``` * **測試的相互呼叫會讓測試單元之間產生顯性依賴的狀況,這也不算是單元測試**,因為你測試了多個狀況;它可能會導致以下問題 * 維護難度上升,修改了共有的部份,可能會讓其他測試失敗 * 必須思考測試的順序性,增加測試撰寫的難度(難度上升讓人想要維護的感覺就會下降) * 測試可能因為錯誤的原因失敗、成功(可能有令一個函數改變了其狀態) * 難以正確的取名 :::info * 減少重複的測試程式? 這是個好想法,但並不代表我們要在程式內共用測試,可以改用 setup 方案、工廠模式來生產或建構共同物件 ::: > 以下呈現呼叫內部測試,這種測試相當不穩定 ```kotlin= class OrderLimitTest_2 { private lateinit var sut : OrderLimit @BeforeTest fun setup() { sut = OrderLimit() } @Test fun getHello_getString() { // 呼叫到內部令一個測試 isInit_setTrue() assertEquals("Hello", sut.getHello()) } @Test fun isInit_setTrue() { sut.isInit = true assertTrue { sut.isInit } } } ``` 3. **反模式 - 共享物件 / 外部狀態** ```kotlin= // 以下有兩個待測試方法 class OrderLimit { companion object { var isInit = false } fun getHello() : String { if (!isInit) { throw Exception("Init first") } return "Hello" } fun getWorld() : String { if (!isInit) { throw Exception("Init first") } return "World" } } ``` * 這種情況最常發生在所有測試共享一個物件,或是測試同時操控物件的一個靜態(**static**)狀態!這危險性就是在 **共同** 的部份,它將原本沒有關係的單元測試進行了連結 可能引發的問題如下 * 維護測試變困難,要多思考是否會影響其他測試 * 測試錯誤、成功其原因難以追尋(有可能在任何一個測試中被修改了我們也不知道) * 修改一個靜態 Field 會影響到多個測試 :::success * 要解決這個問題,其實就是要記得在 `setup`、`tearDown` 去釋放共享的物件 ::: > 以下範例就是一個靜態物件的相互干擾案例 ```kotlin= class OrderLimitTest_3 { private val sut : OrderLimit = OrderLimit().apply { OrderLimit.isInit = true } @Test fun getHello_getString() { assertEquals("Hello", sut.getHello()) } @Test fun getWorld_NotInit() { OrderLimit.isInit = false assertFails { sut.getWorld() } } } ``` :::info * 共享外部狀態 與 共享相同物件差異不大,差別只在它是共享外部的檔案、資料、時間... 等等 ::: ### 一個測試多個驗證 * 一個測試內建議最好只有一個驗證項目,否則測試失敗,你也沒辦法很快速的得知錯誤的原因,甚至需要使用 Debug 來抓出錯誤點(不利於可讀性) * 如果一個方法有多個測試項目,建議可以使用以下方案 1. **每個狀況單獨寫一個測試**:這是最普遍的作法 2. **參數化測試**:這要看你的單元測試框架是否提供參數化測試的功能,如果不提供參數化測試,你可以自己使用創建方法(實做共用部份),並對那個方法傳入需要測試的參數、結果 ### 物件比較 * 在做單元測試時我們偶爾會遇到一個窘境(至少我域到了...),我們要測試為傳物件的內容,而我們完整想測試它是否批配,這是否要分一堆測試項目呢? 範例如下 ```kotlin= data class DownloadFileInfo(val url: String, val downloadable: Boolean, val savedName: String) class MyDownloadManager { fun getDownloadInfo(url: String) : DownloadFileInfo { val downloadable = url.startsWith("https://") val savedName = url.substring(url.lastIndexOf("/") + 1) // 我想測試回傳,但是有三個元素,要寫三個測試?(甚至 3 個以上) return DownloadFileInfo(url, downloadable, savedName) } } ``` 在這裡我們有 2 種方法可以達到測試的目的(比對完整資料)又不寫過多感覺多餘的測試 1. **建立 `expect` (預期物件),再比對預期物件** :::warning * 如果有需要只比較特定數據的話,要 override `equals()` 方法 > 這時別忘記測試 `equals` ::: ```kotlin= @Test fun getDownloadInfo_invalidUrl_Equals() { val stubFileName = "123.txt" val stubUrl = "file://555.666/${stubFileName}" val expect = DownloadFileInfo(stubUrl, false, stubFileName) val sut = MyDownloadManager() assertEquals(expect, sut.getDownloadInfo(stubUrl)) } ``` 2. **程接上一個方法,這次複寫要比較物件的 `toString()` 方法,在測試時比較 `toString()` 方法** > 在測試失敗時,可以更清楚的看出是哪個部份讓測試錯誤 * 複寫 `toString()` 方法 ```kotlin= data class DownloadFileInfo(val url: String, val downloadable: Boolean, val savedName: String) { override fun toString(): String { return "url: $url, downloadable: $downloadable, savedName: $savedName" } } ``` * 測試時比較 toString 輸出 ```kotlin= @Test fun getDownloadInfo_invalidUrl2_Equals() { val stubFileName = "123.txt" val stubUrl = "file://555.666/${stubFileName}" val expect = DownloadFileInfo(stubUrl, false, stubFileName) val sut = MyDownloadManager() assertEquals(expect.toString(), sut.getDownloadInfo(stubUrl).toString()) } ``` :::warning * 另外一個考量點是,是否需要全部比對呢?這樣是否會造成過度指定? ::: ### 避免過度指定 * 過度指定是指你對於測試的目的不清,導致你做了多餘不關這個測試的假物件;過度指定的幾個指標如下 1. **測試 SUT 物件的內部切換狀態**: 我們該驗證的目標不是內部的狀態,而是界面、方法對外的承諾,**也就是對外的契約才是我們測試的目標** 2. **驗證 Stub 物件**: Stub 物件對於測試來說是一個不會改變的物件,我們不需要對一個 Stub 物件(虛設常式)進行驗證 > 也不會多次驗證 Stub、SUT、Mock 物件 3. **不必要的順序、精準的批配**: 在測試的驗證步驟中,如果使用精準批配(`assertEquals`)字串輸出,那可能會導致之後的 Source code 小小測試也會出錯 :::success * 是否精準批配這個具體看你的商業邏輯是否有這樣的需求,當然大多是希望可以使用 `assertContains` 這種方式比對 > 而這個 **寬鬆** 的程度,也要看狀況,不可太過於寬鬆 ::: ## 可讀性 單元測試的可讀性分為下幾個點 * 命名單元測試 * 命名變數 * 單元測試錯誤時的額外訊息 * 操作 & 驗證分離 ### 單元測試命名 * 一個好的單元測試(函數)命名可以幫助我們在還沒看測試程式時,就可以大致了解測試的大概樣貌;其中一個好的命名我們可以接收到以下訊息 * **測試的方法** * **測試環境**(在怎樣的條件下測試該函數) > 可能可以預期到該測試會做啥假物件去協助測試目標 * **預期的測試結果**(測試通過後,我們預期該有的結果) * 依照以上三點,我們創建的測試名稱格式可以如下 ```kotlin= @Test fun <測試方法>_<測試環境>_<預期結果>() { // todo } ``` 舉例: ```kotlin= @Test fun getDownloadInfo_invalidUrl_DownloadableSetFalse() { // todo } ``` * 從這個範例中我們可以知道幾個訊息 1. 測試函數:`getDownloadInfo` 2. 測試環境:是個非法的 Url 3. 預期的測試結果:預期應該返回該 Url 是不可下載的 :::info * Kotlin 可以將函數透過 \` \` 符號定義,可以將函數描述成整個句子(可帶有空格) > 是否使用可以依照團隊討論決定 ```kotlin= @Test fun `test getDownloadInfo fun, if set invalid url, it return cannot downloadable object`() { // todo } ``` ::: ### 單元測試 - 內部變數命名 * 在查看單元測試內部時,好的變數命名可以讓你快速定位到該測試關注的要點,而不是全部都詳細看過之後才知道該測試的目標;我們可以分為以下幾點 * **測試目標變數**: 對於我們的測試目標,我們應該以 `SUT`(System under test)、`CUT`(Class under test) 來命名 > 可以快速定位到測試目標 * **模擬物件變數**:++這點很重要++ 模擬物件就比較麻煩,我們要根據它的作用而定 1. 虛設常式:`Stub` 開頭命名 2. 驗證物件:`Mock` 開頭命名 3. 假物件:`Fake` 開頭命名(它的是使用者可操控的 虛設常式) :::danger 一個好的單元測試內可以有多個 虛設常式物件,但只能有一個 驗證物件 ::: * **預期變數**: 對於一個我們可預期的結果,我們應該將它寫清楚,給它一個變數名稱定義,而不是透過直接寫入的方式測試 ```kotlin= @Test fun getMessage_AfterInit_getDefaultMsg() { // 清楚給予預期結果再驗證 val expectMsg = "Non Msg" val sut = cacheMsg().apply { init = true } val res = sut.getMessage() assertEquals(expectMsg, res) } ``` ### 操作 & 驗證分離 * 建議把單元測試的操作(測試結果)、驗證(驗證行為)分開,如果混用則可能造成需要去閱讀更多的資訊才可以了解測試 1. 沒有把操作、驗證分離 ```kotlin= @Test fun getMessage_AfterInit_getDefaultMsg() { // 清楚給予預期結果再驗證 val expectMsg = "Non Msg" val sut = cacheMsg().apply { init = true } // 直接呼叫操作並同時驗證 assertEquals(expectMsg, sut.getMessage()) } ``` 2. 把操作、驗證分離 ```kotlin= @Test fun getMessage_AfterInit_getDefaultMsg() { // 清楚給予預期結果再驗證 val expectMsg = "Non Msg" val sut = cacheMsg().apply { init = true } // 分離操作 val res = sut.getMessage() // 分離驗證 assertEquals(expectMsg, res) } ``` ### 錯誤時的額外訊息 * 其實把上面幾點做的徹底,比你花時間寫驗證錯誤時的驗證訊息來的重要;但是如果碰到非寫驗證訊息不可的情況,請注意以下幾點 * 不要重複框架已經給予的測試結果訊息 * 不要重複已有的資訊 > 可能你從測試方法名就可以知道的訊息,像這種訊息就不需要 * 如果沒有有用的資訊... 不如寫清楚其他命名(寧可啥都別說,以免混要測試資訊);所謂有用的資訊像是: 1. 該測試應該要觸發哪些事件 2. 該測試不應該觸發的事件 3. 測試不通過的原因(記得不要與框架重複) ## Appendix & FAQ :::info ::: ###### tags: `Test`