beio Logobeio

【Android 架構系列 10】最終驗收:測試驅動 AI (TDAD)——讓單元測試成為你的自動化質檢員

發布於

01. 前言:信任,但要驗證 (Trust, but Verify)

這是系列文的最終章。

在前九篇的旅程中,我們從「MVC 慘案」的廢墟中重建,學會了 SOLID、DI、Clean Architecture、Offline-First,甚至掌握了絞殺舊代碼的「暗殺術」。

現在,你的 App 架構已經非常現代化且優雅。但身為架構師,你心裡可能還有最後一絲恐懼:

「AI 寫的那幾千行程式碼,邏輯真的是對的嗎?」

AI 就像一個自信過剩的資深實習生,它寫出的 Bug 往往隱藏在完美無缺的變數命名與縮排之中。光靠肉眼 Code Review,我們很難抓出像是「大於等於寫成大於」這種邊界錯誤。

這篇最終章,我們要介紹最後一道工序:TDAD (Test-Driven AI Development,測試驅動 AI 開發),把單元測試變成你的自動化質檢員。

老實說,在 AI 出現之前,很多 Android 開發者(包括筆者)在趕 Deadline 時,測試往往是第一個被犧牲的。

但在 AI 時代,不寫測試 = 把靈魂賣給魔鬼

為什麼?因為 AI 生成程式碼的速度太快了,且具有極強的「欺騙性」。它寫的 Code 邏輯看似通順,甚至連註解都寫好了。但魔鬼藏在細節裡:

  • 它可能在日期轉換時弄錯了時區。
  • 它可能在 List 為空時忘記處理,導致 IndexOutOfBounds。
  • 它可能在資料庫存取時,忘了加上 Transaction。

如果靠人工 Code Review 一行一行去讀 AI 的邏輯,那使用 AI 省下的時間又全都吐回去了。

所以,我們需要轉換思維:測試不再是為了防範人類的疏忽,而是為了防範 AI 的幻覺。

02. AI 時代的測試軍火庫

為了讓 AI 能順利生成測試,我們需要選擇「最標準」、「最無爭議」的工具堆疊。如果用太冷門的庫,AI 的訓練資料不足,寫出來的測試會跑不動。

筆者推薦的 黃金三件套

  1. MockK: Kotlin 專用的 Mock 工具。語法直觀,AI 非常擅長使用 every { ... } returns ...
  2. JUnit 5: 業界標準測試框架。
  3. Turbine: Google 推薦的 Flow 測試工具。這是關鍵,它能把複雜的非同步資料流變成線性的「一步一步」驗證。

03. 實戰演練 1:驗收 UseCase (邏輯層)

還記得我們在 Part 9 絞殺舊代碼時提取出來的 FilterNewsUseCase 嗎?我們要驗證 AI 是否真的有把「沒有圖片」的新聞過濾掉。

這時候,Clean Architecture 的優勢展露無遺。因為 UseCase 依賴的是 Repository 介面 (Interface),而不是實作,我們可以輕鬆地 Mock 掉資料來源。

你給 AI 的 Prompt:

"請使用 MockK 為 FilterNewsUseCase 撰寫單元測試。 情境:Repository 回傳兩則新聞,一則有圖片,一則沒有。 預期結果:UseCase 應該只回傳那一則有圖片的新聞。"

AI 生成的測試代碼:

// domain/usecase/FilterNewsUseCaseTest.kt
class FilterNewsUseCaseTest {

    // 1. Mock: 我們不需要真實的網路請求,用假的就好
    private val repository: NewsRepository = mockk()
    private val useCase = FilterNewsUseCase(repository)

    @Test
    fun `invoke SHOULD return only news with images`() = runTest {
        // Given: 準備假資料
        val mixedNews = listOf(
            Article(id = "1", title = "有圖新聞", imageUrl = "http://...", ...),
            Article(id = "2", title = "無圖新聞", imageUrl = null, ...)
        )
        
        // 告訴 MockK:當有人呼叫 getNews 時,請回傳這筆假資料
        // 這就是我們給 AI 的「預設答案」
        coEvery { repository.getNews() } returns Result.success(mixedNews)

        // When: 執行測試目標
        val result = useCase().getOrThrow()

        // Then: 驗收結果
        // 這是 AI 幫我們做的邏輯檢查
        assertEquals(1, result.size) 
        assertEquals("有圖新聞", result.first().title)
    }
}

筆者點評:

當這個測試燈號亮起「綠燈」,你根本不需要去檢查 UseCase 裡面的 filter 語法有沒有寫錯。測試通過,就代表邏輯正確。這省去了大量的人工審查時間。

04. 實戰演練 2:驗收 ViewModel (狀態層)

