首頁/測試

軟體測試基本觀念與有效測試實作

2026年05月15日 測試

軟體測試基本觀念與有效測試實作

「測試到底有沒有用」這個問題,答案不在於有沒有寫測試,而在於寫的測試是不是有效。覆蓋率 95% 但全部測在 happy path 上、案例跑 5 分鐘卻抓不到上線的 bug、寫了一堆但下次需求一改就得整批重寫——這些情況,測試都寫了,但沒有用。


軟體測試到底在做什麼

軟體測試常見有 3 種講法,其實是同一件事的不同角度:

角度 定義 看重
找錯 為了證明程式有錯而執行程式、找出問題 抓 bug、預防上線出包
跑案例 根據規格設計一批測試案例,跑這些案例找錯 流程化、可重複、可審計
驗證需求 確認軟體有沒有滿足使用者顯性或隱性的需求 對使用者交代

3 種角度合起來才是完整的測試:有人專心找 bug、有流程確保覆蓋面,最後還要回到「使用者要的東西有沒有做到」。這 3 個角度在傳統軟體和 AI 系統都適用,差別只在 AI 系統裡「需求」的形狀比較模糊。


冒煙測試:先看會不會炸

「冒煙測試(Smoke Test)」這個名字來自硬體業,早年工程師會把電路板插電,看看會不會短路冒煙。如果冒煙了,後面複雜的測試就不用跑了。

軟體沿用這個比喻:每次 deploy 完,先跑一輪最基本的功能,確保系統還能啟動、登入、發 request 都不會直接 crash。冒煙過了,才做後續詳細測試。

系統類型 冒煙測試做什麼
一般 Web 服務 process 起得來、首頁能打開、健康檢查回 200、最關鍵 API 不 500
LLM / RAG 服務 模型 API 連得到、token 沒爆額度、空 prompt 不 crash、產出 1 個 token 都行
資料管線 DB 連得上、必要 table 存在、最小一筆資料能寫進讀出

黑箱測試 vs 白箱測試

兩種測試的差別不在測什麼,而在測的人能不能看到程式碼:

圖一:黑箱測試 vs 白箱測試

黑箱測試

只看「輸入什麼進去、輸出什麼出來」,程式內部怎麼寫都不管。

  • 程式碼怎麼重構都不影響測試

  • 不會寫程式的人也能寫測試

  • QA / 測試人員主要做這個

白箱測試

看程式碼內部結構,確保「每一行 / 每個分支 / 每條路徑」都有被測到。

涵蓋程度由鬆到嚴:

涵蓋率 要求
語句涵蓋率 每行程式碼都跑過至少一次
分支涵蓋率 每個 if/else 的兩條分支都跑過
路徑涵蓋率 所有可能的執行路徑組合都跑過

實務上追求 80%+ 的分支涵蓋率算合理目標,而且兩種都要做:QA 做黑箱、開發寫單元測試(屬於白箱性質)。


單元測試與 Stub

「單元」= 一個函數、一個 class、一個小模組,只測這一塊的行為,不測它依賴的東西。

圖二:單元測試 + Stub

為什麼需要 Stub

直接拿真實依賴跑會有 4 個問題:跑很慢(連 DB)、依賴有 bug 影響測試、外部 API 不穩、結果不可重現。

解法是用「假的依賴」(Stub)取代真實依賴,回傳固定、預期的值。

// 真實版:會連 DB、跑很慢
int D(int x) { /* 連 DB、查表、計算... */ }

// Stub:直接回固定值
int StubD(int x) { return x + 5; }

測 B 時用 StubD,這樣:跑很快、依賴有 bug 不影響、結果穩定、出錯就 100% 是 B 的問題。

AI 時代的 Stub:mock 掉 LLM API

每次測試打真 LLM 太慢又貴又不穩。對應做法是 mock LLM 回固定 response

# 真實:呼叫 LLM
def summarize(text):
    return openai.chat.completions.create(...)

# 測試時:mock 掉
def stub_summarize(text):
    return "這是 mock 出來的摘要"

這樣可以測「程式怎麼處理 LLM 回應」,例如「LLM 回空字串時要不要重試?」「LLM 回傳格式不合怎麼降級?」這些都是程式邏輯,跟 LLM 品質無關。

