iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

從 Fragment 到 Compose — 老 Android App 重寫的架構取捨系列 第 9 篇

Day 09|測試基礎與架構守門:fake vs mock、Konsist

  • 分享至 

  • xImage
  •  

動筆前先答(兩題)

1.【決策邊界|測試與品質】 有一份能力要求清單寫著「覆蓋率 90%」。今天的做法是只產生 Kover 報告、不設門檻。你會在什麼條件下改成設門檻(讓 koverVerify 不到標就建置失敗)?門檻設多少、套在整個專案還是個別模組?哪些類別你認為不該算進分母,為什麼?如果有人為了過門檻,補了一批只呼叫、不斷言的測試,你會怎麼發現、怎麼防?

我的回答:
(待補)

2.【預測再驗證|架構論述】 本篇有一條規則:「ViewModel 不得 import android.*」。下面三種寫法都讓 ViewModel 用到了 android.content.Context:

  • (a) 不寫 import,程式裡直接寫全名 android.content.Context
  • (b) import android.content.Context as Ctx,程式裡用 Ctx
  • (c) 另一個檔案宣告 typealias AppContext = android.content.Context,ViewModel 只用 AppContext

先寫下預測:哪幾種會被架構測試抓到?再把它們寫成暫時檔(ViewModel 的檔名以 ViewModel 結尾,放在任一個 feature 模組),跑 ./gradlew :core:testing:unitTests 驗證,驗完刪掉。對被放過的寫法,你會補規則,還是接受它?理由是什麼?

我的回答:
(待補)


hook:規範寫在文件裡,沒有人會每次 commit 都去讀。前八天定了一堆規則(core 之間只能依賴 core:common、ViewModel 不拿 Context、效果一律用 Channel……),今天把它們變成會失敗的測試。

今天的取捨問題

舊 App 的測試現況,只列形狀與數量:

  • 4 個測試檔、共 404 行,全部在 app 模組,全部是純函式或資料轉換的測試。沒有任何一個畫面邏輯、網路層或 ViewModel 的測試。
  • 24 個測試,另有 4 個被註解掉的 @Test。
  • 沒有任何 mock 或 fake 函式庫:只有 JUnit 5。
  • log 是靜態方法:Logger 有 10 個 static 方法,直接呼叫 android.util.Log。本機單元測試跑在 JVM 上,android.util.Log 一呼叫就丟例外,所以測試在 @BeforeAll 把一個全域旗標 Logger.isUnitTest 設成 true,@AfterAll 再設回來。這個旗標只有 10 個方法裡的 1 個會檢查;被測程式碼要是走到其他 9 個,測試照樣炸。
  • 沒有架構守門:模組只有一個 app,依賴方向無從檢查;就算有規範,也沒有任何東西會在違規時失敗。

Day 01 講過:一年前寫好的遷移規範一次都沒有執行。今天要回答兩件事:

  1. 測試替身用 fake 還是 mock。 新專案的 core:testing 要提供什麼共用工具。
  2. 架構規則怎麼守。 前八天的規則寫在 docs/conventions.md 裡,要用什麼讓它們在違規時真的失敗。

選項與代價

測試替身

選項 做法 優點 代價
A. 全用 mock 依賴一律 mockk<Logger>(),斷言用 verify { logger.w(any(), any(), any()) } 不用寫任何替身類別;任何介面馬上就能替換 測試綁在「呼叫了哪個方法、幾次、什麼參數」上。實作把一次 w 拆成兩次、或把兩次合成一次,該留下的警告都在,測試卻要跟著改;every { } 的設定散在每個測試裡,讀測試要先在腦中執行一遍 stub
B. 優先 fake,mock 只用在做不出 fake 的邊界 共用 fake 放 core:testing:RecordingLogger 把 log 記成清單,測試斷言清單內容;只有一個函式的依賴直接傳 lambda(Day 06 的 submit) 斷言的是結果狀態,不是呼叫順序,重構時比較不會誤報;fake 本身是一般的 Kotlin 程式,讀得懂、除錯得了 fake 要自己寫、自己維護;介面加方法時 fake 要跟著改(編譯器會提醒);fake 寫錯,測試會一起錯

架構守門

選項 做法 優點 代價
A. 只靠 Gradle 模組依賴 build.gradle.kts 沒宣告的模組,import 就編不過 零成本,編譯器就是守門員 擋不住有人把依賴加回 build.gradle.kts;也表達不了模組依賴以外的規則,例如「ViewModel 不 import android.*」「效果不用 MutableSharedFlow」
B. ArchUnit 讀編譯後的 bytecode,用 Java API 寫規則 成熟、社群大,Java 生態的標準做法 看的是 bytecode:Kotlin 的 sealed、data class、檔案層級函式、import 在 bytecode 裡都變了形,要用 JVM 的角度重新描述;Android 模組的 class 輸出要另外餵給它
C. Konsist 解析 Kotlin 原始碼,用 Kotlin API 寫規則 Kotlin 原生:hasSealedModifier、hasDataModifier、imports 直接可用;看的是原始碼,不必先編譯每個模組 只看得到原始碼寫了什麼,看不到型別推導的結果;專案外的類別(例如 androidx 的 ViewModel)解析不到,實作時踩到一次(見補充三);Gradle 不知道它讀了哪些檔案(見補充四)

上一篇
Day 08|Design system:token、theme、元件的邊界
下一篇
Day 10|Session:從一個塞滿東西的 SharedPreferences 到 typed DataStore
系列文
從 Fragment 到 Compose — 老 Android App 重寫的架構取捨 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言