在 Day 8,我們成功搭建了 TDD Kata 練習的專案,一個結構清晰的 go-tdd-kata 專案。我們將開始 TDD 的黃金循環: 「紅燈 -> 綠燈 -> 重構」 ,而這一切的起點,就是點亮「紅燈」。
今天的目標:為我們的第一個 Kata「FizzBuzz」,寫下一個最小化的、注定會失敗的測試案例。
這一步是 TDD 中最考驗紀律性的一步。我們需要抑制住立即開始寫 production code 的衝動,而是先專注於「定義我們下一步的目標」。
FizzBuzz 是一個經典的程式設計題,規則簡單,卻足以用來檢視程式設計師的基礎能力。它的規則如下:
編寫一個函式,接收一個整數 n
,並根據以下規則回傳一個字串:
面對以上這四條規則,我們應該先從哪一條開始?
一個新手可能會想從最複雜的 "FizzBuzz" 開始,但 TDD 會告訴你:
永遠從最簡單、最平凡的案例開始。
在 FizzBuzz 問題中,最簡單的案例是什麼? 是規則4:當數字不是 3 或 5 的倍數時,直接回傳數字本身,而在這個案例中,最簡單的輸入又是什麼?是 1。
所以,我們的第一個測試目標就確定了: 「當輸入為 1 時,函式應回傳字串 '1'」
TDD 的精髓:將一個大問題,分解成一個個微小、可驗證的步驟。
現在,打開我們在 Day 8 建立的專案,找到 fizzbuzz/fizzbuzz_test.go
檔案,我們將使用在 Day 6 學到的「表格驅動測試」模式來組織我們的測試。
將以下程式碼完整地(自己手寫更有記憶) 貼到 fizzbuzz/fizzbuzz_test.go 中:
// fizzbuzz/fizzbuzz_test.go
package fizzbuzz
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFizzBuzzGenerator(t *testing.T) {
// 我們使用表格驅動測試的結構
testCases := []struct {
name string
input int
expected string
}{
// 這是我們定義的第一個,也是目前唯一的測試案例
{
name: "should return '1' for number 1",
input: 1,
expected: "1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 呼叫我們「即將」實現的產品程式碼
result := Generate(tc.input)
// 斷言結果是否符合預期
assert.Equal(t, tc.expected, result)
})
}
}
testify/assert
來幫助我們寫出優雅的斷言。在 t.Run 中
,我們呼叫了 Generate()
函式(它目前還在 fizzbuzz.go 中,回傳的是一個空字串),我們 assert Generate(1)
的結果應該要等於 "1"。
一切就緒,回到terminal,確保你在專案的根目錄 (go-tdd-kata),然後執行我們熟悉的指令:
go test -v ./...
如果一切順利,你將會看到期待已久的「紅色」失敗訊息!
=== RUN TestFizzBuzzGenerator
=== RUN TestFizzBuzzGenerator/should_return_1_for_number_1
fizzbuzz_test.go:27:
Error Trace: go-tdd-kata/fizzbuzz/fizzbuzz_test.go:27
Error: Not equal:
expected: "1"
actual : ""
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-1
+
Test: TestFizzBuzzGenerator/should_return_1_for_number_1
--- FAIL: TestFizzBuzzGenerator (0.00s)
--- FAIL: TestFizzBuzzGenerator/should_return_1_for_number_1 (0.00s)
FAIL
FAIL go-tdd-kata/fizzbuzz 2.301s
FAIL
這份報告非常清晰地告訴我們:
should return '1' for number 1
的子測試中發生了錯誤。"1"
,但實際 (actual) 得到的是 ""
(空字串)。
為什麼會這樣? 因為我們的產品程式碼 fizzbuzz.go
中的 Generate
函式現在長這樣:
func Generate(number int) string {
return "" // 它總是回傳空字串
}
測試結果完美地反映了程式碼的現狀,第一次勝利:這盞紅燈是我們的目標。對於 TDD 開發者來說,看到這個紅燈並不是挫敗,而是第一次的勝利,我們成功地:
今天我們嚴格遵守了 TDD 的紀律,邁出了最關鍵的第一步,我們分析了需求,選擇了最簡單的案例,並為它編寫了一個會失敗的測試,成功點亮了 TDD 循環中的「紅燈」。
預告:Day 10 - Kata 演練:FizzBuzz (二) - 最簡單的實作與重構 (綠燈 -> 重構)
既然目標已經明確,明天我們的任務就變得非常簡單:寫下「最少量」的程式碼,不多不少,剛好讓今天這盞紅燈變綠。我們將體驗到 TDD 循環中,從「紅」到「綠」的爽感,並探討在這個微小循環中,「重構」的意義。