# Day 19：TDD 的 AAA 與 BDD 的 GWT，小步刻出演算法

By [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake) · 2025-12-16

---

今天身體狀態欠佳，輕量閱讀《Clean Craftsmanship 無暇的程式碼 軟體工匠篇》文字敘述的概念並回顧先前練習實作的程式碼。影片示範和程式碼實作都先跳過。

目前跳過的示範題（包含：質因數分解、整數大小排序、保齡球計分程式、自動換行問題）之後再補，Uncle Bob 書中都是用 Java 寫的，可以參考相同思路寫成 JavaScript 版本。

今日兩個核心理解
--------

*   實作時可以先用白話三階段描述 GWT / AAA 內容，再轉成程式碼，讓「需求故事」和「測試程式」保持同樣的結構。
    
*   小步 TDD 實作可以透過紅–綠–重構的小循環，逐步完成一開始沒有明確完整輪廓的演算法或資料結構，例如先前實作書中一開始以 stack 命名、最後完成為 circular buffer 的示範，我用相同思路寫了 JavaScript 版本的 `IntQueue`。
    

練習題程式碼 GitHub repo: [https://github.com/gcake119/clean-craftsmanship-katas-js](https://github.com/gcake119/clean-craftsmanship-katas-js)

* * *

GWT / AAA / IntQueue 對照表（概念）
----------------------------

面向

BDD：GWT 敘述

TDD：AAA 結構

在 `IntQueue` 裡的具體樣子

前置條件

Given：系統目前在哪個狀態？有什麼資料？

Arrange：建立物件、準備輸入與狀態。

`const queue = createIntQueue(3)`、先 enqueue 幾個值、讓 queue 變滿或變空

行為

When：發生了什麼動作或事件？

Act：實際呼叫被測試的函式或觸發行為。

`enqueue(queue, 42)`、`dequeue(queue)`、嘗試在滿隊列上再 enqueue 一次

結果

Then：外部可以觀察到什麼結果或狀態變化？

Assert：用斷言檢查回傳值、狀態是否符合預期。

`expect(queue.size).toBe(0)`、`expect(peek(queue)).toBe(42)`、`expect(() => enqueue(...)).toThrowError('queue is full')`

* * *

以 `IntQueue` 測試為例的 GWT ↔ AAA
----------------------------

### 1\. 建立空的 queue

角度

內容

GWT 白話

Given：給定一個容量為 3 的整數 queue。When：當 queue 剛被建立。Then：它是空的（size 為 0，`isEmpty` 為 true，`isFull` 為 false）。

AAA + 程式

Arrange：`const queue = createIntQueue(3)`。Act：無額外 Act，建立本身就是行為。Assert：檢查 `size`、`isEmpty(queue)`、`isFull(queue)` 是否符合預期。

對應到測試程式：

    it('create an empty queue', () => {
      const queue = createIntQueue(3)
    
      expect(queue.size).toBe(0)
      expect(isEmpty(queue)).toBe(true)
      expect(isFull(queue)).toBe(false)
    })
    
    

* * *

### 2\. enqueue / dequeue 一個值

角度

內容

GWT 白話

Given：給定一個容量為 3 的空 queue。When：當我 enqueue 42 並再 dequeue 一次。Then：我先能用 `peek` 看到 42，`dequeue` 也會回傳 42，最後 queue 回到空的狀態。

AAA + 程式

Arrange：`const queue = createIntQueue(3)`。Act：先 `enqueue(queue, 42)`，再 `const value = dequeue(queue)`。Assert：中間先驗證 size / `isEmpty` / `peek`；最後驗證 `value` 與最終 size / `isEmpty`。

> 這裡測試程式在同一個 `it` 裡包含了兩段 Act + Assert（先 enqueue、再 dequeue），可以視為「同一個情境裡有多個步驟」。如果想更貼近「一個行為一個測試」的風格，也可以拆成兩個 `it`（一個只驗證 enqueue 後狀態，一個只驗證 dequeue 行為）。

* * *

### 3\. 滿載後再 enqueue 要丟錯

角度

內容

GWT 白話

Given：給定一個容量為 3 的 queue，已經塞滿 3 個值。When：當我再 enqueue 一個值。Then：應該拋出 `queue is full` 的錯誤。

AAA + 程式

Arrange：建立 queue 並連續 enqueue 3 次。Act：嘗試再 enqueue 一次。Assert：用 `expect(() => enqueue(...)).toThrowError('queue is full')` 檢查有對的錯誤。

> 這個情境裡，`expect(() => enqueue(...)).toThrowError('queue is full')` 同時完成 Act 與 Assert。Act 是「在已滿的 queue 上呼叫 `enqueue`」，Assert 是「應該丟出指定錯誤」。對於驗證錯誤的測試，這種把 Act / Assert 合在一行的寫法是很常見、也相對清晰的慣用模式。

程式片段：

    it('throws error when enqueue to a full queue', () => {
      const queue = createIntQueue(3)
      enqueue(queue, 1)
      enqueue(queue, 2)
      enqueue(queue, 3)
    
      expect(() => enqueue(queue, 4)).toThrowError('queue is full')
    })
    

* * *

### 4\. 繞圈後仍維持 FIFO

角度

內容

GWT 白話

Given：給定一個容量為 3 的 queue，先 enqueue 1 和 2。When：我 dequeue 一次，再依序 enqueue 3 和 4，讓 head / tail 繞圈。Then：後面連續 dequeue 時，應該依序拿到 2、3、4，最後 queue 為空。

AAA + 程式

Arrange：`const queue = createIntQueue(3)`。Act：一連串 enqueue / dequeue 操作讓索引繞圈。Assert：每次 `dequeue` 的回傳值順序正確，最後 `isEmpty(queue)` 為 true。

對應到測試程式：

    it('在多次不規則的新增與取出後，索引繞圈時仍維持先進先出的正確順序', () => {
      const queue = createIntQueue(3)
    
      // 初次填入兩個
      enqueue(queue, 1)
      enqueue(queue, 2)
    
      expect(dequeue(queue)).toBe(1)
      expect(peek(queue)).toBe(2)
    
      // 填滿剩下的空間，讓 tail 繞回 index 0
      enqueue(queue, 3)
      enqueue(queue, 4)
    
      // 依序取出，確認順序仍然正確
      expect(dequeue(queue)).toBe(2)
      expect(dequeue(queue)).toBe(3)
      expect(dequeue(queue)).toBe(4)
    
      expect(isEmpty(queue)).toBe(true)
    })
    
    

* * *

小步 TDD：用 GWT 幫忙刻出 IntQueue
--------------------------

步驟

TDD 小循環

在這個練習中的樣子

1

Red：先寫一個描述行為的測試（通常可以先用 GWT 白話寫在註解，再轉成 AAA 測試）。

先寫「建立空 queue」或「enqueue / dequeue 一個值」的測試，還沒有完成 `enqueue` / `dequeue` 的實作。

2

Green：只寫剛好讓測試通過的最小實作，不管還沒處理到的情境。

先讓基本 enqueue / dequeue 動得起來，可能還沒處理滿載、繞圈等邊界。

3

Refactor：在測試都綠的情況下整理設計與命名，確保程式碼清晰可讀。

把 queue 內部狀態抽成 `buffer`、`headIndex`、`tailIndex`、`size`，命名更語意化。

4

重複

每次新增一個新的 GWT 情境，轉成新的 AAA 測試，再跑一輪紅–綠–重構。

*   同一個情境如果自然包含多個步驟，可以在一個 `it` 裡寫多段 Act + Assert，但要讓每個步驟的意圖清楚。
    
*   驗證錯誤時，常見的寫法是用 `expect(() => someAct()).toThrowError(...)`，把 Act 與 Assert 合併，讓測試更精簡易讀。
    

這一套流程可以直接複製到之後的 kata（質因數分解、排序、保齡球計分、自動換行）：  
先用 GWT 說清楚行為，再用 AAA 寫測試，小步紅–綠–重構，把演算法一步一步刻出來。

* * *

參考文件連結
------

*   《Clean Craftsmanship 無暇的程式碼 軟體工匠篇》紙本書
    
*   [The Arrange, Act, and Assert (AAA) Pattern in Unit Test Automation](https://semaphoreci.com/blog/aaa-pattern-test-automation)
    
*   [Given-When-Then - Martin Fowler](https://martinfowler.com/bliki/GivenWhenThen.html)
    
*   [Using “Given-When-Then” to Discover and Validate Requirements](https://ebgconsulting.com/blog/using-given-when-then-to-discover-and-validate-requirements-2/)
    
*   [Agile Alliance: What is "Given - When - Then"?](https://agilealliance.org/glossary/given-when-then/)
    
*   [Given‑When‑Then Acceptance Criteria: Guide](https://www.parallelhq.com/blog/given-when-then-acceptance-criteria)
    
*   [Unit test basics - Microsoft Learn](https://learn.microsoft.com/en-us/visualstudio/test/unit-test-basics)
    
*   [expect API - Vitest](https://vitest.dev/api/expect.html)
    
*   [Test-Driven Development: Steps & Examples](https://engini.io/blog/test-driven-development-tdd/)
    
*   [How to Implement Test-Driven Development (TDD)](https://www.testrail.com/blog/test-driven-development/)
    
*   [5 steps of test-driven development](https://developer.ibm.com/articles/5-steps-of-test-driven-development/)
    
*   [What Is Test-Driven Development? Benefits and Approaches](https://www.legitsecurity.com/aspm-knowledge-base/test-driven-development)

---

*Originally published on [雞蛋糕的前端修煉屋](https://paragraph.com/@gcake/day-19)*