Mock vs Stub(補充)

名詞 做什麼
Stub 給假資料、讓被測函數能跑下去
Mock 給假資料 + 檢查「有沒有被正確呼叫」(次數、參數)

多數情境用 Stub 就夠,需要驗證「呼叫了幾次、參數對嗎」才升級到 Mock。


整合測試:把幾個模組組起來測

單元測試只看「一個函數」,但實際系統是很多模組組起來跑。模組與模組之間最常出包:欄位名字對不上、null 處理不一致、SQL 寫錯、API 契約破裂。這些單元測試抓不到,需要整合測試。

圖九:整合測試 ─ 多模組 + 真實依賴

整合測試 vs 單元測試的差別

對比點 單元測試 整合測試
範圍 1 個函數 多個模組組起來
依賴 全部用 Stub 內部依賴用真的(DB、Redis、內部 service)
速度 毫秒級 秒級
抓的 bug 函數邏輯錯 模組之間的接縫錯

範例:使用者註冊流程

一個 POST /api/register 會經過:

  1. API 層接收輸入

  2. RegisterService 呼叫驗證模組、建帳號模組、寄信模組

  3. 寫進真實 DB

整合測試把這條路全部接起來跑,DB 用測試專用的 schema,跑完就清掉。

def test_register_creates_user_and_sends_email():
    # Arrange:清空測試 DB
    test_db.clear()

    # Act:打真的 API
    response = client.post("/api/register", json={
        "email": "[email protected]",
        "password": "Secret123"
    })

    # Assert:驗 API 回應、驗 DB 真的寫入、驗信被寄出
    assert response.status_code == 201
    assert test_db.query(User).filter_by(email="[email protected]").count() == 1
    assert mock_email_sender.was_called_with(to="[email protected]")

哪些依賴要用真的、哪些可以 mock

依賴類型 做法 原因
自家 DB(PostgreSQL / Redis) 用真的(測試 schema) 抓 SQL 錯、schema 不符
自家內部 service 用真的 抓模組契約不一致
第三方 API(金流、寄信、LLM) mock 掉 不要每次測試都打到別人家、不要花錢
檔案系統 用真的(臨時目錄) 抓路徑 / 權限問題

原則:自家的東西用真的,別人家的用假的。

整合測試專抓的 4 種 bug

  1. 欄位名字對不上:Service 傳 userId,Repository 期待 user_id

  2. null 處理不一致:A 層回 null,B 層假設一定有值,瞬間炸

  3. transaction 範圍錯:寄信寄完了,DB 卻 rollback

  4. API 契約破裂:後端改了 response 格式,前端沒同步

這 4 種沒人會在單元測試裡看到,因為問題出在模組之間。


E2E 測試:模擬真實使用者

E2E(End-to-End,端到端)=從使用者點按鈕、到看到結果,整條路用真的跑一次。

圖十:E2E 測試 ─ 從使用者到資料庫整條路

E2E 測試在驗什麼

不是只看「API 回 200」,而是看使用者真的看到、摸到的東西:

  • 按按鈕後,網址有沒有跳到 /home

  • 畫面有沒有顯示「歡迎,王小明」?(DOM 真的長出來了沒)

  • DB 裡 last_login_at 有沒有被更新?

  • Cookie / localStorage 有沒有 token?

常見工具

工具 特色
Playwright Microsoft 出的,跨瀏覽器、API 設計現代、近兩年首選
Cypress 開發體驗最好、debug 介面強,但只支援 Chromium 系
Selenium 老牌、跨語言支援最廣,相對較重
Puppeteer Google 出的,Chromium 專用,輕量

E2E 的缺點

缺點 為什麼
跑得很慢 一個案例幾十秒到幾分鐘
容易 flaky 網路延遲、動畫沒跑完、時序問題會偶發失敗
改 UI 就壞 改個按鈕的 selector,10 個測試紅燈
Debug 困難 失敗了不知道是前端、後端、DB 哪一塊壞

適合用 E2E 測試的情境

不是每個流程都要寫 E2E,只挑真的不能壞的關鍵路徑寫:

  • 登入 / 註冊

  • 結帳 / 付款

  • 跨多個系統的整合流程(不容易切片驗證)

  • 「壞了客訴會爆」的路徑

