拒絕加班的藝術:為什麼「測試左移」是測試工程師準時下班的救星?

拒絕加班的藝術:為什麼「測試左移」是測試工程師準時下班的救星?
Photo by charlesdeluvio / Unsplash

前言

常常上線前三天才開始測新功能,結果一跑測試才發現功能與當初的文件不符合,於是開始與 PM 和開發釐清,花了幾天的時間,才搞清楚功能規格,但上線日期不會因為這些事情而延後,但我的下班時間會?

我曾經試著將測試左移引導至開發流程裡,對於釐清使用者需求的效果不錯,但涉及團隊的開發習慣、公司組織架構與產品特性,落地的困難度不一。如果你和我真的很想準時下班,我們可以來重新看看「測試左移」會不會是測試工程師準時下班的救星。

測試左移的起源與定義

要理解左移能不能真的幫上忙,先看看它從哪裡來。

2001 年的時空背景

測試左移在 2001 年被提出,想要理解為什麼被提出,需要先理解當時的開發環境。首先要介紹瀑布模型(Waterfall Model)和 V 模型(V-Model)這兩個當時主流的開發模型。

瀑布模型(Waterfall Model)

由 Winston Royce 在 1970 年的論文中首次描述(諷刺的是,Royce 本人其實是在論述這個模型的缺陷)。瀑布模型將軟體開發切成線性的階段,每個階段完成後才進入下一個:

V 模型

在瀑布模型的基礎上,Paul Rook 在 1980 年提出了 V 模型,將測試階段與開發階段對應起來:左邊是需求定義、系統設計、詳細設計,右邊是對應的驗收測試、系統測試、單元測試。V 模型比瀑布模型進步的地方在於它明確了「每個開發階段都有對應的測試活動」,但測試的執行仍然集中在右半邊 — 也就是開發完成之後。

2001 年的軟體產業,在瀑布模型V 模型,測試是一個獨立的階段,由專責的測試團隊在開發完成後執行持續數週甚至數月。CI(持續整合)剛起步 — CruiseControl 在 2001 年才出現。自動化測試工具還很原始,大量測試仰賴手動執行。當時的痛點很明確:問題的發現和修復都被推遲到專案後期。這也是 Larry Smith 在 2001 年 9 月於 Dr. Dobb's Journal 提出測試左移(Shift Left Testing)一詞,主張將測試活動「向左移動」——也就是往專案時間軸的前期推進。

傳統模式下,測試集中在「開發 → 測試」這個階段。左移的意思是把測試活動推到開發、設計、甚至需求階段:

值得注意的是,Kent Beck 在 1999 年出版的《Extreme Programming Explained》中已經提出了 TDD(Test-Driven Development)作為 XP 的核心實踐,2001 年 2 月 Agile Manifesto 也已發布。但這些在當時還只是小眾社群的先鋒實驗 — 主流產業對於「開發人員撰寫測試」這件事仍然抱持懷疑,更何況要「先寫測試再寫程式碼」。

年份 事件 意義
1970 Winston Royce 描述瀑布模型 線性開發流程的起點
1980 Paul Rook 提出 V 模型 測試與開發階段對應,但執行仍在後期
1999 Kent Beck 出版《XP Explained TDD 概念首次被系統化提出
2001 年 2 月 Agile Manifesto 發布 敏捷運動的起點,但尚未普及
2001 年 9 月 Larry Smith 發表 "Shift-Left Testing" 從 QA 管理角度提出測試前移
2002 Kent Beck 出版《TDD: By Example TDD 方法論的完整闡述
2003 Dan North 開始發展 BDD 從「測試」到「行為」的思維轉變

其實 Smith 的 Shift-Left 和 Kent Beck 的 TDD 同時出現並非巧合,兩者都是對「測試太晚、回饋太慢」這個痛點的回應,只是切入角度不同。Smith 從 QA 管理的角度出發——「怎麼讓 QA 和 Dev 更早整合」;Beck 從開發實踐的角度出發——「先撰寫測試再寫程式碼」。

微軟的實踐:L0/L1/L2/L3 分類

Microsoft 在 2014 年分享了他們的測試左移轉型過程("Shift testing left with unit tests"),是業界最完整的大規模左移案例之一。核心做法是用四個層級重新分類測試:

