把覆蓋率當 KPI
覆蓋率 80% 作為 PR merge 的門檻,結果是工程師寫 expect(result).toBeDefined() 來刷過門檻——函式被「覆蓋」了,但 assertion 沒有驗任何業務意義。
覆蓋率是副作用,不是目標。它告訴你哪些 code path 被跑過,告訴不了你那些測試有沒有在驗正確的東西。把覆蓋率當 KPI,你得到的是高覆蓋率的無效測試套件,同時讓工程師失去對測試的信任。
有意義的方式:用 mutation testing(Stryker / PIT)衡量測試的實際嚴格度,而不是行覆蓋率。
E2E 只測 happy path
E2E 測試建立成本高,所以只測「正常流程」。結果:用戶輸入空白欄位、網路中斷、session 過期——這些「不正常」情況在生產環境天天發生,但測試套件裡完全沒有覆蓋。
E2E 的高成本應該花在「最重要的流程 + 最常見的失敗場景」,不是「最簡單的 happy path」。
Mock 太多,integration 變成空殼
為了讓測試快、隔離好,把所有外部依賴都 mock 掉。最後你測的是「這些 mock 有沒有被以正確的參數呼叫」,不是「這個系統有沒有正確運作」。
// 這測的是 mock,不是系統行為
jest.mock('./orderRepository')
jest.mock('./inventoryService')
jest.mock('./notificationService')
test('creates order', async () => {
await createOrder(cmd)
expect(mockOrderRepo.save).toHaveBeenCalledWith(expect.any(Object))
// 但實際的 order 物件對不對?資料庫真的存進去了嗎?完全不知道
})過度 mock 的訊號:測試通過但部署到 staging 立刻壞掉,因為 mock 的行為和真實依賴不一樣。解法是在適合的層次用真實依賴(testcontainers 起真實 DB),只在真的需要隔離的地方用 mock。
Flaky tests 沒人處理
測試偶爾失敗、重跑就過了,大家習慣了就開始忽略「只要重跑幾次就好」。結果是 CI 紅燈變成沒有意義的訊號——大家看到紅的就重跑,而不是去查原因。
Flaky test 的成本是隱性的:它消耗的不只是 CI 時間,而是整個團隊對測試結果的信任度。一旦紅燈失去意義,測試套件作為安全網就廢掉了。
flaky test 的常見原因:時間依賴(Date.now() 沒有 mock)、測試之間共享狀態沒有清理、網路依賴沒有穩定化。找到根因修掉,不是重跑繞過。
測試跑太慢,沒人跑
本機跑一次測試要 5 分鐘,工程師開始跳過本機測試,只推上去讓 CI 跑。CI 跑 20 分鐘,工程師開始在 PR review 前不等 CI 結果。測試套件存在,但不在開發流程裡發揮作用。
慢的原因通常是:太多不必要的真實 DB 呼叫、沒有並行化、測試順序依賴造成無法並行。解法是按速度分層——快的 unit test 本機每次跑,慢的 integration test 推 PR 時跑,最慢的 E2E 只在 staging 部署前跑。
沒有 contract test,服務之間悄悄壞掉
微服務 A 修改了 response schema,服務 B 的 unit test 都通過(因為 mock 了 A),直到部署到生產才發現 B 在解析 A 的新 response 時壞掉。
Contract testing(Pact)讓消費方定義自己對提供方 API 的期待,在 CI 裡驗證提供方的實作符合這些期待——不用起完整的微服務環境,在 unit test 層就能抓到跨服務的協議破壞。