其他流程(次要設定頁、後台管理)用單元 + 整合測試就夠。

對抗 flaky 的常見手段

  • waitForSelector 等元素出現,不要寫死 sleep

  • 重要案例給 retry 機制(連續失敗 3 次才算真的壞)

  • E2E 跑在獨立的 staging 環境,不要跟其他人的測試打架

  • 失敗時自動截圖 + 錄影,方便事後 debug


怎麼做有效的測試

知道測試在做什麼之後,重點是怎麼寫出真正有效的測試。

一個好的測試長什麼樣:AAA 模式

「AAA」是 Arrange-Act-Assert 的縮寫,是業界公認最清楚的單元測試結構:

圖五:AAA 模式 ─ 一個好測試的 3 段結構

3 個階段

def test_shopping_cart_calculates_total_correctly():
    # ① Arrange:準備好被測對象與輸入
    user = create_user("Alice")
    cart = ShoppingCart(user)
    cart.add(book, quantity=2)

    # ② Act:執行要測的那一個動作
    total = cart.calculate_total()

    # ③ Assert:檢查結果
    assert total == 200

3 段用空行分開,看 5 秒就知道這個測試在驗什麼。

為什麼這樣寫有效

三段分開帶來三個好處。看 5 秒就懂測試在驗什麼,不必整段讀完才知道哪一行是重點。失敗時也容易定位:是 Arrange 出錯、Act 錯,還是 Assert 寫錯,三段分開一眼就能查。而且 Act 通常只有一行,等於強迫每個測試只聚焦單一行為,不會把兩件事混在一起測。

命名也是有效的關鍵

測試函數的名字要直接讀出測試意圖:

# ✗ 壞:看名字不知道在測什麼
def test_cart()

# ✓ 好:給定什麼、做什麼、預期什麼
def test_shopping_cart_with_2_books_returns_double_price()

# ✓ 也行:簡短但描述行為
def test_calculate_total_sums_all_items()

命名建議用 given_when_thenshould_do_X_when_Y 格式,測試壞了,光看名字就知道是哪個行為壞了。


怎麼設計測試案例:等價類劃分+邊界值

寫測試的人常常卡在「我要測哪些輸入?」暴力測所有可能值不現實,要用等價類劃分和邊界值這兩個技巧:

圖六:等價類劃分 + 邊界值

等價類劃分

把「行為相同的輸入」分成一群,每群挑 1-2 個代表來測。

例:年齡欄位接受 18-65 歲:

  • 等價類 A(太小):0、1、5、17 都會被拒絕,這群挑「10」當代表就好

  • 等價類 B(合法):18、25、40、65 都會被接受,挑「30」當代表

  • 等價類 C(太大):66、80、100 都會被拒絕,挑「80」當代表

從 100 個案例縮成 3 個,等於少做 97 個重複測試。

邊界值

Bug 最容易發生在「界線」上:「>= 18」誤寫成「> 18」、「<= 65」誤寫成「< 65」。

所以每個界線都要測「界線本身」+「界線 ±1」:

界線 要測的值
18(合法區下界) 17(剛好不行)、18(剛好可以)
65(合法區上界) 65(剛好可以)、66(剛好不行)

加上等價類的 3 個代表(10、30、80),總共 7 個案例就能涵蓋這個欄位的所有風險。

進階:決策表

當有多個欄位互相影響時,等價類不夠用,要用決策表列出所有條件組合。

例:折扣計算規則「VIP + 滿千 → 9 折;VIP + 未滿千 → 95 折;非 VIP + 滿千 → 95 折;非 VIP + 未滿千 → 不打折」。每個 row 就是一個測試案例,不會漏掉組合。


測試金字塔:什麼測試該多、什麼該少

不是「多寫測試」就好,寫對地方的測試才有效。業界共識是「測試金字塔」:

圖七:測試金字塔(傳統 + AI 系統)

傳統金字塔

從底層大量、頂層少量:

層級 測什麼 速度 / 成本 比例
單元測試 單一函數,用 Stub 隔離依賴 毫秒級、極便宜 70%
整合測試 幾個模組組起來測(含 DB / API) 秒級、便宜 20%
E2E 測試 完整使用者流程、跑瀏覽器 分鐘級、貴 10%