層級 測試類型 特性 執行時機
L0 快速 Unit Test In-memory,無外部依賴,平均 < 60ms 每次 commit
L1 Unit Test 可依賴少量資源(如 SQL),平均 < 400ms 每次 commit
L2 Functional Test 需要外部依賴,但可在隔離環境執行 Pull Request
L3 Functional Test 需要服務部署,可能使用 stub 部署前

微軟團隊所提供的一個關鍵數據是 60,000 個 L0/L1 測試在 6 分鐘內平行跑完,而微軟的目標是壓到 1 分鐘以內。整個轉型花了兩年半(42 個 sprint),將 27,000 個 legacy test 逐步替換為新的測試分層體系,並且在重構過程中,刪除了大量不再有價值的測試案例。

在微軟的做法裡,有一個務實的設計決策值得我們省思,就是對於 legacy codebase,他們允許 unit test 可以依賴某些資源(例如:SQL resource provider),而不需要 mock 所有相依物件。

這個決策背後的思維是:測試程式碼的行為,比測試函式或類別本身更有價值。 一個依賴真實 SQL 的測試,雖然不符合學術上對「單元測試」的嚴格定義,但它驗證的是「這段程式碼在接近真實環境下的行為是否正確」。這與 Kent C. Dodds 在測試獎盃中提倡的「測試行為,不測試實作細節」、以及 Spotify 蜂窩模型刻意用「Implementation Detail Tests」取代「Unit Tests」的命名,是同一條思路。微軟的 L0/L1 分類也正是把這個光譜具體化了:L0 是完全隔離的 solitary test,L1 是允許碰真實依賴的 sociable test — 兩者都歸類為 unit test,差別只在務實程度。

左移的驅動力:不是成本,而是風險前置

一個常見的誤區

測試左移常常被誤認為單元測試或自動化測試要提早執行,但這只是測試左移的其中一個面向。測試左移的精神,是把測試思維提前介入,讓軟體開發週期的每個階段都有測試活動的發生。

例如:在需求階段,我們不可能有單元測試或 E2E 測試可以驗證,但可以提早提出「當這個功能上線後,我要怎麼驗證它是正確的」。光是這個問題就可以釐清需求裡面的模糊地帶——測試參與討論、確認功能需求和實作的範疇,即使沒有撰寫任何一行程式碼,也是測試的一環。

這也是持續測試(Continuous Testing)的核心概念——在 SDLC 的每個階段都介入測試思維,而不是把所有驗證壓在最後一關。Dan Ashby 在 2016 年的文 Continuous Testing in DevOps 對此有深入的討論。

理解了這個前提之後,我們可以重新檢視左移的驅動力——它不是關於「把測試提早寫」,而是關於「把風險提早暴露」。

傳統論述的侷限

傳統測試左移的文章的理論基礎是基於「成本考量」,需求階段發現問題修復成本是 1,測試階段是 10,上線後是 100。這個數據最早來自 Barry Boehm 在 1981 年的著作《Software Engineering Economics》,後來被 IBM Systems Sciences Institute 的研究進一步引用和推廣。它意味著我們越早在前期修復問題,我們的成本越低。這個論述不算錯誤,但在現今的軟體開發流程,更新版本的迭代速度早就超過當時的迭代速度,修復的成本可能已經沒有當初的差距這麼龐大。

在前一篇文章《測試策略的全知讀者視角:從金字塔到 AI 時代的多次轉變》中,我的核心觀點是:測試資源應該配置在風險最高的地方。並不是哪種測試成本最低,測試案例數量就必須要更多,而是系統哪裡最容易出問題,測試就應該集中在哪裡。

同樣的邏輯套用到測試左移:當需求階段的風險不是程式碼的問題,是需求或實作,甚至驗證的方向錯了。即使程式碼可以寫得完美無缺,但如果一開始需求就沒對齊,那些完美的程式碼只是在「正確地做錯誤的事」。

測試左移的三個驅動力

