iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Build on Google AI

30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記系列 第 22 篇

Day 22:專案正式合體!先替 DevPulse 準備三個真的會壞掉的 Bug

  • 分享至 

  • xImage
  •  

前三週先把 Google AI 生態系拆開來認識。

Gemini API 負責模型呼叫,Zod 負責結構化資料,Genkit 負責 AI Flow,Stitch 負責介面生成,Antigravity 則開始接觸 Agentic 開發。

到了第四週,這些東西不再只是獨立的實驗。

DevPulse 正式開始組裝。

不過第一天的任務不是急著把 Gemini 接進來,而是先準備好一組固定的「問題」。

因為後面要讓 AI 找 Bug、產生修復程式碼,最後還要驗證修復結果,前提是必須先有可以穩定重現的 Bug 與測試。

所以 Day 22 先把最基礎的地基做好:

Bug 程式碼
    ↓
Jest 測試
    ↓
❌ Fail
    ↓
後續交給 AI 修復

今天先不追求 AI。

先讓「紅燈」站穩。


一、從零建立 DevPulse

先建立新的專案目錄:

mkdir devpulse
cd devpulse
npm init -y

接著安裝 Jest:

npm install -D jest

再把 package.json 的測試指令改成:

{
  "scripts": {
    "test": "jest"
  }
}

專案初期先維持最簡單的結構:

devpulse/
├─ samples/
├─ tests/
├─ package.json
├─ package-lock.json
└─ node_modules/

今天的內容刻意不先加入 Genkit。

按照這幾天的學習節奏,工具不需要一次全部塞進來,而是每一天只增加一個真正需要的能力。


二、第一個 Bug:forEach 搭配 await

第一個案例是非同步程式碼。

建立:

samples/async-await.cjs

內容:

function sendAll(users, sendMail) {
  const results = [];

  users.forEach(async (user) => {
    const result = await sendMail(user);
    results.push(result);
  });

  return results;
}

module.exports = { sendAll };

表面上看起來很合理。

把所有使用者跑過一次,寄送完成後,把結果放進 results。

但問題在於:

users.forEach(async (user) => {

forEach() 不會等待裡面的 async callback。

因此:

return results;

可能在 sendMail() 真正完成之前就執行。

一開始的測試

建立:

tests/async-await.test.cjs

原本的測試是:

const { sendAll } = require("../samples/async-await.cjs");

test("sendAll 應該在全部完成後回傳結果", async () => {
  const sendMail = async (user) => user;

  const result = await sendAll(["A", "B"], sendMail);

  expect(result).toEqual(["A", "B"]);
});

執行:

npm test

結果卻是:

PASS  tests/async-await.test.cjs

這裡反而出現了一個很有意思的問題:

Bug 存在,但測試沒有抓到 Bug。

原因是 sendMail() 幾乎立即完成,這個測試的執行時序剛好沒有把問題暴露出來。

因此測試本身也需要重新設計。

讓問題真的出現

把測試改成刻意延遲:

const { sendAll } = require("../samples/async-await.cjs");

test("sendAll 應該在全部完成後回傳結果", async () => {
  const sendMail = (user) =>
    new Promise((resolve) => {
      setTimeout(() => resolve(user), 50);
    });

  const result = await sendAll(["A", "B"], sendMail);

  expect(result).toEqual(["A", "B"]);
});

這時候才會真正出現:

FAIL  tests/async-await.test.cjs

也因此得到 Day 22 第一個很重要的觀念:

測試不是為了讓程式 PASS,而是要有能力把 Bug 穩定抓出來。


三、第二個 Bug:SQL 字串直接拼接

第二個案例換成資安問題。

建立:

samples/sql.cjs

內容:

function findUser(db, userName) {
  const sql =
    `SELECT * FROM users WHERE name = '${userName}'`;

  return db.query(sql);
}

module.exports = { findUser };

這段程式碼看起來可以正常產生 SQL:

SELECT * FROM users WHERE name = 'Alice'

但問題在於 userName 是直接拼進 SQL 字串。

建立:

tests/sql.test.cjs

內容:

const { findUser } = require("../samples/sql.cjs");

test("findUser 應該使用參數化查詢", () => {
  const db = {
    query: jest.fn()
  };

  findUser(db, "Alice");

  expect(db.query).toHaveBeenCalledWith(
    "SELECT * FROM users WHERE name = ?",
    ["Alice"]
  );
});

執行測試時,Jest 會指出:

Expected:
"SELECT * FROM users WHERE name = ?", ["Alice"]

Received:
"SELECT * FROM users WHERE name = 'Alice'"

這代表測試成功抓到問題。

程式目前雖然可以組出 SQL,但沒有使用參數化查詢。

這個案例也很適合留給後面的 Gemini 做程式碼審查。


四、第三個 Bug:資料結構少一層就直接爆掉

最後一個案例處理邊界條件。

建立:

samples/boundary.cjs

內容:

function getUserName(user) {
  return user.profile.name.trim();
}

module.exports = { getUserName };

正常資料:

{
  profile: {
    name: "Alice"
  }
}

沒有問題。

但是只要 API 回傳:

{}

這段:

user.profile.name

就會變成:

undefined.name

最後直接拋出:

TypeError: Cannot read properties of undefined

因此測試先定義預期行為:

tests/boundary.test.cjs
const { getUserName } = require("../samples/boundary.cjs");

test("缺少 profile 時應該回傳 Unknown", () => {
  expect(getUserName({})).toBe("Unknown");
});

目前程式沒有處理這個邊界情況,因此測試也會亮紅燈。


五、三組 Bug 正式就位

現在 samples/ 與 tests/ 會形成:

devpulse/
├─ samples/
│  ├─ async-await.cjs
│  ├─ sql.cjs
│  └─ boundary.cjs
│
├─ tests/
│  ├─ async-await.test.cjs
│  ├─ sql.test.cjs
│  └─ boundary.test.cjs
│
├─ package.json
├─ package-lock.json
└─ node_modules/

再執行:

npm test

這次應該得到:

FAIL  tests/sql.test.cjs
FAIL  tests/boundary.test.cjs
FAIL  tests/async-await.test.cjs

Test Suites: 3 failed, 3 total
Tests:       3 failed, 3 total

三個測試全部紅燈。

而且這三個紅燈其實代表三種不同問題:

async-await
    ↓
非同步流程錯誤

SQL
    ↓
資安風險

boundary
    ↓
邊界條件錯誤

這三個案例也正好對應規劃中 Day 22 要準備的「非同步、SQL、邊界」三組微型 Bug。


六、為什麼故意讓測試 Fail?

到了這裡,很容易產生一個錯覺:

「測試不是應該全部通過嗎?」

但 DevPulse 現在的狀態剛好相反。

今天需要的是:

故意有問題的程式
        ↓
穩定重現的測試
        ↓
❌ Fail

因為後面真正要做的是:

Day 22
建立 Bug
   ↓
❌ Fail

Day 23
Gemini 分析
   ↓
找到問題

Day 24
Gemini 產生 Patch
   ↓
修復程式

Day 25
重新執行 Jest
   ↓
✅ Pass

因此現在這三個紅燈,其實就是後面 AI 修復功能的「起跑線」。

沒有紅燈,就沒有辦法證明 AI 修完之後真的變好了。


七、今天先把驗收標準固定下來

DevPulse 最終想完成的並不是單純:

貼上程式碼
     ↓
Gemini 找問題

而是:

Bug 程式碼
    ↓
Gemini 分析
    ↓
產生修復版本
    ↓
執行 Jest
    ↓
Fail → Pass

這也是為什麼 Day 22 沒有急著把 Genkit 接進來。

先把問題固定,後面的 AI 才有明確的工作目標。

下一篇才會正式讓 Genkit 回到 DevPulse:

sample code
     ↓
codeReviewFlow
     ↓
Gemini
     ↓
漏洞說明
     ↓
修改建議

今日學習踩坑小記

今天真正花時間的地方,反而不是建立三個 Bug,而是發現:

一段有問題的程式,不代表測試一定能抓到問題。

最明顯的例子就是 forEach() 搭配 async。

第一次測試竟然 PASS,並不是因為程式沒問題,而是測試沒有製造出足以暴露競態的執行條件。

把 sendMail() 改成具有延遲的 Promise 後,問題才真正被測試抓出來。

這也讓 Day 22 的重點更加清楚:

AI 修 Bug 之前,先建立一個可靠的驗收標準。

這三個紅燈,就是 DevPulse 接下來幾天的起點。


Day 22 完成

今天完成:

✅ 從零建立 devpulse/
✅ 安裝 Jest
✅ 建立 samples/
✅ 建立 tests/
✅ 非同步 Bug
✅ SQL Bug
✅ 邊界條件 Bug
✅ 3 組測試穩定 Fail

下一篇進入 Day 23:撰寫 codeReviewFlow,讓 Genkit 開始替 DevPulse 分析這三個 Bug。


上一篇
Day 21:第三週覆盤:Genkit、Stitch、Antigravity,到底該怎麼一起用?
下一篇
Day 23:撰寫 codeReviewFlow,讓 Gemini 精準揪出三個 Bug
系列文
30 天玩轉 Google AI 全家桶:初學者的隨身 Coding 助理養成記 共 24 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言