Day 19:TDD 的 AAA 與 BDD 的 GWT,小步刻出演算法

Share Dialog

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

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

今日兩個核心理解

  • 實作時可以先用白話三階段描述 GWT / AAA 內容,再轉成程式碼,讓「需求故事」和「測試程式」保持同樣的結構。

  • 小步 TDD 實作可以透過紅–綠–重構的小循環,逐步完成一開始沒有明確完整輪廓的演算法或資料結構,例如先前實作書中一開始以 stack 命名、最後完成為 circular buffer 的示範,我用相同思路寫了 JavaScript 版本的 IntQueue

練習題程式碼 GitHub repo: 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:檢查 sizeisEmpty(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 內部狀態抽成 bufferheadIndextailIndexsize,命名更語意化。

4

重複

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

  • 同一個情境如果自然包含多個步驟,可以在一個 it 裡寫多段 Act + Assert,但要讓每個步驟的意圖清楚。

  • 驗證錯誤時,常見的寫法是用 expect(() => someAct()).toThrowError(...),把 Act 與 Assert 合併,讓測試更精簡易讀。

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


參考文件連結