驅動力 說明 對應的左移實踐
縮短回饋週期 不是因為晚發現比較「貴」,而是晚發現代表在錯誤的方向上走了更久 需求階段的 Three Amigos
前置風險 需求階段是整個 SDLC 中不確定性最高的地方,測試思維在此的投報率最高 Acceptance Criteria 的提前定義
建立共識 PM、Dev、QA 三方對「做完了」有相同的理解,後續的開發和測試都會更順暢 Given-When-Then 格式的驗收條件

這三個驅動力的共同點是:它們解決的不是「程式碼有沒有 bug」的問題,而是「我們是不是在做對的事情」的問題。

從需求就開始「找麻煩」

起源與角色定義

Three Amigos 的概念最早由 George Dinwiddie 在 2009 年的一篇部落格文章中提出,後來被 Janet GregoryLisa Crispin 在其 2009 年的著作《Agile Testing: A Practical Guide for Testers and Agile Teams》中深入闡述。 Three Amigos 的核心是在每個 User Story 開始開發前,由三個角色進行一場 30-60 分鐘的討論:

然而這三個視角缺一不可,因為 PM 知道業務邏輯但不知道技術上的限制,開發知道如何實作但可能會漏掉邊界條件、而測試可能思考如何讓系統中斷或是使用者會怎麼使用。

以 FoodRush 評分功能為例

FoodRush 是一個類似美食外送的平台,消費者會下單,接著餐廳接單、外送員取餐配送的完整流程。假設今天 PM 提出一個功能:「消費者在訂單完成後,可以對這次外送針對餐廳與外送員打 1-5 顆星,並且留下評論。」

在 Three Amigos 的方法下,會議的對話可能會類似如下(實際上這些問題是在 30 分鐘的討論中逐步浮現的,這裡濃縮呈現):

PM:「消費者在訂單完成後,可以對這次外送針對餐廳與外送員打 1-5 顆星,並且留下評論。」

Dev:「評分要存在哪裡?目前的 Order Service 還是實作一個新的 Rating Service?在目前的微服務架構,API 可能會出現幾秒的延遲。」

QA:「那如果消費者在評分的時候網路斷線了,評分送出了一半怎麼辦?」

PM:「⋯⋯我沒想到網路斷線的情境。」

QA:「另外,外送員的平均分數是即時計算的還是批次計算的?如果是即時計算,一筆一星評分進來,瞬間從 4.8 掉到 3.2,外送員會在 app 上看到分數先顯示 3.2 再到 4.8 嗎?」

PM:「評分時的更新頻率我也還沒決定。」

30 分鐘的對話,暴露了至少以下未定義的需求:

  • 外送員的平均分數多久更新一次(24 小時?72 小時?無限?)
  • 如果消費者已取消訂單是否還可以繼續評分
  • 評分是否可以修改幾次?
  • 網路中斷的處理機制
  • 平均分數的計算策略(即時計算 vs 批次計算)
  • 隱私問題(外送員能否看到評分者姓名或身份)

假設這些問題在需求階段沒有釐清的話,後期測試階段就會以「這個到底是 Bug 或是 Feature」的形式討論,並且壓縮原本測試的時間。

將 Three Amigos 產出轉化為驗收條件

Three Amigos 會議的產出需要被結構化,否則只是一場有用的聊天。這裡的關鍵工具是 Dan North 在 2006 年的文章 Introducing BDD 中提出的 Given-When-Then 格式。

North 提出 BDD(Behaviour-Driven Development)的動機,是觀察到開發者對 TDD 的抗拒往往來自「test」這個詞。當他把焦點從「驗證程式碼」轉向「描述行為」——用 "behaviour" 取代 "test"——開發者的接受度大幅提升。這個思維轉變讓 BDD 不只是一個測試方法,更是一個需求對齊工具。

用 Example Mapping 拆解 FoodRush 評分功能

Matt Wynne(Cucumber 共同創辦人)在 2015 年提出了 Example Mapping,這是一種用四色卡片在 25 分鐘內將 User Story 拆解為可驗證的結構化產出的協作技巧。搞笑談軟工(Teddy Chen)在影片「Example Mapping 簡介」中對此有完整的中文介紹。