為什麼是金字塔形狀

  • 下層的 100 個案例的成本 ≈ 頂層 1-2 個:單元測試跑 1000 個只要幾秒,E2E 跑 10 個就要幾分鐘

  • 失敗訊號精準度倒過來:E2E 失敗你不知道是哪一塊壞了,單元測試失敗就 100% 知道是哪個函數

  • 越底層越穩定:E2E 容易 flaky(隨機失敗),單元測試結果幾乎 100% 一致

常見反模式:冰淇淋甜筒

把金字塔倒過來,大量 E2E、手工測試堆在上面,底層幾乎沒有單元測試,這是業界最常見、也最痛的反模式:

圖十一:冰淇淋甜筒反模式 vs 正常金字塔

特徵:

  • E2E 跑很久、常 flaky(偶發失敗)

  • 改一行 code 要等 1 小時才知道有沒有壞

  • 出錯時不知道是哪裡壞了

  • 改個 button 樣式,10 個 E2E 紅燈

為什麼會掉進這個坑:

  • 早期沒寫單元測試,靠 QA 點來補

  • E2E「看起來最像真的」、好說服老闆

  • 程式碼沒設計成可單元測試 → 後來只能從外面整條路測

如果你的團隊長這樣,先從底層補單元測試開始,慢慢把上層的測試往下移。每補一層底,上層就能瘦一些。

AI 系統的金字塔

AI 系統的形狀類似,但每一層測的東西不同:

層級 測什麼 工具
底層(多) 程式邏輯(mock LLM 後測「LLM 回空怎辦」「解析失敗怎辦」) 一般 unit test + mock LLM
中層(中) LLM 答案的品質、faithfulness、相關性 金樣本 + rubric + LLM-as-Judge
頂層(少) 真實情境下使用者滿不滿意 人類專家 review、A/B 測試、👍/👎

3 層都不能少:只做底層會錯過 LLM 品質問題,只做頂層又跑得慢又貴。


把測試自動化進 CI/CD:沒接 CI 的測試等於沒寫

寫完測試如果不上 CI、不是每次 push 都跑,這些測試半年內就會跟現實脫節,變成廢紙。

圖八:CI/CD 中的測試管線

5 個階段,每階段該跑什麼

階段 觸發 跑什麼 目標時間
① Commit / push 每次提交 Lint、單元測試、型別檢查 秒級
② PR 開出 / 更新 開 PR 或 push 更新 整合測試、API 契約測試 分鐘級
③ 合進 main merge E2E 測試、視覺迴歸 10 分鐘級
④ Pre-release 標記要 release 性能 / 安全掃描、跨瀏覽器、人類抽樣 小時級
⑤ 上線後 部署完成 冒煙測試、健康檢查、異常監控 持續

設計原則

越早期的階段要越快、越多;越後期要越完整,但跑得少。

理由是讓 bug 在離開發者最近的時間點被攔住:commit 時抓到,1 分鐘就能改完;上線後才抓到,要客服、緊急修補,可能還要賠錢。

接 CI 的 3 個基本要求

  1. 每次 push 自動跑:不能靠工程師自己記得跑

  2. 失敗就擋下來:unit test 不過、PR 不准 merge

  3. 結果要清楚:失敗時 1 秒內看出是哪個測試壞了、哪一行 assert 沒過

GitHub Actions、GitLab CI、Jenkins、CircleCI 都能做到。沒做到這 3 件,測試就只是寫好但沒人看的文件。


軟體測試的 6 大原則

業界共識的 6 條基本原則,多數來自 ISTQB 國際測試認證教材:

圖三:軟體測試 6 大原則 cheat sheet

精簡版(詳細推理直接看圖):

# 原則 一句話
1 測試證明有錯、不證明沒錯 「測試都過」≠「沒 bug」
2 不可能 100% 測完 追求 good-enough,依風險決定
3 越早測越好 從需求分析就介入,越晚改越貴
4 80/20 缺陷群集 bug 集中在少數模組,加倍測它們
5 殺蟲劑悖論 老案例抓不到新 bug,要持續更新
6 看情境而定 銀行 vs 遊戲 vs AI 系統的測試重點完全不同

