---
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 修改了參數、或是類的使用方式修改),再次運行單元測試導致單元測試一系列的失敗;
這時可以在程式中設計一個隔離層(重複利用的部份),隔離測試程式本身、產品程式,當改動時你只需要跳整隔離層;如下圖
> 
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`