前一天我們透過 AGENTS.md 等專案指引,讓 AI 了解專案架構、程式碼規範和開發方式,讓它寫程式時更符合專案原本的設計。
不過,就算 AI 已經理解專案,也順利把程式碼寫好了,功能就一定能正常運作嗎?
例如,我們請 AI 新增一支筆記搜尋 API,希望使用者輸入 node 時,可以找到標題為 Node.js 入門 的筆記。結果程式看起來沒有什麼問題,實際搜尋卻發現什麼都找不到。
這類錯誤有時候藏得很細,只看程式碼不一定能馬上發現,所以寫完程式之後,還需要實際執行、確認結果,看看功能是不是真的符合需求。
這次就從一支簡單的筆記搜尋 API 開始,讓 AI 自己撰寫測試、執行測試,再根據結果修正程式,看看我們能怎麼把測試放進 AI Coding 的開發流程裡。
先來定義這次的搜尋功能應該怎麼運作。使用者輸入 node、Node 或 NODE,都應該找到 Node.js 入門;如果沒有輸入關鍵字,就回傳 400 狀態碼和錯誤訊息;如果找不到符合的筆記,則回傳 200 狀態碼和空陣列。
這些條件就是我們判斷功能是否正確的依據。接下來會先準備一支有 Bug 的 API,再讓 AI 根據需求撰寫測試,實際找出程式中的問題。
先在專案中安裝 Express 和測試會用到的工具。如果已經有前面的 Node.js 專案,就不需要重新初始化,只要安裝缺少的套件即可。
npm init -y
npm install express
npm install --save-dev jest supertest
這次會用到 Jest 和 Supertest,兩者的工作不太一樣。
Jest 負責執行測試、比對實際結果和預期結果;Supertest 則負責向 Express API 發送請求,讓我們可以檢查回傳的狀態碼和資料。
為什麼需要 Supertest?因為這次要檢查的是一支 API,除了搜尋結果對不對,也需要確認狀態碼和回應格式是否符合預期。使用 Supertest 就能直接對 Express App 發送請求,不必另外啟動伺服器,也不用自己處理測試用的 Port,對這次的練習來說會簡單許多。
接著打開 package.json,在原本的 scripts 欄位中加入測試指令,記得保留其他既有設定。
{
"scripts": {
"start": "node server.js",
"test": "jest --runInBand"
}
}
start 用來啟動 API,test 則會執行 Jest。之後只要輸入 npm test,就能開始跑測試。
接著整理一下檔案結構,把 API、伺服器啟動程式和測試檔案分開放置。
project/
├── app.js
├── server.js
├── package.json
└── tests/
└── notes.test.js
在 app.js 中加入筆記資料和搜尋 API。這裡先使用固定的資料練習,不另外連接 PostgreSQL,讓我們把重點放在搜尋邏輯和測試流程上。
const express = require("express");
const app = express();
const notes = [
{ id: 1, title: "Node.js 入門" },
{ id: 2, title: "Express API" },
{ id: 3, title: "SQL 筆記" },
];
app.get("/api/notes/search", (req, res) => {
const keyword = req.query.keyword;
if (typeof keyword !== "string" || keyword.trim() === "") {
return res.status(400).json({
message: "請提供搜尋關鍵字",
});
}
const result = notes.filter((note) =>
note.title.includes(keyword)
);
return res.status(200).json(result);
});
module.exports = app;
這支 API 會從網址的 Query String 取得關鍵字,檢查輸入是否有效,再搜尋筆記標題並回傳結果。
不過,搜尋這一段其實藏著一個問題:includes() 會區分英文大小寫,所以輸入小寫的 node 時,找不到標題中大寫開頭的 Node.js 入門。這就是等等要透過測試找出來的 Bug。
另外建立 server.js,負責啟動 API 伺服器。
const app = require("./app");
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
把 Express App 匯出後,測試就能直接匯入 app.js,不用先啟動伺服器,也能向 API 發送請求。
API 準備好之後,就可以讓 AI 根據需求撰寫測試了。我們不需要自己把每個測試案例都寫出來,只要先把需求說清楚,再請 AI 使用 Jest 和 Supertest 檢查目前的程式。
例如,可以輸入以下指令,要求 AI 先建立測試、實際執行,再回報結果。
請閱讀目前的筆記搜尋 API,根據需求建立測試。
搜尋功能必須不區分英文大小寫,輸入 node 時應該找到 Node.js 入門;搜尋不到資料時應回傳空陣列;沒有提供關鍵字時,則應回傳 400 與錯誤訊息。
請使用 Jest 和 Supertest 撰寫測試,並將測試放在 tests/notes.test.js。
完成後執行 npm test,確認哪些測試通過、哪些測試失敗,並說明失敗的原因。
這個階段先不要修改 app.js,請先找出目前程式是否符合需求。
這裡先請 AI 檢查原本的程式,暫時不要修改 API,主要是想先看清楚問題出在哪裡。如果一開始就讓 AI 同時修改程式和測試,最後即使測試全部通過,也比較難知道它究竟發現了什麼。
AI 產生的測試可能會像下面這樣。實際操作時,可以直接讓 AI 建立檔案,不用自己逐行輸入。
tests/notes.test.js:
const request = require("supertest");
const app = require("../app");
describe("GET /api/notes/search", () => {
test("搜尋時不區分英文大小寫", async () => {
const response = await request(app)
.get("/api/notes/search")
.query({ keyword: "node" });
expect(response.status).toBe(200);
expect(response.body).toEqual([
{ id: 1, title: "Node.js 入門" },
]);
});
test("沒有符合資料時回傳空陣列", async () => {
const response = await request(app)
.get("/api/notes/search")
.query({ keyword: "找不到的內容" });
expect(response.status).toBe(200);
expect(response.body).toEqual([]);
});
test("沒有提供關鍵字時回傳 400", async () => {
const response = await request(app)
.get("/api/notes/search");
expect(response.status).toBe(400);
expect(response.body).toEqual({
message: "請提供搜尋關鍵字",
});
});
});
這三個測試分別檢查搜尋成功、沒有搜尋結果,以及沒有提供關鍵字的情況。像 request(app).get(...) 就是在向 API 發送 GET 請求,.query() 會把關鍵字帶進網址,而 expect() 則用來確認實際回應是否符合預期。
接著,AI 會執行 npm test,讓程式實際跑過這些測試,而不是只靠閱讀程式碼判斷功能有沒有問題。
以目前這份 API 來說,第一個測試預期會失敗,另外兩個則應該通過。
原因就在搜尋條件使用了 includes(),直接比對原始字串,所以小寫的 node 無法匹配大寫開頭的 Node.js 入門。最後得到的是空陣列,和測試預期的一筆筆記不同。
這時候,測試結果就提供了很明確的線索,讓我們知道問題出在搜尋邏輯,而不是沒有提供關鍵字時的錯誤處理。
找到問題之後,就可以再請 AI 分析原因並修改程式,但這裡有件事需要特別注意:不能只為了讓測試通過,就把測試的預期結果改成錯誤的答案。
例如,原本需求明確要求輸入 node 時要找到 Node.js 入門,如果 AI 發現搜尋不到資料,卻直接把測試改成預期空陣列,測試或許會通過,功能卻還是沒有做好。
因此,可以接著要求 AI 依照原本的需求修正程式,並且重新執行測試。
請根據剛才的測試結果,找出問題並修正 app.js。
請以原本的功能需求為依據,不要為了讓測試通過,就修改或刪除合理的測試案例與預期結果。
如果你認為需求或測試本身有問題,請先說明原因。
修正後請再次執行 npm test,並回報實際結果與目前尚未涵蓋的情況。
這個案例的修正方式很單純,讓搜尋關鍵字和筆記標題在比對之前先轉成小寫即可。原本的程式是:
const result = notes.filter((note) =>
note.title.includes(keyword)
);
可以改成:
const normalizedKeyword = keyword.trim().toLowerCase();
const result = notes.filter((note) =>
note.title.toLowerCase().includes(normalizedKeyword)
);
trim() 會移除關鍵字前後的空白,toLowerCase() 則會將英文轉成小寫。這樣輸入 node、Node 或 NODE,就都能找到同一筆筆記。
修正完成後,再執行一次 npm test。這次三個測試都應該通過,代表目前測試涵蓋的情境已經符合預期。
還不能完全這麼說,因為測試只能確認程式在特定情境下的結果是否正確,測試案例有沒有寫完整,同樣會影響最後的判斷。
例如,AI 如果只測試搜尋成功的情況,卻沒有檢查搜尋不到資料或沒有提供關鍵字時的回應,即使測試全部通過,也不代表其他情況都沒有問題。
所以開發者還是需要確認測試案例是否對應原本的需求,也要留意 AI 是否真的執行測試、有沒有擅自更改測試條件,以及哪些情況還沒有經過檢查。
另外這次只用固定資料練習搜尋邏輯,還沒有涵蓋資料庫、登入驗證或使用者權限。正式專案遇到這些功能時,也需要針對它們設計相應的測試,不能因為眼前的測試全數通過,就認定整個系統已經沒有問題。
前一天,我們讓 AI 理解專案應該怎麼寫;今天則進一步確認它寫出的程式是否符合需求。
這次透過 Jest 和 Supertest,讓 AI 根據需求建立測試、從失敗結果找出搜尋問題,再修正程式並重新驗證。整個過程中,我們不一定要親手寫出每一行測試,但需要清楚知道功能應該怎麼運作,也要判斷測試是否真的檢查到重要的情況。
AI 可以幫忙寫程式、寫測試和修正錯誤,但最後是否符合需求,仍需要開發者來確認。
程式碼完成,只是初步實作;透過測試和再次確認,才能更有依據地判斷它是不是真的做好了。