四色卡片的對應關係: 顏色用途說明 🟡 黃色 Story 要討論的 User Story,一張就好 🔵 藍色 Rule 從討論中提煉出的業務規則 🟢 綠色 Example 具體例子,驗證每條規則 🔴 紅色 Question 無法當場回答,需後續釐清的問題

以 FoodRush 評分功能為例,一次 Example Mapping 的產出可能長這樣:

🟡 Story:消費者可以對外送評分

🔵 Rule 1:超過 72 小時不可評分

  • 🟢 訂單完成 47 小時後點評分 → 可以評分
  • 🟢 訂單完成 73 小時後點評分 → 顯示「評分已過期」
  • 🟢 剛好 72 小時 → 可以評分(邊界值)

🔵 Rule 2:已取消訂單不可評分

  • 🟢 訂單狀態為「已取消」→ 評分按鈕不可點擊
  • 🟢 訂單在配送途中被取消 → 評分按鈕不可點擊

🔵 Rule 3:每筆訂單只能評分一次

  • 🟢 已評分的訂單 → 顯示已提交的評分,不可重新提交
  • 🟢 評分送出後 → 無法修改

🔴 Question:

  • 外送員的平均分數是即時更新還是批次計算?
  • 外送員能看到是哪位消費者給的評分嗎?
  • 網路中斷時,評分送出一半怎麼辦?

這裡的關鍵不是格式本身,而是產出的結構:藍色 Rule 讓開發知道邊界在哪,綠色 Example 讓 QA 有明確的驗收依據,紅色 Question 確保未決問題不會被遺忘,而 PM 在驗收時知道什麼算「做完了」。需求、開發、測試三方終於在同一共識上面。

測試左移對測試分佈的影響

測試左移還有一個重要的副作用:它會自然改變測試的分佈。(如果你對測試金字塔、獎盃、蜂窩等模型還不熟悉,可以參考《測試策略的全知讀者視角:從金字塔到 AI 時代的多次轉變》。)

從 FoodRush 看測試層級的選擇

當需求在 Three Amigos 階段就被釐清後,很多邊界條件可以在更低的測試層級被驗證。以 FoodRush 的評分功能為例:

「超過 72 小時不可評分」— L0 Unit Test

// rating.service.ts
export function isRatingWindowOpen(
  orderCompletedAt: Date,
  now: Date = new Date()
): boolean {
  const RATING_WINDOW_HOURS = 72;
  const diffInHours =
    (now.getTime() - orderCompletedAt.getTime()) / (1000 * 60 * 60);
  return diffInHours <= RATING_WINDOW_HOURS;
}
// rating.service.spec.ts
import { isRatingWindowOpen } from './rating.service';

describe('isRatingWindowOpen', () => {
  it('should return true within 72 hours', () => {
    const completedAt = new Date('2025-03-01T10:00:00Z');
    const now = new Date('2025-03-03T09:00:00Z'); // 47 小時後
    expect(isRatingWindowOpen(completedAt, now)).toBe(true);
  });

  it('should return false after 72 hours', () => {
    const completedAt = new Date('2025-03-01T10:00:00Z');
    const now = new Date('2025-03-04T11:00:00Z'); // 73 小時後
    expect(isRatingWindowOpen(completedAt, now)).toBe(false);
  });

  it('should return true at exactly 72 hours (boundary)', () => {
    const completedAt = new Date('2025-03-01T10:00:00Z');
    const now = new Date('2025-03-04T10:00:00Z'); // 剛好 72 小時
    expect(isRatingWindowOpen(completedAt, now)).toBe(true);
  });
});

這個測試不到 1 秒就能跑完。用微軟的分類,這是典型的 L0 測試:純 in-memory,無外部依賴。

「Rating Service 和 Order Service 的資料一致性」— L2 Integration Test

FoodRush 是微服務架構,Rating Service 在提交評分前需要向 Order Service 確認訂單狀態。這兩個服務之間的 API 介面如果在某次部署後改了回傳格式,unit test 完全不會發現——因為 unit test 只測單一服務內部的邏輯。 這正是 Pact 契約測試的用武之地。Pact 採用 Consumer-Driven Contract Testing:由呼叫方(Rating Service)定義「我期望你回傳什麼」,再讓提供方(Order Service)去驗證這份契約。