這是最多 Bug 產生的地方:狀態流失、UI 沒有轉圈圈、錯誤訊息沒顯示...

有了 Turbine,我們可以用「時間軸」的概念來驗收 AI 寫的 ViewModel。

必備基礎設施:MainDispatcherRule

Android 的 ViewModel 依賴主執行緒 (Main Looper),在單元測試環境中是沒有的。AI 常常會忘記這點。請務必將這個 Rule 餵給 AI。

// test/util/MainDispatcherRule.kt
class MainDispatcherRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() { ... }

你給 AI 的 Prompt:

"請測試 NewsViewModel。 使用 Turbine 驗證:當 Repository 成功回傳資料時,UiState 的變化流程應該是 Loading -> Success。 請記得套用 MainDispatcherRule。"

AI 生成的測試代碼:

// presentation/news/NewsViewModelTest.kt
class NewsViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule() 

    private val repository: NewsRepository = mockk()
    private lateinit var viewModel: NewsViewModel

    @Test
    fun `loadNews sequence SHOULD be Loading then Success`() = runTest {
        // Given
        val fakeArticles = listOf(Article("1", "Title", ...))
        // 使用 Flow 回傳假資料
        coEvery { repository.getNewsStream() } returns flowOf(fakeArticles)

        // When
        viewModel = NewsViewModel(repository) // 初始化觸發 flow 訂閱

        // Then: 使用 Turbine 捕捉狀態流
        viewModel.uiState.test {
            // 第 1 個狀態必須是 Loading (初始狀態)
            assertEquals(NewsUiState.Loading, awaitItem())
            
            // 第 2 個狀態必須是 Success,且資料要對
            val successState = awaitItem() as NewsUiState.Success
            assertEquals(fakeArticles, successState.news)
            
            // 確保沒有其他奇怪的狀態跑出來
            cancelAndIgnoreRemainingEvents()
        }
    }
}

筆者點評:

awaitItem() 的強大之處在於它把「非同步」變成了「同步」。我們可以清晰地看到狀態變化的順序。如果 AI 在 ViewModel 裡忘了用 stateIn 設定初始值,或者 map 邏輯寫錯,這個測試就會立刻報錯。

05. TDAD 流程:讓 AI 自己測自己

在 AI 時代,最高效的開發流程不是傳統的 TDD (先寫測試),而是 TDAD (Test-Driven AI Development)

  1. Define (定義): 你定義 Interface 和 UseCase 的輸入輸出。
  2. Generate (生成): AI 實作 Impl 和 ViewModel。
  3. Verify (驗證): 你要求 AI: "請針對你剛剛寫的 Impl,寫一個單元測試來覆蓋 Happy Path 和 Error Path。"
  4. Refine (修正): 如果測試失敗,把錯誤訊息貼給 AI,讓它自己修。

這就像是讓 AI 左手畫圓,右手畫方,互相比對。這能極大程度地減少幻覺。

06. 全系列總結:開發者角色的終極轉身

這十篇文章,我們走過了一趟完整的旅程。

  1. 覺醒 (Part 1-2): 我們意識到 AI 是雙面刃,並用 SOLID 簽訂了契約。
  2. 防禦 (Part 3-6): 我們用 DI、Clean Architecture、SSOT 和 ROP 建立了銅牆鐵壁。
  3. 進攻 (Part 7-9): 我們用 Sealed Interface、Compose 和絞殺榕模式解決了複雜的 UI 與遺產問題。
  4. 驗收 (Part 10): 我們用單元測試完成了自動化質檢。

我們還需要寫程式嗎?

在 AI Agent 已經足夠強的現在,我們還需要學架構嗎?

答案是:比以往任何時候都重要。

但我們的角色變了。

以前,我們是 Code Writer (搬磚工人),我們花 80% 的時間在敲鍵盤,思考 for 迴圈怎麼寫。

現在,我們是 System Architect (架構師)Reviewer (審查者)

  • 我們負責設計邊界(定義 Interface)。
  • 我們負責設計規則(定義 Prompt)。
  • 我們負責設計驗收(定義 Test Case)。
  • 至於中間那塊繁瑣的實作?那是 AI 的工作。

給讀者的 Call to Action

如果你現在手上的專案還是一團混亂,不要試圖一次重寫。試著從今天開始:

  1. 挑一個新功能。
  2. 先寫 Interface 和 UseCase。
  3. 把這些檔案餵給 AI,讓它幫你生成 ViewModel。
  4. 最重要的一步:要求 AI 補上測試。

握緊你手中的藍圖(架構),指揮你的 AI 大軍。你會發現,寫程式從未如此輕鬆,而且——令人安心

(全系列完)


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner