Whisper Isle 遊戲連結(PC Only):whisperisle.app(註:線上已推進至 v0.2.2)
很多前端或後端測試喜歡用 Mock 把外部依賴包得嚴嚴實實:把 WebSocket mock 掉、把房間廣播 mock 掉、把計時器 mock 掉。這種測試跑起來飛快,幾毫秒內全部綠燈,但往往測試全過,實際上線一跑就掛。
這邊 acceptance 測試則採用「無頭客戶端(Headless Clients)」:
直接引用 @colyseus/sdk,在 Node.js 環境中向本地運行的遊戲伺服器發起真實的 WebSocket 握手。發送真實的 Client 意圖(例如 move、attack、chat)。掛載伺服器的 Schema 狀態監聽器,等待真實的 State Patch 推送過來,並對狀態數值做斷言。以下是開案的測試(acceptance-core.mjs)的縮影:
// client/tests/acceptance/acceptance-core.mjs (片段)
import { Client } from "@colyseus/sdk";
import assert from "node:assert/strict";
const client = new Client("ws://localhost:2567");
const room = await client.joinOrCreate("world", {
nickname: "core-tester",
classKey: "knight",
});
// 1. 驗證連線與出生點指派
assert.ok(room.sessionId, "必須取得有效 sessionId");
const me = room.state.players.get(room.sessionId);
assert.ok(me, "玩家狀態必須同步進 Room State");
assert.equal(typeof me.x, "number", "必須具備世界 X 座標");
// 2. 發送真實位移意圖並等待狀態廣播
room.send("move", { targetX: me.x + 32, targetY: me.y });
await waitForState(room, () => room.state.players.get(room.sessionId).isMoving === true);
// 3. 退場
await room.leave();
當一個新功能完成時,只要本地 dev server 在跑,AI 敲下一行指令,整套測試就會化身為幾名玩家進房間、移動、發話、承受怪物傷害。
當專案後來引入了 PostgreSQL 帳號與角色持久化之後,一個新的隱患浮出水面:跑測試產生的海量垃圾帳號,會不會污染生產資料庫與營運數據?
我們在規範中立下了,所有自動化測試與線上驗機帳號,信箱一律強制以 @whisper.invalid 結尾。伺服器在註冊路由中檢測到此後綴時,會將其權限自動打上 role = 'test'。日後所有營運指標、留存分析與 GA 漏斗,只要加一道 WHERE role = 'player',就能天然將測試雜訊剔除乾淨。
「為了讓玩家體驗到穿裝備的成就感,新創角的騎士不再直接穿著新手劍與盾,而是將鐵劍與潮木圓盾放進背包,由玩家手動穿戴。」
這純粹是一個背包與裝備展示的改動,根本沒動到核心戰鬥代碼。然而,當我跑測試時,整個測試套件突然徹底卡死:
acceptance-skill(技能學習測試):逾時掛掉。
acceptance-economy-transactions(經濟交易測試):逾時掛掉。
acceptance-stale-connection(斷線重連測試):逾時掛掉。
三個完全不相干的系統,連線測試無聲卡死,終端機卡在那裡動也不動,40 秒後噴出崩潰訊息。
根因剖析:測試裡的真實打怪耦合
在寫 acceptance-skill 時,該測試的目標是驗證「玩家達到 Lv.2 時,NPC 會解鎖二級技能『疾風突刺』供學習」。AI 在寫這支測試時,為了「端對端真實性」,竟然寫了一個 helper 叫 killOne(room):它讓測試帳號走到野外,在世界裡尋找怪獸,並一刀一刀把怪砍死,直到經驗值升到 Lv.2!
在舊版本中,新角色預設手拿鐵劍、身背圓盾,打村口門外的怪幾刀就砍完了。但在改動後,新角色出生是空手的!
空手的騎士攻擊力只有可憐的 9 點。而村口最近的怪物是「山豬」(HP 84,攻擊力 14)。一級空手角色衝上去跟山豬肉搏,下場只有一個:角色在 6 秒內被山豬無情拱死。
角色死後回到重生點,伺服器永遠不會發送 monsterKilled 封包;killOne() 在等待 40 秒逾時後,內部邏輯竟然寫了「自動遞迴重試」——於是測試帳號不斷重生、衝出去送死、重試、卡死,直到整個 runner 徹底超時崩潰。
這場事故暴露了自動化測試中最經典的反模式:
你把「前置狀態(練級、賺金幣)」與「被測系統(技能解鎖、交易流程)」深度耦合在了一起。
你要測的是「技能學習的狀態機與封包防作弊」,而不是「空手打怪的數值平衡」。當你把被測系統建立在一段漫長且脆弱的遊戲前置流程上時,任何企劃數值的微調,都會產生幾十倍的誤傷半徑。
隨著專案持續擴大,到了今天,client/tests/acceptance/ 已經累積了超過 90 支驗收腳本,涵蓋了從酒館傳送、天氣變化、迷霧照明、仇恨追擊到背包交易的每一個角落。
但這時候,新的問題又來了:跑完整套 90 支測試,在本地需要花上整整 8 到 10 分鐘。
如果你要求 AI「每次動到 server 邏輯,commit 前都必須跑完全套」,這段長達 10 分鐘的等待時間會把人機協作的流暢感撕裂。AI 坐在那裡乾等,Token 視窗卡住,人類的專注力也被打散。因此在 2026-08-16,我們做出了務實的修訂:
「Commit 前只跑『本輪動到的系統』對應的那支(或那幾支)acceptance 全綠;全套跑完僅在 user 明確下令或重大版本發布前執行。」
這是一個在安全性與反饋速度之間的平衡: