配合分支:day19/start,Repo: 按我
想起以前 Bug 的正式中文翻譯是「臭蟲」,太 Y2K 了先讓各位知道。好的,回到今天要討論的範疇,就是「檢查是否能夠加入的日記」的判斷,抽成另外一個 Class,例如 DiaryValidator 之類的。
為何要這麼做呢?其實這類判斷文字是否是空的,完全可以直接放在 ViewModel 就好,另外抽出來反而把事情弄的太複雜了,(開發界有句話:「預先做的最佳化,是萬惡之源。(Premature optimization is the root of all evil.)」)所以各位不要看到這種四散的判斷就小警總爆發進行大改動喔。
今天為了教學用途,試著請各位想想,如果照片日記未來發展之後,除了標題、內容是否為空以外,還需要檢查更多的東西,例如:日期可調整但需要檢查是否合理,未來發展了 PRO 功能需要判斷使用者有沒有付費⋯ 等等。
先將文字檢查抽出來作為 DiaryValidator:

接著,只要調整 ViewModel 裡頭判斷日記是否合理的地方就行了。可以在 PhotoDiaryViewModel 之中建立 Validator 即可。
private val diaryValidator = DiaryValidator()
然後更新 PhotoDiaryViewModel:
fun saveDiary(title: String, note: String) {
val normalizedTitle = title.trim()
val normalizedNote = note.trim()
// 👇🏼 改成使用 validate
val validationMessage = diaryValidator.validate(normalizedTitle, normalizedNote)
// 👀 如果有任何錯誤,就回傳錯誤訊息就好
if (validationMessage != null) {
validationError = validationMessage
} else {
// ... 省略
}
}
這類抽出來的程式,不含 Android Context(只有用到 Kotlin 內建的函式),就可以寫 JVM Unit Test,不需要開 Android 模擬器!前面提過 Kotlin 和 Java 關係難分難捨,Kotlin 在 Android 上會編譯成 Java bytecode 讓 Java 虛擬機器(就是 JVM!Java Virtual Machine)來跑,Android 是跑在 Google 手作的 Java 的虛擬機器上跑的 (相容於 Java 但不是 Java,這也是為何 Oracle 每次會來找碴的原因⋯),只要可以用 JVM 的就能直接跑!好處是跑測試的速度超快!
補充:Android 用的是 Google 自家的 Android Runtime (ART),不是真正的 JVM;本機JVM 測試則是跑在開發電腦的 JVM 上。
androidTest 下面放的是 Android Unit Test (或叫 Instrumented tests),而 JVM Test 則放在 test 資料夾下。(兩者後面都和 main 相同,用程式結構去開資料夾即可)

接下來的測試,和之前寫過的 PhotoDiaryViewModelTest 很類似,連 Before 和 After 的 Annotation 用法都一樣。跑測試的方法也一樣,就是按下 UI 左邊的綠色「播放鍵」按下「Run 'DiaryValidatorTest'」即可。
在 test 對應的資料夾下新增 DiaryValidatorTest。
class DiaryValidatorTest {
// 先留空白
}
不過 JVM Test class name 上面不用加上 @RunWith(AndroidJUnit4::class)。接著就可以寫相關的測試:

只要驗證 result 是錯誤訊息即可!

因為 DiaryValidator() 完全不用 Android 相關的元件,可以直接這樣 New 出來,非常爽。

沒有錯誤訊息,代表通過啦。
三個測試都加入之後,再嘗試跑跑看。就目前的程式碼來看,會通過嗎?
每個 class 最好只負責一件事,例如這個 validator 就只做「查證」。如果做了與其名稱無關的其他事情(像是在 validator 裡頭去改日記內容),就做太多囉!
沒有 SRP 的話,維護的人就無法直接透過 class 名稱,去推測這個東西可以拿來做什麼事情。這樣就需要每次都進去看詳細實作,否則容易出現意料之外的行為,不太好。
經典的說法是:一個 class 應該只有一個改變的理由(only one reason to change),這個 class 唯一的改變理由就是:「日記要怎樣才算合格」的規則變了——例如前面說的,以後要檢查日期合理性、檢查 PRO 付費。這些全是同一個理由的延伸,改起來還是只動 DiaryValidator。
DiaryValidator,是今天的主角;重點是認得「這段四散的判斷」可以搬去哪。null;不重組、不修改日記內容。(不違反單一職責)test 與 androidTest 的差別:JVM 測試放 test,需要在 Android 裝置上跑的 Instrumented test 放 androidTest;兩者都用跟 main 相同的套件結構開資料夾。@RunWith(AndroidJUnit4::class) 不是必備:JVM 測試的 class 上面不用加這個 Annotation;只有 Android 裝置上跑的測試才需要。