這 6 條講的是心態和策略,不是技術。寫了 100 個測試卻忽略這 6 條,多半還是會出包。


AI 系統測試的新挑戰

前面的基本觀念跟做法,在傳統軟體和 AI 系統都適用。但 AI 系統有 4 個傳統軟體沒有的特性,讓「跑測試、看通不通」這套不太夠用:

圖四:傳統軟體測試 vs AI 系統測試

確定性沒了

傳統軟體:f(2, 3) 永遠回 5。 AI 系統:同樣 prompt 跑兩次,答案可能不一樣。

對策:測試時 temperature = 0;多次取中位數而不是單次評分。

輸出空間爆炸

傳統軟體:輸出是 int / bool / JSON,assertEqual 就夠。 AI 系統:輸出是自由文字,正確答案有無數種。

對策:用 rubric(評分表) 從多個面向評答案的品質,配合 [LLM-as-Judge](../LLM/AI 評委打分:評分表怎麼設計.md)。

版本漂移

傳統軟體:升級套件 → 看 changelog。 AI 系統:模型升一次版本,同樣的輸入分數可能整組跑掉。

對策:釘死模型版本(用 gpt-4-0613 而非 gpt-4),每季用金樣本重跑對照。

失敗的長相

傳統軟體:throw exception → 一眼看出。 AI 系統:幻覺,講得頭頭是道,其實是錯的。

對策:建「金樣本」回歸測試集;對 RAG 系統檢查 faithfulness(每個說法都能對應原文)。


AI 系統測試對策三件套

濃縮成 3 件事:

動作 做什麼 為什麼
建金樣本 50-100 個有共識的 input + expected 對抗模型漂移、能做迴歸測試
rubric + LLM-as-Judge 用 rubric 從多個面向評分,請另一個 AI 當評委 解決「自由文字怎麼評」
釘死模型版本 不要用 latest tag 升級 → 重新跑金樣本 → 確認可接受才換

3 件都做到,AI 系統的測試就能做到可重現、可信、能被 CI 自動化,這正是傳統軟體測試原本就有的基本要求。


用 AI 幫忙寫測試?

近兩年另一個趨勢是用 AI 來寫測試(Copilot、Cursor、Claude Code 都很會這個),但要小心:

AI 寫測試的常見陷阱

陷阱 1:AI 寫的測試常常只是把「code 在做的事」變成 assertion。

例如某個函數有 bug,回傳的值是錯的。AI 看了 code,寫出來的測試卻是「assert 回傳值等於 code 算出來的值」,測試永遠會過,bug 永遠抓不到。

AI 看的是現在的程式碼,不是程式碼應該做什麼。需求面的 bug,它看不出來。

陷阱 2:測試覆蓋率高 ≠ 測試有用。

AI 很會把覆蓋率衝到 95%,但衝的都是 happy path,邊角案例沒測到。重要的失敗模式,像是同時併發、網路斷線、惡意輸入,AI 不會主動想到。

正確使用方式

把 AI 寫的測試當作起點,不是終點:

  1. 讓 AI 產一批基礎測試(happy path、obvious edge cases)

  2. 人類 review:補上 AI 沒想到的邊角案例、確認測試在驗證「規格」而非「現有 code」

  3. 對重要模組,人類主動寫幾個刁鑽案例

最佳組合:AI 寫 80% 的常規案例、人類寫 20% 的關鍵案例。


小結

「有效的測試」需要 3 件事,從具體到抽象:

  1. 形式正確:每個測試用 AAA 模式寫、命名說明意圖,用等價類加邊界值設計案例

  2. 比例正確:測試金字塔——底層大量單元測試、中層整合測試、頂層少量 E2E

  3. 流程正確:接 CI/CD、每次 push 自動跑、失敗就擋下來

3 件都做到,寫測試的時間才不會白費。

AI 時代多兩個技能:用 Stub mock 掉 LLM 來測程式邏輯,用 LLM-as-Judge、金樣本、釘死版本這三招來測 LLM 品質。

測試不能證明沒錯,只能證明目前測的情境沒錯;不可能測到 100%,時間要花在風險最高的地方;越早測越好,需求階段就該開始想「這要怎麼測」。把這幾句話放在心上,會比追求覆蓋率數字有用得多。