Skip to content

2026-09-14

/ 13 分鐘閱讀

/ Testing Fundamentals

測試指南:把程式碼拆成可測函式、看清程式碼覆蓋率的盲點,並認識所謂的 unhappy path 情境 ​

寫測試真的沒有想像中難 ​

回顧前面幾篇練習寫下的幾個測試,Steve 認為這已經初步證明了他一開始說的:寫測試本身不難。拿兩個數字相加、預期得到一個結果,這件事本身沒有太多難度,測試也確實通過了。當然這幾個範例刻意隱藏了不少複雜度,但測試撰寫本身確實不是門檻。

把程式碼拆成小函式,才有東西可以測 ​

前幾篇一直在測的加法、乘法這類函式,之所以好測,是因為它們本身就是獨立、命名清楚、只做一件事的小函式。現實中的程式碼常常不是這樣:兩個數字可能就埋在一行七百行的條件判斷式裡,難以單獨測試。

這時候可以做的,是把這些邏輯拆出來,變成像加法、乘法這樣有明確參數與回傳值的小函式,例如把渲染標題時要做的字串轉換(像是 title case 的正規表示式處理)從一個龐大的元件裡抽出來,獨立成一個函式。不需要一次就把整個功能都寫成可測試的樣子,可以先拆出幾個小片段,隨著開發進度逐步增加。

程式碼覆蓋率的盲點 ​

程式碼覆蓋率分析工具的運作方式,是從測試出發去統計涵蓋了哪些程式碼:報告只計算測試實際匯入、執行到的部分,測試沒碰到的程式碼完全不算數。這也代表要拿到 100% 覆蓋率,其中一個做法就是只寫測試會匯入的檔案。

以前幾篇的加法測試為例,如果自己粗略估算一下覆蓋率,看起來已經是 100%,一切都很美好。但這種美好只是表面的,因為這些測試完全沒有考慮到程式碼在意料之外的情境下會怎麼運作。這正好呼應前面提到「即使有百分之百的覆蓋率,也可能完全沒有消除改動程式碼時的焦慮感」這個結論:覆蓋率高,只代表測試有跑過那些程式碼,不代表那些程式碼在各種情境下都是正確的。

Happy path 之外:認識 unhappy path ​

外部世界本來就充滿各種意料之外的狀況,這就是所謂的 unhappy path。前面測加法時,只驗證了「兩個數字相加」這個最理想的情境,完全沒考慮過其他的可能性:如果只給一個參數會怎樣?如果第一個參數是 null 呢?如果一個是字串、一個是數字呢?如果乾脆傳進去一個字串 "potato" 呢?如果除數是 0 呢?

JavaScript 在數學運算上的行為又特別容易踩到意料之外的地雷:true + true 會得到 2,'1' + '1' 會得到字串 '11' 而不是數字,還有無所不在的 NaN。在沒有 TypeScript 這類型別系統把關的情況下(即使有,只要用 any 就能繞過去),這些型別轉換的怪異行為都有機會真的發生在傳進函式的參數上。

挑選測試案例,靠的是經驗累積的「合理偏執」 ​

有學員(Speaker 2)當場問,寫測試時該怎麼決定要拿哪些數字來測,是隨便挑像 10 跟 6 這樣的數字嗎?Steve 的回答是:這件事沒有測試框架裡的哪個方法能幫你決定,也沒有程式碼裡的固定公式可以套用。真正影響判斷的,是多年下來累積出的那種對「所有可能出錯的地方」保持警覺的直覺。這種直覺不是天賦異稟,而是踩過夠多次同樣的坑、甚至曾經為了一個 bug 燒掉一整週之後,才慢慢練出來的。

面對非預期輸入的三種選擇 ​

碰到這些意料之外的輸入時,基本上只有三種處理方式:

  • 優雅失敗:想辦法讓函式在拿到奇怪的輸入時依然能運作,例如把傳進來的字串嘗試解析成數字,或是在缺少第二個參數時預設當作 0
  • 主動拋出錯誤:尤其是在建置階段就能被攔下來、或者已經有完整錯誤處理機制的情境下,明確拋出錯誤,讓問題浮現出來,比讓程式碼帶著錯誤的狀態繼續執行更安全
  • 放著不管,讓行為變成未定義:這是唯一真正錯誤的答案。如果傳入 undefined,即使測試覆蓋率顯示 100%,卻完全不知道函式實際上會發生什麼事,這種狀態最危險

這三個選項沒有標準答案,該選哪一個取決於當下要解決的實際情境,不論選哪一種,日後都有可能發現這個決定不是最好的,但至少有測試在那裡把關,一旦之後改了程式碼,馬上就能知道行為有沒有跟著改變。

從好案例開始,逐步擴充測試 ​

實務上的做法,通常會先從更多「理想情境」的測試案例開始擴充,確保基本行為沒有問題,再往 unhappy path 延伸。以加法函式為例,可以先加入負數相加的案例:

javascript
it('should add two negative numbers', () => {
    expect(add(-2, -2)).toBe(-4);
});

測試套件真正的價值,是能夠用各種不同的可能性去持續轟炸自己的程式碼。每加入一個新案例,都是在對程式碼「應該怎麼運作」這件事做出更多明確的假設,而這些假設一旦被寫成測試,往後只要程式碼跑偏了,就會立刻被抓出來。

複習 ​

測試裡的「unhappy path」指的是什麼? ​

考慮那些非預期或有問題的輸入與情境,例如傳入 null、字串、undefined 或型別不對的參數,這些都可能讓函式的行為不符合預期

什麼是程式碼覆蓋率分析? ​

一種衡量測試實際執行了多少程式碼的方式,通常只計算被測試匯入的檔案,並且追蹤哪些程式碼真的被測試跑過

把程式碼拆成小而命名清楚的函式,有什麼好處? ​

小函式更容易被測試、模組化程度更高,也能個別驗證正確性,讓整體測試過程更簡單、涵蓋得更完整

在 JavaScript 裡進行數學運算時,可能會遇到哪些潛在問題? ​

型別強制轉換的問題,例如 true + true 會等於 2、'1' + '1' 會變成字串 '11',還有面對字串與數字混用時的非預期行為,以及像 NaN 這種特殊值

面對非預期輸入或錯誤情境,有哪三種主要的處理方式? ​

一是優雅失敗,嘗試處理這個輸入(例如把字串解析成數字,或對缺漏的參數使用像 0 這樣的預設值);二是在輸入無法安全處理時主動拋出錯誤;三是讓函式在未定義的行為下繼續執行,這是應該避免的錯誤做法

小測驗 ​

測試裡的「unhappy path」是什麼? 一種考慮非預期或邊界情境輸入、可能導致錯誤發生的測試情境
依照測試的最佳實務,面對函式裡的非預期輸入,有哪三種合理的處理方式? 優雅處理輸入、主動拋出錯誤,或選擇什麼都不做
JavaScript 在處理不同型別的數學運算時,可能出現什麼問題? 型別強制轉換可能產生非預期的結果
測試裡的程式碼覆蓋率是什麼? 一個顯示程式碼有哪些部分被測試實際執行過的指標

此文章是 FrontendMasters 上的 Testing Fundamentals 課程筆記

最後更新時間:

Buy Me A Coffee

系列章節 第 6 篇 / 共 27 篇

0 %
MIT Licensed | Copyright © 2025-present Wen-Hsiu's Blog
Photo by Federica Galli on Unsplash