遊戲《情書》Walking Skeleton 的開局測試

Ching Yi, Chan
6 min readNov 24, 2022

跟著水球軟體學院遊戲微服務計劃,走到實際開發的階段囉!作為大舉開發的前哨戰,建立一個 Walking Skeleton 是重要的,它恰好可以用來整合先前探索與分析的基礎,構成一個良好的進攻據點,並作為一個可以測試的文件存在。

分析材料

實作 Walking Skeleton 時,需要將完整情境的其中一條打通,讓它是最簡單的實作型式。目標不是「完整」「完美」,而是一個走過全局的感覺,並且儘可能「接觸」到實際規劃好的 Domain Model。不管有沒有 Domain Layer 都可以有 Domain Model 概念的存在,只是它也許不是被好好保護著的一層。

以上面的 Event Storming 結果來說,我們可以由需要曝露在外的 Command 來開始設計,先不管「真實」會用到的、需要補完的 Command 來說,其實只有:

  1. 開始遊戲
  2. 玩家出牌

而其它的 Command 都是在系統內部之間串連起來時自動呼叫的。所以,我們以的 end-to-end 情境來說,可以是這樣:

我們就這樣完成了開始遊戲到結束一局的動作,若是想要補完到遊戲結束,那只是事先給 Player A 更多好感指示物,讓他可以達成結束遊戲的條件罷了。但以走完一局的流程,其實已經涵蓋到多數的 OOA 出現過的內容:

我們將上面描述的情境拆解一下。

Given 就是事先設好的局

在開始打 HTTP Request 前,你 Test Server 啟動後,自動在裡面建立好一個 Game 的狀態:

Given 
已經準備好的遊戲,並且有 Player A 與 Player B 在其中
Player A 持有 衛兵
Player B 持有 神父
新的一輪由 Player A 開始
牌庫剩 1 張牌 (是什麼都無所謂)

When 就是主要的觸發點

When 
開始遊戲、開始回合,Player A 對 Player B 出牌 衛兵 猜測 神父

假設 game id 為 g-5566,我們的開始遊戲 API 用「法想」像如下:

POST /games/g-5566/start
200 OK

只要在系統中查得到 g-5566 這組 game_id 都應該回應 OK。儘管在這時候,我們完全沒有實作取得 Game Status (給玩家用的 Read Model),你心中要有 Given 那邊描述的內容。遊戲啟動後,我們得給玩家行動的 API (設假他們的 Player ID 分別是 player_aplayer_b)。

目前輪到 Player A 行動,而系統已替他抽出一張卡片,它的手上正好有二張卡,但他選擇打出其中一張卡片:

POST /games/g-5566/play
{
"turn_player": "player_a",
"effect_player": "player_b",
"card_action": [
1,
2
]
}

上面的 Player A 對 Player B 出牌的 Request Body 不要想太久,也不需要特別為它討論是否符合各種情況,我們只是要滿足了在 Event Storming 出現的流程,還有 OOA 理出來的概念而已。一旦 Walking Skeleton 與 ATDD 完工後,它是具有測試保護的 end-to-end 使用案例,你就能享用真正的「重構」。到時,你可以安全的重新實作出更好的設計。

(註:沒有測試保護的改寫,只是重寫並不能稱為重構。)

在 Then 時驗收你的成果

當 When 做完動作後,我們在 Then 需要可以驗證成果。有二個簡單的方式,一個是直接去拿 Game 物件出來檢查。但這條路並不太 ATDD,那個 Acceptance 我們得將測試的路徑放在系統使用者 (也就是玩家) 走的路線會好一些。所以,這時你可以提供 Read Model 需要的 API 了,也就是以玩家的角色來說,他會看到什麼呢?

GET /games/g-5566/player/player_a/status

想像得到下列的回應:

{
"game_id": "g-5566",
"who_am_id": "player_a",
"player_queue": [
"player_a",
"player_b"
],
"rounds": [
[
{
"is_winner": true,
"player_id": "player_a",
"hand_card": [
3
],
"used_cards": [
1
]
},
{
"player_id": "player_b",
"hand_card": [
0
],
"used_cards": []
}
],
[
{
"player_id": "player_a",
"hand_card": [
3
],
"used_cards": []
},
{
"player_id": "player_b",
"hand_card": [
0
],
"used_cards": []
}
]
]
}

我們先忽略 rounds,它最開始給的資訊是哪一場遊戲與 要求看 Game Status,還有玩家們目前的順序。而在 rounds 中,是每一局中的狀態 Sanpshot,玩家從遊戲還沒開始,到遊戲還沒結束都可以去看它的資料,我們只是選擇在一局結束的時候看它罷了。

你會發現在這粗略的計劃中有 2 筆資料在 rounds 中,表達了第 1 個 round 結束,而 player_a 是贏家,而第 2 個 round 還沒開始的狀態。為什麼還沒開始呢?因為 used_cards 是空的,還沒有人出過牌。

我們大概會在實作中去先弄個大概的樣子,接著會發現是不是有更直覺得表達方式而不斷修改它 (同樣提醒你,還是以先衝完 Walking Skeleton 為主,不用太計較設計完不完整)。

摘要一下成果

在上面「各式想像」結果的用途中,我們會看到 Read Model 成形了,也知道至少有 3 個 API 可以先刻出來再說,完成第 1 個 end-to-end 測試後,後續只是充實著各種細節,與補完真實遊玩需要的 API。

舉例來說,先以系統與外部互動的層面來看你還得有:

  • 建立遊戲
  • 玩家加入遊戲

試著將 Given 階段,直接操作 Domain Model 的部分都改成外部實際的 API 處理。而系統內部的細節,除了 BDD 能測的部分,你可以接著優化 Game Status 與分數計劃的各種細節。

註:當大部分的概念與實作的對應都出來後,再來弄成本比較大的 I/O 會相對不容易重工。

以下為製作過程中記錄的播放清單

原始碼連結:

--

--