App 工程師的 Unit Test 實戰課:你的測試真的在測東西嗎?
#Unit Testing #單元測試 #App 開發 #測試策略 #TAU 課程筆記 #QA 學習
App 工程師的 Unit Test 實戰課:你的測試真的在測東西嗎? 你有沒有遇過這種情況: unit test 全過,CI 綠燈,release 出去,用戶回報 bug。 然後你回頭看測試,才發現——測試根本沒在測對的東西。 這篇是我上完 Test Automation University 上 Unit Testing 課程的筆記,整理成給 App 工程師直接能用的實戰指南。課程用 Java 範例,但概念對 iOS、Android、後端工程師都適用。 課程概覽 課程名稱 :Unit Testing 平台 :Test Automation University(免費) 章節 :15 章 語言 :Java(TestNG 框架) 先釐清一件事:從外測和從內測 課程開頭先把測試分成兩種視角。 從外測(Black Box Testing) :把程式當作黑盒子,只看輸入和輸出,不管裡面怎麼實作。 從內測(White Box Testing) :打開盒子,看程式碼的內部結構,根據實作邏輯設計測試。 Unit test 是典型的「從內測」。你知道這個函式的邏輯,你根據邏輯設計測試資料。 為什麼兩種都要做?課程用了個好比喻: 「醫學界需要內科醫生,也需要外科醫生。你看起來完全健康,但可能有高血壓。」 功能測試(從外測)看不到程式碼裡面的問題。很多 performance issue 和 bug,來源是爛的 coding,不是功能設計錯。 Code Review 比你想的更有效 課程引用了 Steve McConnell 在 Code Complete 2 裡的研究數據,讓我看了有點意外: | 技術 | 平均缺陷發現率 | | | | | Unit Testing | 25% | | Functional Testing | 35% | | Integration Testing | 45% | | Code Review / Inspection | 55–60% | Code review 找到的 bug,比 unit test 還多。 這不代表 unit test 不重要,而是說: 光靠測試是不夠的,看 code 本身就是一種測試 。 課程特別強調一個問題方式的轉換——不要問「這段 code 哪裡有問題」,要問「這段 code 怎麼可以更好」。前者讓人防禦,後者讓人協作。 三種程式結構,各有不同的測試重點 Unit test 的核心是搞清楚程式碼的結構,然後針對每種結構的風險點設計測試。 1. Sequence(循序) 最基本的結構,一行接一行執行。 可能出錯的地方: 某一行的邏輯寫錯(加法寫成乘法) 缺少某個應該要有的步驟 步驟順序錯了 測試建議: 測試資料要有「鑑別力(discriminatory power)」。 課程舉了一個反面教材:測試 add(a, b) 函式,用 (0,0) 和 (2,2) 作為測試資料。 問題在哪?0+0=0,0×0 也是 0;2+2=4,2×2 也是 4。如果開發者不小心寫成乘法,這兩個測試還是會過。 測試要能在程式邏輯錯的時候抓到問題,不然等於白寫。 2. Selection(選擇,if/else) 有條件判斷就有分支,每條路都要測到。 可能出錯的地方: 邊界值寫錯( 應該寫 =) 條件反向 某個分支完全沒測到 測試建議: 邊界值測試(Boundary Value Analysis)。 如果 code 是 if (x 10),要測:9、10、11 三個值。「差一個的錯誤(off by one error)」是條件判斷最常見的 bug,只有測邊界才能找到。 100% 程式碼覆蓋率(statement coverage)不代表測完整——你可能只用 20 和 10 就達到 100%,但邊界完全沒測。 3. Iteration(迭代,for/while 迴圈) 迴圈包含了循序和條件,複雜度最高。 可能出錯的地方: 迴圈初始化錯(沒初始化、初始值錯) 邊界條件:跑太多次或太少次 測試建議: 0 次、1 次、2 次、典型次數、最大次數、超過最大次數。 課程特別提醒:要最早測試「正常使用路徑」,因為如果用戶第一次用就炸,所有其他測試都沒意義。 Unit Test 七個黃金原則 這是課程最核心的部分,每一條都是工作中常見的反模式。 1. 清楚知道你在測什麼 測試要從 需求 出發,不是從 code 出發。 課程強調:如果你的期望值是從看 code 推導出來的,而不是從需求推導的,那這個測試永遠不會失敗(除非有人改 code)。這種測試沒有任何防護作用。 先問: 這個函式/這個類別是來解決什麼業務問題的? 2. 測行為,不測實作 測試應該根據「公開介面(public interface)」寫,而不是根據內部實作細節。 為什麼重要? 因為如果你的測試依賴內部實作細節(私有方法、內部資料結構),只要工程師做 refactoring,測試就會爆——即使功能完全正確。 好測試的標準:refactoring 之後,測試不用改,但 bug 改了就會失敗。 3. 每個測試只測一件事 每個 test method 應該有一個清楚的、單一的目的。 這樣一旦失敗,你馬上知道是什麼東西壞了,不用再去猜「是這個條件還是那個條件?」 4. 測試也是文件,要寫得像文件 測試的命名要描述場景和期望: 當測試失敗,好的命名讓你 3 秒內知道哪個功能壞了、在哪裡找。 5. 測試必須是確定性的(Deterministic) 測試要嘛永遠過、要嘛永遠失敗——在程式沒有改動的前提下。 最常見的破壞確定性的東西: Sleep/explicit wait :Thread.sleep(3000) 在快機器可能 OK,但在 CI 環境可能不夠,導致 flaky test。 條件邏輯(if/else) :測試裡有 if,就代表有時候走這路、有時候走那路,不可預期。 隨機數、時間 :對 App 開發來說特別常見——用 Date.now() 或 Random() 的測試,每次執行結果不同。 解法是 mock 掉時間和隨機數,讓測試輸入固定。 6. 每個測試要獨立自足 測試不能依賴其他測試的執行順序。測試 A 的 setup 不能假設測試 B 已經先跑過。 這也意味著每個測試要自己做 setup 和 teardown。 7. 覆蓋率是指標,不是目標 課程引用了一句話,我覺得說得非常精準: 「If a part of your suite is weak in a way that coverage can detect, it is likely also weak in a way that coverage can't detect.」— Brian Marick 白話文:覆蓋率低,代表測試一定不夠強。但覆蓋率高,不代表測試夠強。 追求 100% 覆蓋率是個陷阱——你會開始寫只為了增加覆蓋數字的測試,而這些測試沒有任何鑑別力。 更好的問法: 我的測試有沒有覆蓋最重要的邊界條件和業務邏輯? Mock 怎麼用,什麼時候不該用 Mock 讓你把測試對象從依賴中隔離出來,這對 App 開發特別實用——你可以 mock 掉網路請求、資料庫、裝置 API。 適合用 Mock 的情境: 依賴還沒實作好(前後端並行開發時) 依賴難以重現特定狀態(例如:硬碟滿、網路斷線、伺服器錯誤) 依賴有隨機或時間相關的輸出(讓測試確定化) 依賴是耗時操作(帳單計算、批次處理) Mock 的大陷阱: 課程特別警告:Mock 太多,你的測試和真實環境差距就會越來越大。Mock 的行為是你假設的,如果假設錯了,測試照樣通過,但 prod 還是會爆。 法則:Mock 用來隔離測試對象,不是用來讓測試更容易通過。 不同層次的測試關注點 App 的架構通常分三層,每層的測試重點不同: | 層次 | 測試重點 | 常見工具 | | | | | | UI 層 | 元件行為、使用者互動 | XCTest UI、Espresso | | Service / Business 層 | 業務邏輯、規則驗證 | JUnit、XCTest、pytest | | Data 層 | CRUD 正確性、資料格式 | 直接測 DAO/Repository | Unit test 的主戰場是中間的 Service 層。UI 層的 unit test 可以做,但要避免測試 UI 渲染的細節(那些很容易因為設計調整而改壞);Data 層的測試要小心環境依賴問題。 我的綜合觀點 這門課讓我最有收穫的不是語法,而是幾個思維方式的轉換: 「期望值要從需求來,不是從 code 來。」 這個觀念糾正了我見過最多的 unit test 壞習慣——很多工程師寫測試時先看 code,然後用 code 的輸出當作期望值。這樣的測試根本不是在測功能,只是在測「code 目前長這樣」。 「覆蓋率是負向指標。」 覆蓋率低一定不好,但覆蓋率高不代表測試好。這個心態轉換,讓你從「追數字」轉向「追品質」。 「測試是文件。」 一套好的 unit test,讓新人加入專案時,能靠讀測試理解這個模組的所有行為和邊界條件。如果你的測試讀起來不像文件,可能代表你的測試沒有真正涵蓋行為。 課程連結:TAU Unit Testing 參考資料 Test Automation University — Unit Testing — 本文課程來源 Martin Fowler — Unit Test — Unit test 定義與分類 Kent Beck — Test Driven Development: By Example — TDD 之父 Kent Beck 的經典著作 Google Testing Blog — Testing on the Toilet — Google 每週分享的單元測試最佳實踐 Robert C. Martin — Clean Code — 包含測試章節,說明可測試性設計原則