Consumer 端(Rating Service)定義契約:

// rating-service/tests/pact/order.consumer.spec.ts
import { PactV4, MatchersV3 } from '@pact-foundation/pact';
const { like } = MatchersV3;

const provider = new PactV4({
  consumer: 'RatingService',
  provider: 'OrderService',
});

describe('Order Service Contract', () => {
  it('should return order status for a completed order', async () => {
    await provider
      .addInteraction()
      .given('order ORD-001 exists and is completed')
      .uponReceiving('a request for order status')
      .withRequest('GET', '/api/orders/ORD-001/status')
      .willRespondWith(200, (builder) => {
        builder.jsonBody({
          orderId: like('ORD-001'),
          status: like('completed'),
          completedAt: like('2025-03-01T10:00:00Z'),
        });
      })
      .executeTest(async (mockServer) => {
        const response = await fetch(
          `${mockServer.url}/api/orders/ORD-001/status`
        );
        const data = await response.json();

        expect(data.status).toBe('completed');
        expect(data.completedAt).toBeDefined();
      });
  });
});

Provider 端(Order Service)驗證契約:

// order-service/tests/pact/order.provider.spec.ts
import { Verifier } from '@pact-foundation/pact';

describe('Order Service Provider Verification', () => {
  it('should fulfill the contract with Rating Service', async () => {
    const verifier = new Verifier({
      providerBaseUrl: 'http://localhost:3001',
      pactUrls: ['./pacts/RatingService-OrderService.json'],
      stateHandlers: {
        'order ORD-001 exists and is completed': async () => {
          // 在測試資料庫中建立測試資料
          await seedOrder({
            id: 'ORD-001',
            status: 'completed',
            completedAt: new Date('2025-03-01T10:00:00Z'),
          });
        },
      },
    });
    await verifier.verifyProvider();
  });
});

這個契約測試不需要同時啟動兩個服務,每個服務可以在自己的 CI pipeline 中獨立驗證。當 Order Service 修改了 API 回傳格式(例如把 completedAt 改名為 completed_at),Provider 端的驗證會立即失敗,精確指出哪個欄位的契約被破壞——而不是等到 staging 環境全部部署後才在 E2E 測試中發現莫名其妙的錯誤。 這正是 Spotify 測試蜂窩模型中用契約測試取代 E2E 的實踐:執行速度快、失敗訊息明確、每個團隊只需維護自己的契約。

左移如何改變測試分佈

左移前後的測試分佈對比

測試目標 未左移時的測試層級 左移後的測試層級 原因
72 小時評分窗口 E2E(需要完整系統) L0 Unit Test 需求釐清後,純邏輯可獨立驗證
已取消訂單不可評分 E2E L1 Unit Test 狀態判斷邏輯可在 service 層測試
跨服務資料一致性 L3(需部署多個服務) L2 Integration Test Testcontainers / Pact 隔離驗證
完整評分流程 E2E E2E(不變) 端到端使用者旅程仍需 E2E 覆蓋

測試分佈的變化不是因為 unit test 比較便宜,而是因為需求對齊了,邊界清楚了,自然就知道哪些邏輯可以在最小的測試單位裡驗證。這與前一篇測試模型文章的觀點一致:不是哪個模型更「正確」,而是測試分佈是否反映了系統真正的風險所在。

推動左移的現實阻力

左移的價值在理論上很清楚,但實踐中會遇到組織和技術層面的阻力。我自己在推動左移的過程中,最大的體會是:技術上的改變反而是最容易的部分 — 真正困難的地方是讓團隊願意多花 30 分鐘坐下來對齊需求,而不是各自埋頭做事後再來,並且養成先對齊需求的習慣。

組織層面

阻力 常見表現 應對策略
開發沒空參加 Three Amigos 「我在趕功能,沒空開會」 先挑一個高風險 story 做試點,用結果說服團隊。微軟的轉型花了兩年半,不需要一個 sprint 改變所有事
PM 認為 QA 不需要參加需求會議 「QA 等開發完再測就好」 將「QA 介入需求」重新定義為「提前定義 Definition of Done」——這是在幫 PM 減少後期的來回溝通,不是越權
團隊對左移的抗拒 「我們以前都這樣做,也沒出什麼大問題」 收集「測試階段才發現的需求認知差異」的數據,讓問題可見化

