用 AI 寫單元測試:從產生測試骨架到補齊邊界案例的實戰指南
AI 能在幾秒內產出一整組單元測試,但產出的測試常常只測快樂路徑、斷言薄弱,甚至測到假的行為。本文教你如何引導 ChatGPT 或 Claude 寫出真正有價值的測試,涵蓋邊界案例、例外處理與 mock,並附 Python 與 JavaScript 範例。
AI 寫測試的機會與風險
單元測試是保障程式品質的基礎,但很多人不寫,因為覺得花時間又枯燥。AI 助手大幅降低了寫測試的門檻:你把一個函式貼過去,幾秒鐘就有一整組測試。問題是,AI 產出的測試品質參差不齊。它傾向測「快樂路徑」,也就是輸入正常、輸出正常的情況,卻常常漏掉邊界值、例外處理與異常輸入。更糟的是,AI 有時會寫出「反映目前程式行為」的測試,即使那個行為本身就是 bug,測試照樣通過,反而把錯誤固化下來。
所以用 AI 寫測試的重點,不在於讓它產出「能跑」的測試,而在於引導它產出「能抓到問題」的測試。本文拆解這套引導方法。
第一步:給 AI 明確的測試目標
把函式貼給 AI 時,不要只說「幫我寫測試」,要說清楚你關心什麼。以下是一個購物車折扣函式:
def apply_discount(price: float, rate: float) -> float:
if rate < 0 or rate > 1:
raise ValueError("rate must be between 0 and 1")
return round(price * (1 - rate), 2)
有效的提問會這樣寫:
請為這個函式用 pytest 寫單元測試,要求涵蓋:
1. 正常折扣計算(例如原價 100 打八折)
2. 邊界值:rate 為 0 與 1
3. 非法輸入:rate 為負數與大於 1,應拋出 ValueError
4. 浮點數四捨五入是否正確
請每個測試用清楚的函式名描述它在測什麼。
明確列出要涵蓋的面向,AI 就不會只寫一個快樂路徑。
第二步:檢視 AI 產出的測試品質
AI 可能產出這樣的測試:
import pytest
from shop import apply_discount
def test_normal_discount():
assert apply_discount(100, 0.2) == 80.0
def test_zero_rate():
assert apply_discount(100, 0) == 100.0
def test_full_discount():
assert apply_discount(100, 1) == 0.0
def test_negative_rate_raises():
with pytest.raises(ValueError):
apply_discount(100, -0.1)
def test_rate_above_one_raises():
with pytest.raises(ValueError):
apply_discount(100, 1.5)
def test_rounding():
assert apply_discount(9.99, 0.1) == 8.99
看起來不錯,但你要用批判眼光檢查:斷言是否夠精確?有沒有測到浮點數陷阱?test_rounding 這個斷言是對的嗎?9.99 乘 0.9 等於 8.991,四捨五入到兩位是 8.99,正確。但如果 AI 寫成 == 8.9910000001 之類的浮點誤差值就有問題。這一步需要你自己心算或實際跑一次驗證,不能盲信。
第三步:主動要求邊界與異常案例
AI 最容易漏的就是邊界。你可以用一個通用清單去逼問它:
- 空值:null、None、空字串、空陣列
- 邊界數字:0、負數、最大值、最小值
- 型別錯誤:傳入字串卻預期數字
- 重複與極端量:非常大的輸入、重複元素
- 並發:多執行緒同時呼叫
提問範例:「這組測試漏掉了哪些邊界案例?請針對空輸入、型別錯誤與極大值再補三個測試。」這種主動追問,往往能挖出你自己也沒想到的漏洞。
第四步:處理外部相依,善用 mock
真實函式常會呼叫資料庫、API 或檔案系統,這些不該在單元測試裡真的執行。AI 很擅長寫 mock,但你要告訴它相依是什麼。以 JavaScript 的 Jest 為例:
// notifier.js
export async function notifyUser(userId, sendEmail) {
const user = await db.getUser(userId);
if (!user.email) throw new Error('no email');
return sendEmail(user.email, 'Hello');
}
提問:「請用 Jest 為 notifyUser 寫測試,用 mock 取代 db.getUser 與 sendEmail,涵蓋:使用者有 email、沒有 email、db 拋錯三種情況。」AI 會產出:
import { notifyUser } from './notifier';
import { db } from './db';
jest.mock('./db');
test('sends email when user has email', async () => {
db.getUser.mockResolvedValue({ email: '[email protected]' });
const sendEmail = jest.fn().mockResolvedValue(true);
await notifyUser(1, sendEmail);
expect(sendEmail).toHaveBeenCalledWith('[email protected]', 'Hello');
});
test('throws when user has no email', async () => {
db.getUser.mockResolvedValue({ email: null });
const sendEmail = jest.fn();
await expect(notifyUser(1, sendEmail)).rejects.toThrow('no email');
expect(sendEmail).not.toHaveBeenCalled();
});
test('propagates db error', async () => {
db.getUser.mockRejectedValue(new Error('db down'));
const sendEmail = jest.fn();
await expect(notifyUser(1, sendEmail)).rejects.toThrow('db down');
});
注意第二個測試不只斷言拋錯,還斷言 sendEmail 沒有被呼叫,這種「否定斷言」能抓到副作用外洩的 bug,是高品質測試的特徵。
第五步:警惕「固化錯誤行為」的測試
這是用 AI 寫測試最危險的陷阱。AI 看到程式碼,會傾向寫出讓現有程式通過的測試。假設你的函式其實有 bug,AI 卻寫了一個測試去斷言那個錯誤結果,測試就變成保護 bug 的圍牆。
避免方法有兩個。第一,先自己想清楚「正確行為應該是什麼」,把期望值寫在提問裡,而不是讓 AI 從程式碼反推。第二,採用測試先行的思路:先寫測試描述正確行為,看它失敗,再修程式讓它通過。你可以請 AI「根據這份規格寫測試」,而不是「根據這段程式碼寫測試」,兩者產出的品質天差地遠。
第六步:檢視覆蓋率但不迷信數字
寫完測試可以跑覆蓋率工具,例如 Python 的 coverage、JavaScript 的 --coverage。但要記住,100% 覆蓋率不代表沒有 bug,它只代表每行程式都被執行過,不代表每種情況都被驗證。一行 return a / b 被執行到,不代表你測了 b 為 0 的情況。所以覆蓋率是找「完全沒測到的死角」的工具,不是品質保證。可以請 AI:「這個檔案覆蓋率報告顯示第 20 到 25 行沒被測到,請幫我補測試涵蓋這段。」
小結
AI 是強大的測試產生器,但不是測試設計師。它能快速產出骨架、mock 與大量案例,省下打字時間,但測試要測什麼、期望值是什麼、有沒有固化錯誤,這些判斷必須由你來下。把 AI 當成加速器,自己保留對「正確性」的最終決定權,才能寫出真正保護程式品質的測試。
常見問題
AI 寫的測試都通過了,是不是就代表程式沒問題?
不一定。AI 傾向根據現有程式碼行為寫測試,如果程式本身有 bug,測試可能斷言的正是那個錯誤結果,全數通過卻保護了 bug。建議先明確定義正確行為再讓 AI 依規格寫測試,並自己驗證關鍵斷言的期望值。
該用哪種提問方式讓 AI 寫出更完整的測試?
不要只說「幫我寫測試」。要列出你關心的面向,例如正常路徑、邊界值、非法輸入、例外處理與外部相依的 mock,並要求每個測試用清楚的名稱說明它在驗證什麼。明確的目標會讓 AI 跳出只測快樂路徑的習慣。
測試需要呼叫資料庫或 API,AI 能處理嗎?
能,但你要告訴它有哪些外部相依,並要求用 mock 取代。好的做法是明確列出要模擬的情境,例如相依回傳正常值、回傳空值、拋出錯誤三種,並要求斷言副作用是否被正確觸發或抑制。
覆蓋率達到 100% 是不是就夠了?
不夠。覆蓋率只表示每行程式被執行過,不代表每種輸入情況都被驗證。例如除法那行被執行到,不代表你測了除以零。覆蓋率適合用來找完全沒測到的死角,但不能當成品質保證,仍需人工檢視斷言是否夠嚴謹。
測試先行(TDD)和讓 AI 看程式碼寫測試哪個好?
若追求品質,測試先行更好。讓 AI 根據規格或期望行為寫測試,會產出真正驗證正確性的測試;讓 AI 看著現有程式碼寫,容易產出只反映當前行為的測試。實務上可先用規格產生核心測試,再用 AI 補齊邊界案例。