# Day 38:單元測試的進入點與退出點;物件當 Map 與 Object.values **Published by:** [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/) **Published on:** 2026-01-14 **URL:** https://paragraph.com/@gcake/day-38 ## Content 單元測試裡的「單元」「進入點」「退出點」在單元測試語境下,「單元」指的是系統內部的一個「工作單元」或「用例(use case)」,不一定只是一個函式。它可以是從一次調用開始,到產生某個明確結果為止,這段過程裡所有一起合作的程式碼。**工作單元(unit of work)**可以是一個函式,也可以是多個函式組成的流程,只要是「從一個調用開始,到產生結果為止」的一整段邏輯。這段流程從 進入點(entry point) 開始,直到一個或多個 退出點(exit point) 結束。在寫測試時:進入點就是「測試程式呼叫哪個 API」:例如呼叫一個函式、送一個 HTTP 請求、操作一個公開 method。每個退出點代表「一種行為或需求」,通常會為不同的退出方式寫對應的測試案例。退出點的三種類型典型的退出點大致可以分成三類,對應不同測試手法:回傳值最直覺的情況,測試會對函式的回傳值做斷言。適合純計算、純轉換類函式(沒有副作用)。狀態改變函式不一定回傳有用的東西,但會改變某個狀態(物件屬性、全域狀態、DOM、store 等)。測試會在呼叫後檢查狀態是否符合預期。呼叫第三方/依賴項目例如:呼叫資料庫、發送 HTTP request、寫檔案。單元測試通常不直接真的去打外部系統,而是用 stub / mock(test doubles)檢查「有沒有用正確參數呼叫依賴」。這三種退出點也可以同時存在:例如某個函式既會回傳值,又會改變狀態,還會呼叫外部服務,測試時就要清楚知道要驗什麼、哪些要用 stub 取代。什麼是「好的」單元測試?書裡整理了一些特徵,這些其實對所有自動化測試都適用:容易閱讀、容易編寫,一看就知道作者想驗證什麼。自動化,按一下就能跑完。結果一致,不會「有時過、有時不過」。有用:失敗時能指出有意義的資訊,幫助找到問題。可以在任何地方、任何人一鍵執行,不綁定作者機器上的特殊設定。常見的單元測試額外特性:執行速度快。能完全控制受測程式碼(含 stub 掉外部依賴)。測試之間彼此獨立,不互相影響。在記憶體內運行,不依賴檔案系統、網路、資料庫等 I/O。在合理狀況下盡可能保持同步、線性執行,降低平行執行緒/非同步帶來的不穩定因素。在 JavaScript 環境裡,這也呼應了為什麼 async/await 這種語法糖這麼重要:它讓非同步流程可以用「看起來像同步」的方式描述,測試時也能寫出比較線性的斷言流程。[1]用同步測試思維處理非同步即便底層是非同步(例如 Promise、計時器、HTTP request),測試可以透過幾種方式保持「語意上的同步」:使用 async/await 把非同步流程寫成「直式」敘述。若沒有語法糖(例如在 callback 風格 API 中),可以在測試裡明確等待非同步完成:例如傳入 done callback、或在測試框架提供的 API 裡 await 某個 Promise。重要的是:測試本身要「知道什麼時候可以斷言」,而不是隨機等一段時間。這樣一來,即使系統內部充滿非同步,測試的進入點與退出點依然是清楚、可控的。統計器材數量:Object 當 Map 的實戰模式接下來的案例,是一個「統計每種器材數量」的小需求,很適合作為「物件當 Map 用」與 Object.values() 的練習。[2][1]需求:把重複的器材合併計數輸入:[ { name: 'Kettlebell', spec: '6 kg', weight: 6 }, { name: 'Kettlebell', spec: '6 kg', weight: 6 }, // 重複 { name: 'Dumbbell', spec: '10 kg', weight: 10 }, ] 輸出:[ { name: 'Kettlebell', spec: '6 kg', weight: 6, count: 2 }, { name: 'Dumbbell', spec: '10 kg', weight: 10, count: 1 }, ] 核心邏輯:掃過一遍清單,用「名稱 + 規格」當 key 把同一種器材累積起來,最後把結果轉回陣列。陣列 vs 物件當 Map:效能與語意方案 A:用陣列搜尋(O(n²))const equipmentsReport = []; equipmentsList.forEach((item) => { const key = `${item.name}-${item.spec}`; const existing = equipmentsReport.find((report) => ( `${report.name}-${report.spec}` === key )); if (existing) { existing.count += 1; } else { equipmentsReport.push({ ...item, count: 1 }); } }); 外層 forEach 跑 n 次,每次裡面 find 平均要掃 n/2 個元素。整體時間複雜度約 O(n²),資料一多就會明顯變慢。方案 B:用物件當 Map(O(n))const countMap = {}; equipmentsList.forEach((item) => { const key = `${item.name}-${item.spec}`; if (countMap[key]) { countMap[key].count += 1; } else { countMap[key] = { ...item, count: 1 }; } }); const summary = Object.values(countMap); countMap[key] 查找是物件屬性存取,平均視為 O(1)。整體流程是單一遍歷,時間複雜度 O(n)。最後用 Object.values(countMap) 把 Map 結果轉回陣列。[1]選擇理由:效能:從 O(n²) 降到 O(n)。語意:用「查表」取代「每次搜尋整個陣列」,閱讀起來更貼近問題本質。實作:Object.values() 是標準 API,支援度很好。[1]Object.values / Object.entries:把物件轉成陣列Object.values(obj)回傳「物件自己的、可列舉、字串 key 的屬性值」陣列。不會包含原型鏈上的屬性。不會包含 Symbol key。const obj = { a: 1, b: 2 }; Object.values(obj); // [1, 2] 這就是前面 countMap 統計完後,用來取得結果陣列的關鍵 API。[3][1]Object.entries(obj)回傳「[key, value]」二元陣列組成的陣列。const obj = { a: 1, b: 2 }; Object.entries(obj); // [['a', 1], ['b', 2]] 常用在需要同時拿 key 與 value 的場合,例如把物件轉成 Map 或是用解構搭配 for...of 做迴圈。[4][2]物件動態 key 與屬性檢查1. 動態新增屬性:一定要用方括號統計時的 key 通常是組合而來,例如 Kettlebell-6 kg,這個 key 裡面有空白與 dash,不可能用點語法:const obj = {}; const key = 'Kettlebell-6 kg'; // ✅ 正確寫法 obj[key] = { name: 'Kettlebell', spec: '6 kg', weight: 6 }; // ❌ 錯誤:點語法只能用合法識別字 // obj.Kettlebell-6 kg = ... // 這會直接語法錯誤 這也是「物件當 Map」模式中,方括號語法不可或缺的原因。2. 檢查屬性是否存在:三個層次實務上有三種常見寫法,各有適用情境。寫法一:直接判斷 value(最簡潔)if (countMap[key]) { countMap[key].count += 1; } else { countMap[key] = { ...item, count: 1 }; } 適用前提:確定 value 不會是 0、false、'' 等 falsy 值。物件是自己建立的,不是外部輸入。在「統計次數」這種情境,count 一開始是 1 之後只會變大,所以不會是 falsy,非常適合用這種寫法。寫法二:Object.hasOwn(obj, key)(現代寫法)if (Object.hasOwn(countMap, key)) { // ... } ES2022 新增,語法精簡,專門用來取代 obj.hasOwnProperty(key)。使用前要確認執行環境(現代 Node / 瀏覽器基本沒問題)。[5]寫法三:Object.prototype.hasOwnProperty.call()if (Object.prototype.hasOwnProperty.call(countMap, key)) { // ... } 相容性最好、最安全的老派寫法。主要是避免兩種情況:物件自己定義了 hasOwnProperty,把原本的方法蓋掉。物件是 Object.create(null) 建出來的,根本沒有原型鏈。[6]ESLint 的 no-prototype-builtins 規則就是在提醒:不要直接用 obj.hasOwnProperty(key),改用上面兩種其中之一。[7][8][9]統計函式完整實作:countEquipments/** * 統計器材清單中每種器材的數量 * @param {Array} equipmentsList - 器材陣列 * @returns {Array} 統計後的陣列,格式:[{ name, spec, weight, count }, ...] */ export function countEquipments(equipmentsList) { const countMap = {}; equipmentsList.forEach((item) => { const key = `${item.name}-${item.spec}`; if (countMap[key]) { countMap[key].count += 1; } else { countMap[key] = { name: item.name, spec: item.spec, weight: item.weight, count: 1, }; } }); return Object.values(countMap); } 這個模式可以重複用在各種「統計每種東西出現幾次」的場景,例如:統計購物車裡每種商品數量。統計 log 檔中每種錯誤代碼的出現次數。統計 API 回傳資料中,每個城市出現幾次。關鍵元素只有三個:用 Object 當 Map 儲存中間結果。用「某種唯一 key」把同一種東西歸在一起。最後用 Object.values() 把 Map 轉回陣列。[1]小結:這次的幾個重點觀念在單元測試裡,先想清楚工作單元的進入點和退出點,再設計測試案例:不同退出點通常代表不同需求。好的單元測試應該可讀、可重複、可自動,並且盡量在記憶體內同步運行,不依賴外部 I/O。JavaScript 的 async/await 是把非同步邏輯以同步形式表達的語法糖,有助於讓測試流程更線性。在「統計」這種問題上,Object 當 Map + Object.values 是非常實用的模式,時間複雜度從 O(n²) 降到 O(n)。[2][1]檢查物件屬性存在時,要根據情境選擇:自己控制的資料 + 值不會是 falsy:if (obj[key]) 就夠用。嚴謹、泛用:Object.hasOwn(obj, key) 或 Object.prototype.hasOwnProperty.call(obj, key),並搭配 ESLint 規則避免陷阱。[9][7]如果之後想把這段統計邏輯加上單元測試,下一步就可以練習:為 countEquipments 設計進入點(呼叫函式)、退出點(回傳值)與幾組不同情境的 test cases。 ## Publication Information - [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/): Publication homepage - [All Posts](https://paragraph.com/@gcake/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gcake): Subscribe to updates