技術層面

Legacy codebase 的可測試性不足是最常見的技術障礙。耦合度太高,寫一個 unit test 要 mock 半個世界。

微軟的做法是務實的:對 legacy codebase 允許 unit test 依賴某些資源,先用寬鬆標準開始寫測試,再逐步改善可測試性。

一個不依賴學術定義、只關注「能不能往前推」的分層方式:

分類 依賴程度 策略
可完全隔離的測試 純邏輯,不依賴外部 直接左移到 L0
有依賴但可 mock 的測試 資料庫、外部 API 可用 mock 替代 左移到 L1
有依賴且難以 mock 的測試 需要真實環境(Testcontainers 保持在 L2,但可在 CI 中執行
需要完整環境的測試 UI 測試、E2E 流程 保持在 L3/L4

核心原則:哪些測試能跑得快、跑得穩,就往前推;哪些需要完整環境,就留在後面。不要讓完美成為好的敵人——一個依賴資料庫的 L1 測試,比沒有測試好太多了。

平衡之道:左移之後,別忘記測試右移

測試左移解決的是「提早對齊認知、前置已知風險」,但有些風險在上線前無法預測。真實使用者的行為模式、生產環境的負載特徵、第三方服務的間歇性故障——這些只有在 production 才會浮現。

微軟在同一篇文章中提到:

"We must use data, driven by user signals, to generate and validate requirements."

這正是測試右移(Shift Right Testing)的核心——用生產環境的真實數據來驗證假設。

Cindy Sridharan 在《Testing in Production: the Safe Way》中指出:Testing in production 不是「不做測試就上線」,而是在有充分的可觀測性(Observability)和安全機制下,把部分驗證延伸到生產環境。Google SRE Book 第 17 章也強調了類似的觀點:production 本身就是一個持續驗證系統可靠性的場域。

以 FoodRush 的評分功能為例,右移的實踐包括:

實踐 說明 FoodRush 範例
Canary Release / Feature Flag 新功能先開放給一小部分使用者 評分功能先對某個城市開放,觀察錯誤率後再全面推開
Monitoring & Alerting 設定關鍵指標的自動告警 評分 API 的 p99 latency > 500ms 或失敗率 > 2% 時告警
Observability Structured logging + distributed tracing 用 OpenTelemetry 追蹤評分請求從 API Gateway 到 Rating Service 的完整鏈路

左移用的是「測試思維」來前置已知風險,右移用的是「真實數據」來發現未知風險。兩者是互補的,結合起來才是完整的測試策略。

[左移 + 右移的完整測試策略]

結語 - 划算的投資

其實測試左移並不是什麼新概念 — Larry Smith 在 2001 年就提出了,微軟在 2014 年就大規模實踐。但到了 2026 年,很多團隊還是在「上線前三天才開始測」的循環裡打轉。

最主要的原因並不是他們不知道測試需要左移。左移需要改變的不只是測試流程,還有團隊的協作方式——而協作方式又跟組織架構密切相關。理想的路徑是從組織層級開始改變產品協同的開發方式,再透過不同的實踐方法縮小 PM、開發、測試之間的認知盲點。

必須強調的是:Three Amigos 不是一個技術實踐,是一個溝通實踐。Given-When-Then 不是一個測試格式,是一種讓需求對齊的工具。

回到文章開頭的場景:如果在那個 story 開始開發前,這三方已經花了 30 分鐘用 Three Amigos 把需求拆解清楚,而邊界條件寫成了 Acceptance Criteria,開發在寫 code 的同時用 unit test 擋住了核心邏輯 - 那麼到了上線前三天,測試工程師要驗證的是 Acceptance Test Case 是否正確通過,並且執行 exploratory testing 確認沒有其他問題,然後終於可以準時下班了。


延伸閱讀

測試左移核心:

歷史脈絡:

Three Amigos & BDD:

測試策略與模型:

測試右移與 Production Testing:

工具:

  • Testcontainers — 用真實容器跑整合測試
  • Pact — Consumer-Driven Contract Testing

持續測試: