iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Software Development

AI時代下的軟體工程系列 第 10 篇

Day10: Test:把預期行為寫成可執行的例子

  • 分享至 

  • xImage
  •  

What is testing

Error、Fault、Failure

討論測試之前,先把「錯」分成三個層次:

  • Error(錯誤):人犯的錯,例如誤解需求、算錯邊界條件。
  • Fault(缺陷):error 留在程式碼裡的結果,也就是一般說的 bug。
  • Failure(失效):含有 fault 的程式碼被執行,實際行為和預期不符。

三者的關係是:人犯下 error,在程式裡留下 fault,fault 被執行到時才表現為 failure。測試能直接觀察到的只有 failure,fault 和 error 都要從 failure 往回推。一個 fault 如果從來沒被執行到,測試就看不見它。

Test case

測試(testing)是用一組 test case 執行程式,觀察是否發生 failure。一個 test case 至少包含兩個部分:輸入與預期輸出。

以軟體測試教科書常用的三角形問題為例:輸入三邊長,判斷能組成哪一種三角形。

輸入 (a, b, c) 預期輸出
(5, 5, 5) 正三角形
(5, 5, 3) 等腰三角形
(3, 4, 5) 不等邊三角形
(1, 2, 5) 無法構成三角形

只有輸入不算 test case。沒有預期輸出,執行完也無從判斷對錯。決定「預期輸出是什麼」的依據稱為 test oracle,這往往是測試中最難的部分。

測試的層級

依照測試的範圍,常見的分法是 V-model:

圖片:V-model 中的測試層級,左側由上而下為 Requirements Specification、Preliminary Design、Detailed Design,底部為 Coding,右側由下而上為 Unit Testing、Integration Testing、System Testing,左右以虛線對應

圖片來源:Paul C. Jorgensen, Byron DeVries, Software Testing: A Craftsman's Approach, 5th Edition

左邊從需求一路細化到程式碼,右邊把程式碼逐層組回去並驗證。虛線表示每一層測試對照的依據:

對照的依據 測試層級 驗證的對象
Requirements Specification System Testing 整個系統是否滿足需求
Preliminary Design Integration Testing 模組之間的介面與互動是否一致
Detailed Design Unit Testing 單一函式或類別是否照設計運作

V-model 是一種偏瀑布式的開發流程,但它指出的對應關係不受開發流程限制:每一層測試,都是拿程式去對照某一份預期。

Why we need test

一次沒被測到的 fault

2024 年 7 月 19 日,資安公司 CrowdStrike 推送了一份例行的偵測規則更新。更新發布後,全球約 850 萬台 Windows 電腦陸續藍屏、無法開機,航空、銀行、醫院與電視台的系統同時停擺。光是 Delta 航空就取消了數千個航班,並表示損失約 5 億美元。

根據 CrowdStrike 事後公布的根因分析,問題出在一個欄位數量的不一致:規則的範本定義了 21 個輸入欄位,負責比對的程式卻只提供 20 個。這份更新第一次用到了第 21 個欄位,程式讀取超出範圍的記憶體而崩潰。

這個 fault 並不是那天才出現的。它早已存在於程式中,只是先前的測試與規則從未執行到第 21 個欄位。報告中列出的改善項目,第一項就是補上相關的測試。

越晚發現,代價越高

同一個 fault,在不同時間點被發現,處理的代價截然不同:

  • 寫程式時,由 unit test 發現:修改一個函式,重新執行測試。
  • 整合時,由 integration test 發現:需要追查是哪個模組的假設有誤,可能牽動多個團隊。
  • 上線後,由使用者發現:除了修正程式,還要處理停機、資料修復、客戶賠償與信任損失。

回頭看 V-model,這個差距來自回溯的距離。越右上方發現的 failure,越需要沿著左側往回推,才能找到最初的 error。CrowdStrike 的 fault 如果在開發階段被一個 test case 執行到,修正成本只是幾行程式;到了 850 萬台電腦上,它變成了一場需要逐台手動修復的事故。

需求、程式與測試

Jorgensen 用一張文氏圖描述程式行為:

圖片:Program Behaviors 文氏圖,S 圈為 Specification(expected),P 圈為 Program(implemented),T 圈為 Test Cases(verified),三圈交疊形成 1 到 7 號區域,圈外為 8 號區域;S 圈旁標註 Spec-based testing,P 圈旁標註 Code-based testing

圖片來源:Paul C. Jorgensen, Byron DeVries, Software Testing: A Craftsman's Approach, 5th Edition

  • S(Specification):需求要求的行為。
  • P(Program):程式實際做出的行為。
  • T(Test Cases):測試驗證過的行為。

理想情況下三個圈完全重合,實際上一定會錯開。S 和 P 的落差對應兩種 fault:

  • S 有、P 沒有(區域 4、5):需求要求了,程式沒做到,稱為 fault of omission。
  • P 有、S 沒有(區域 3、6):程式做了需求沒要求的事,稱為 fault of commission。

T 圈的作用,是把這些落差找出來。區域 4 是需求有寫、測試有驗、但程式沒做到——測試會失敗,fault 被發現了。區域 5、6 則是落差存在,卻沒有任何測試涵蓋,只能等使用者遇到。

測試的目標,是讓區域 1 盡可能大。

測試的限制

Dijkstra 有一句常被引用的話:

Program testing can be used to show the presence of bugs, but never to show their absence.

測試只能執行有限的輸入,而大多數程式的輸入空間都遠大於此。三角形問題只有三個整數,組合數就已經無法窮舉。所以測試通過,只代表在 T 圈涵蓋的範圍內沒有發現 failure,不代表程式正確。

這也是為什麼「怎麼挑選 test case」本身是一門學問。

Spec-based 與 Code-based

圖上標出了挑選 test case 的兩個方向:

方法 別名 依據 找不到的問題
Spec-based testing Functional、Black-box 需求規格 需求沒寫、程式卻做了的行為(區域 6)
Code-based testing Structural、White-box 程式碼結構 需求有寫、程式卻沒做的行為(區域 5)

兩者各有盲點。Spec-based 只看需求,不知道程式偷偷多做了什麼;code-based 只看程式,無從得知有什麼該做卻沒做。實務上兩種方法互相補充。

常見的 test 工具

語言 代表工具 使用例子
Python pytest pytest:執行 test_*.py 裡的測試
JavaScript / TypeScript Vitest、Jest npx vitest run:執行專案測試
Go go test go test ./...:執行所有套件的測試
Rust cargo test cargo test:執行專案測試
C++ GoogleTest 建置後透過 ctest 執行
Java JUnit 5 mvn test 或 gradle test

Go 和 Rust 將測試工具內建在語言的工具鏈中,其他語言則需要額外安裝測試框架。

test 如何幫助 AI

測試給 coding agent 的,是一個可以自己執行、自己判斷的完成條件:

讀取需求與測試 → 修改程式 → 執行測試 → 依失敗訊息修正

失敗訊息會指出哪個 test case、預期輸出是什麼、實際輸出是什麼。這比自然語言描述的需求更精確,agent 不需要猜「做到什麼程度才算完成」。

AI 寫的測試,oracle 從哪裡來?

AI 也能寫測試,而且很快。常見的做法是先請 AI 實作,再請它「幫這段程式補上測試」。

這時 AI 手上唯一的依據就是實作本身,它寫出的是 code-based testing,而且預期輸出是從程式的行為抄來的。如果實作誤解了需求,測試會照著同樣的誤解寫下預期輸出。結果是全部通過,但這些測試落在區域 3:有實作、有驗證,卻不在需求裡。

這類測試少了 oracle。它只能證明程式和自己一致,無法發現 fault of omission,也不會縮小區域 5。

分工:人決定預期,AI 負責實作

比較穩的分工,是讓 oracle 留在知道需求的人手上:

  • 由人決定 test case 的預期輸出。 輸入可以請 AI 幫忙列舉,但每個輸入「應該得到什麼」,要對照需求決定。
  • 把 test case 當成規格交給 AI。 先寫好會失敗的測試,再請 AI 讓它們通過。
  • 分開 review 測試與程式的 diff。 留意 agent 是否為了讓測試通過而修改預期輸出、跳過或刪除測試。讓 failure 消失,不等於修好 fault。

Reference


上一篇
Day9: Type Checker:檢查是否有矛盾的地方
下一篇
Day11: CI:更新共用版本前的最後一道關卡
系列文
AI時代下的軟體工程 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言