このコードラボでは、テスト駆動開発(TDD)という開発手法を使ってFizzBuzzを実装します(FizzBuzzをご存じない方はリンク先でご確認ください)。

テスト駆動開発(TDD)によるソフトウェア開発の流れを学びつつ、Goのテストコードの書き方を学ぶことができます!

このコードラボでは1からソースを作成していきますが、事前に雛形・ディレクトリ構成が用意された以下のサンプルコードをgit cloneまたは、ZIPでダウンロード・解凍しておくとスムーズに開始できます。

$ git clone https://github.com/DeNA/codelabs.git

このコードラボのサンプルコードはsources/tdd-go以下に格納されています。

$ cd codelabs/sources/tdd-go
$ ls
1_fizzbuzz_start 2_fizzbuzz_answer

ディレクトリ構造は以下のようになっており、1_fizzbuzz_startがこのコードラボを始める際の雛形になっています。2_fizzbuzz_answerはこのコードラボを終えたときの最終形のコードが格納されています。

$ tree .
.
├── 1_fizzbuzz_start
│   └── gopath
│       └── src
│           └── fizzbuzz
│               ├── fizzbuzz.go
│               └── fizzbuzz_test.go
└── 2_fizzbuzz_answer
    └── gopath
        └── src
            └── fizzbuzz
                ├── fizzbuzz.go
                └── fizzbuzz_test.go

最後にGOPATHを設定し、このコードラボを開始できる状態にしましょう。

$ cd 1_fizzbuzz_start/gopath
$ export GOPATH=`pwd`

テスト駆動開発(TDD:Test Driven Development)とは、最初にテストを書き、そのテストがパスする最低限の実装をし、コードを洗練させる(リファクタリング)、というサイクルを繰り返しながらソフトウェアを開発する手法です。

Red / Green / Refactor

このサイクルは「Red」「Green」「Refactor」という単語で語られます。

Red

失敗するテストを書く

Green

テストがパスするコードを書く

Refactor

コードを洗練させる(リファクタリング)

TDDではこの Red → Green → Refactor というサイクルをテンポよく繰り返しながら進めていきます。

メリット

TDDのメリットとして以下のようなものが挙げられます。

説明はこれくらいにして、実際に手を動かしながらTDDを体験してみましょう!

最初にFizzBuzzで実装すべきことを洗い出しましょう。

このように最初に『やること』をリストアップするのはTDDでよく行われます。そして、一度にすべてを行おうとせず一つずつ消化をしていきます。

TDDは短時間で Red → Green → Refactor のサイクルを繰り返すため、最終的に何をやるのか忘れてしまいがちです。それを防ぐためにも最初にやることをリストアップするのは良い習慣と言えるでしょう。

このコードラボでは上から順番に実装を進めていきます。

まずはテストコードの雛形としてfizzbuzz_test.goを作成します。

package fizzbuzz_test

import (
        "testing"
)

func TestFizzBuzz(t *testing.T) {
}

ファイル名のルール

Go言語ではファイル名の末尾に_testをつけたものがテストコードとして認識されます。ここではテスト対象のファイル名をfizzbuzz.goにするつもりでfizzbuzz_test.goというファイル名にしました。

パッケージ名も同様に末尾に_testをつけています。Go言語では、他のパッケージから利用される想定の関数に対するテストでは、このようにすることで別パッケージとして扱うことができます。

testingパッケージ

Go言語では標準パッケージにtestingというテスト用のパッケージが含まれています。

TestFizzBuzzという関数を作成しましたが、これがテスト用の関数になります。Go言語では次のルールにあてはまるものがテスト用の関数として認識されます。

まだ関数の中身はありませんが一度テストを実行してみましょう。fizzbuzzパッケージのディレクトリに移動し、ターミナルからgo testコマンドでテストを実行できます。

$ cd src/fizzbuzz 
$ go test -v
=== RUN   TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASS

うまく実行できれば最後にPASSという単語が出力されます。-vオプションは必須ではありませんが、これを付けることでどのテストが実行されたか分かるようになります。

これでテストを書いて実行できる環境が整いました。次のステップから実際にTDDで実装を進めていきます!

それではTDDの最初のステップである『失敗するテストを書く』ところから始めましょう。

ここでのポイントは自分が欲しいインターフェースを考えることです。今回はintを渡してstringを返すインターフェースが欲しいと考え、関数名はシンプルにFizzbuzzとしてみました。

// fizzbuzz_test.go

package fizzbuzz_test

import (
        "testing"
        "fizzbuzz" // インポートを追加
)

func TestFizzBuzz(t *testing.T) {
        got := fizzbuzz.FizzBuzz(1)
        if got != "1" {
                t.Errorf(`FizzBuzz(1) is %q`, got)
        }
}

ここでは関数の呼び出し結果を変数gotに格納し、それが期待値である"1"と一致しなかった場合にテストを失敗させるようにしています。

このようにGo言語ではユニットテストにおいて幅広く利用されているxUnitフレームワークとは異なりアサーション関数は用意されていません。そのかわりに期待しない結果が得られた場合に、テストが失敗したことを記録する関数(ここではtestingパッケージのT.Errorf関数)を呼びます。

さてFizzBuzzという関数は作成していないので現状ではコンパイルは通りません。テストを実行してそれを確認してみましょう。

$ go test -v
./fizzbuzz_test.go:8:7: undefined: fizzbuzz

まずはこれを修正することをだけを考えて、プロダクトコードを実装していきます。fizzbuzz.goというファイル名でソースを作成し、FizzBuzz関数の雛形を実装します。

// fizzbuzz.go

func FizzBuzz(n int) string {
        return ""
}

中身の実装は空文字列を返すだけのダミー実装ですが問題ありません。今はコンパイルを通すことが目的だからです。

この状態でテストを実行すると次のようにテストが失敗することが確認できます。

$ go test -v
=== RUN   TestFizzBuzz
--- FAIL: TestFizzBuzz (0.00s)
    fizzbuzz_test.go:11: FizzBuzz(1) is "" # Errorf関数による出力
FAIL
exit status 1
FAIL    github.dena.jp/swet/go-codelabs/tdd/fizzbuzz    0.018s

FizzBuzz(1) is ""という出力がされていますが、これはT.Errorf関数に渡した引数によるものです。このように『テストが失敗したときの正確な状況』を把握できるようにするため適切な値を設定することが大切です。

これで最初の『失敗するテストを書く(Red)』が出来ました!

次のステップは『テストがパスする最小限のコードを書く』です。

今回のテストをパスさせる最小限の実装は何でしょうか。それは文字列"1"を返すことです。

// fizzbuzz.go

func FizzBuzz(n int) string {
        return "1" // "1"を返すように修正
}

この状態でテストを実行すると、確かにテストがパスすることがわかります。

$ go test -v
=== RUN   TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASS

このようにテストがパスするように値をハードコーディングすることをTDDではFake It(仮実装)と呼びます。TDDではこのように確実なことを少しずつ進めることで、自信をたもちながら開発していく方法がよく使われます。

これで最初の『テストがパスする最小限のコードを書く(Green)』が出来ました!

最後のステップは『コードをきれいにする』です。

コードをきれいにする行為は一般的にリファクタリングという単語で知られています。このコードラボでも以降はリファクタリングという単語を使用します。

リファクタリング

さて現状でリファクタリングすべき箇所はあるでしょうか?

テストコードをあらためて見てみます。

// fizzbuzz_test.go

func TestFizzBuzz(t *testing.T) {
        got := fizzbuzz.FizzBuzz(1)
        if got != "1" {
                t.Errorf(`FizzBuzz(1) is %q`, got)
        }
}

fizzbuzzパッケージのFizzBuzz関数を呼ぶというインターフェースになっていますが、単語が重複していて冗長な印象を受けます。ここではfizzbuzz.Convert(1)と呼べるほうが直感的であると感じたのでリファクタリングしてみます。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {
        got := fizzbuzz.Convert(1)
        if got != "1" {
                t.Errorf(`Convert(1) is %q`, got)
        }
}

関数名やエラーメッセージをConvertに変更し、それにあわせてテスト用の関数も名前を変更しました。

この状態でテストを実行すると「fizzbuzz.Convertが定義されていない」というコンパイルエラーとなり、期待通りにテストが失敗していることが確認できます。

./fizzbuzz_test.go:9:9: undefined: fizzbuzz.Convert

コンパイルエラーを解消するために、プロダクトコードの方も変更します。

// fizzbuzz.go

func Convert(n int) string {
        return "1"
}

テストを実行する

コード変更が終わったらテストを実行し、コードを壊していないことを確認しましょう。

$ go test -v
=== RUN   TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASS

問題なさそうですね!

このようにテストコードが用意されていることで、自信を持ってリファクタリングできます。

おめでとうございます!

これでTDDのサイクルである Red / Green / Refactor を体験できました!

ここまでで数字の1を渡したときに文字列"1"が返されるところまで進められました。しかし、他の数字、例えば2を渡した時にFizzBuzzの仕様どおりに動くのでしょうか?

ためしにテストコードを追加してみましょう。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {
        got := fizzbuzz.Convert(1)
        if got != "1" {
                t.Errorf(`Convert(1) is %q`, got)
        }

        // このテストを追加
        got = fizzbuzz.Convert(2)
        if got != "2" {
                t.Errorf(`Convert(2) is %q`, got)
        }
}

これでテストを実行してみると失敗することがわかります。

$ go test -v
=== RUN   TestConvert
--- FAIL: TestConvert (0.00s)
    fizzbuzz_test.go:16: Convert(2) is "1"

現状では"1"を固定で返却していたので、テストがパスするように修正してみます。strconv.Itoa関数を利用することで数値から文字列への変換が行えるのでそれを利用します。

// fizzbuzz.go

import (
        "strconv"
)

func Convert(n int) string {
        return strconv.Itoa(n)
}

テストが成功することを確認しましょう。

$ go test -v
=== RUN   TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASS

このようにあとから別のデータを追加し、それらのテストがパスするように本来あるべきコードに修正する方法を三角測量と呼びます。

これで最初のTODOが消化できました!

残りのTODOも順番に消化していきましょう。

3で割り切れるときは"Fizz"を返す

3で割り切れるときは"Fizz"という文字列を返す実装を進めていきましょう。

Red

まずはテストコードを追加します。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {
        got := fizzbuzz.Convert(1)
        if got != "1" {
                t.Errorf(`Convert(1) is %q`, got)
        }

        got = fizzbuzz.Convert(2)
        if got != "2" {
                t.Errorf(`Convert(2) is %q`, got)
        }

        // このテストを追加
        got = fizzbuzz.Convert(3)
        if got != "Fizz" {
                t.Errorf(`Convert(3) is %q`, got)
        }
}

この状態でテストを実行すると失敗するはずです。確認してみましょう。

$ go test -v
=== RUN   TestConvert
--- FAIL: TestConvert (0.00s)
    fizzbuzz_test.go:16: Convert(3) is "3"

このようにあえて失敗させるのは自分が書いたテストコードが正しいことを確認するという意味があります。

TDDによる実装を始める前にテストの雛形だけ用意してテストを実行した時、テストが成功したのを覚えているでしょうか?これはテストが成功したとしても、正しくテストできているという保証はないことを意味します。

テストがパスした時に「ちゃんとテスト出来ているのだろうか?」という不安に陥らないように、テストが期待どおり失敗することを確認するのです。

Green

テストがパスするように実装していきます。

// fizzbuzz.go

func Convert(n int) string {
        if n%3 == 0 {
                return "Fizz"
        }
        return strconv.Itoa(n)
}

前回は仮実装(Fake It)を利用して固定値を返すようにしましたが、ここでは自信があったので最初から本来のロジックを実装しました。このように正解が明らかで不安がないと自分が感じる場合に最初から実装することを明白な実装と呼びます。

その自信のとおりに正しく実装できているのでしょうか、テストを実行して確認してみましょう。

$ go test -v
=== RUN   TestConvert
--- PASS: TestConvert (0.00s)
PASS

問題ないことを確認できたら次に進みましょう。

Refactor

この段階でリファクタリングすべき箇所はあるでしょうか?

テストコードに重複があるのでリファクタリング候補ではありますが、現状では十分にシンプルにも感じます。今回はこのままにして次のステップに進むことにします。

これで2つ目のTODOが消化できました。

5で割り切れるときは"Buzz"を返す

次は5で割り切れるときは"Buzz"という文字列を返す実装です。

Red

これまで同様に失敗するテストコードを追加します。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {
        got := fizzbuzz.Convert(1)
        if got != "1" {
                t.Errorf(`Convert(1) is %q`, got)
        }

        got = fizzbuzz.Convert(2)
        if got != "2" {
                t.Errorf(`Convert(2) is %q`, got)
        }

        got = fizzbuzz.Convert(3)
        if got != "Fizz" {
                t.Errorf(`Convert(3) is %q`, got)
        }

        // このテストを追加
        got = fizzbuzz.Convert(5)
        if got != "Buzz" {
                t.Errorf(`Convert(5) is %q`, got)
        }
}

テストが失敗することを確認できたら進みましょう。

Green

テストがパスするコードを実装していきます。

今回も自信があるので「明白な実装」をしました。

// fizzbuzz.go

func Convert(n int) string {

        // これを追加
        if n%5 == 0 {
                return "Buzz"
        }

        if n%3 == 0 {
                return "Fizz"
        }
        return strconv.Itoa(n)
}

テストがパスすることが確認できたら進みましょう。

Refactor

さきほどリファクタリングを見送ったテストコードの重複ですが、このままテストパターンが増えていくことを考えると冗長だと感じます。このタイミングでリファクタリングしてみましょう。

Go言語ではテーブル駆動テストという手法が多く利用されます。事前にテストケース(入力データ、期待値)を用意しておき、それを順番に繰り返しテストするという考え方です。

現状のテストコードを次のように書き換えてみましょう。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {

        tests := []struct {
                n    int    // 入力値
                want string // 期待値
        } {
                { n: 1, want: "1" },
                { n: 2, want: "2" },
                { n: 3, want: "Fizz" },
                { n: 5, want: "Buzz" },
        }

        for _, tt := range tests {
                got := fizzbuzz.Convert(tt.n)
                if got != tt.want {
                        t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
                }
        }
}

テストとして行っていることは変わりませんが、事前にテストパターンが列挙される形式になり見通しがよくなりました。

ここでもテストを実行して、リファクタリングによって何かを壊していないか確認してみましょう。

$ go test -v
=== RUN   TestConvert
--- PASS: TestConvert (0.00s)
PASS
...

テストがパスしたので一安心ですが、そもそも全パターンのテストが実行されているのかという不安はないでしょうか。例えば、ループ処理がうまく書けていなかった場合は十分ありえる話です。

その不安を取り除くために、テストデータをあえて失敗するはずの値に変更してテストを実行してみます。期待値であるwantの先頭にxを追加してテストを実行してみます。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {

        tests := []struct {
                n    int
                want string
        } {
                { n: 1, want: "x1" }, // 先頭にxを追加
                { n: 2, want: "x2" },
                { n: 3, want: "xFizz" },
                { n: 5, want: "xBuzz" },
        }

        for _, tt := range tests {
                got := fizzbuzz.Convert(tt.n)
                if got != tt.want {
                        t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
                }
        }
}

すべてのテストケースが実行されていれば、そのようにレポートされるはずです。

$ go test -v
=== RUN   TestConvert
--- FAIL: TestConvert (0.00s)
    fizzbuzz_test.go:22: Convert(1) = "1" but want "x1"
    fizzbuzz_test.go:22: Convert(2) = "2" but want "x2"
    fizzbuzz_test.go:22: Convert(3) = "Fizz" but want "xFizz"
    fizzbuzz_test.go:22: Convert(5) = "Buzz" but want "xBuzz"
FAIL

期待どおり4つのテストすべてが失敗しています!

このように不安な部分があればひとつずつ確認して進むというのもTDDでは大切とされています。今回あえてテストを失敗させましたがこれはTDDサイクルにおけるRedではなく、不安を取り除くために自発的におこなったものです。

これで不安を取り除けたので、テストデータをもとに戻して最後の実装に進みます。

3と5の両方で割り切れる場合に"FizzBuzz"を返す

最後に35の両方で割り切れるときは"FizzBuzz"という文字列を返す実装です。

Red

いつもどおり失敗するテストの追加から始めます。

さきほどのリファクタリングによりテストの追加はとても簡単です。

// fizzbuzz_test.go

func TestConvert(t *testing.T) {

        tests := []struct {
                n    int
                want string
        } {
                { n: 1, want: "1" },
                { n: 2, want: "2" },
                { n: 3, want: "Fizz" },
                { n: 5, want: "Buzz" },
                { n: 15, want: "FizzBuzz" },
        }

        for _, tt := range tests {
                got := fizzbuzz.Convert(tt.n)
                if got != tt.want {
                        t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
                }
        }
}

テストが失敗することを確認できたら次に進みます。

Green

実装を追加してテストがパスすることを確認します。

// fizzbuzz.go

func Convert(n int) string {
        if n%3 == 0 && n%5 == 0 {
                return "FizzBuzz"
        }
        if n%5 == 0 {
                return "Buzz"
        }
        if n%3 == 0 {
                return "Fizz"
        }
        return strconv.Itoa(n)
}

そろそろこのリズムにも慣れてきた頃ではないでしょうか?

Refactor

このタイミングでリファクタリングすべき箇所はあるでしょうか?

実装側のコードを見てみるとifの羅列になっているので、Go言語のswitch文を利用するとスッキリかけそうな気がします。やってみましょう。

// fizzbuzz.go

func Convert(n int) string {
        switch {
        case n%3 == 0 && n%5 == 0:
                return "FizzBuzz"
        case n%5 == 0:
                return "Buzz"
        case n%3 == 0:
                return "Fizz"
        default:
                return strconv.Itoa(n)
        }
}

テストを実行してリファクタリングが成功していることを確認しましょう。

$ go test -v
=== RUN   TestConvert
--- PASS: TestConvert (0.00s)
PASS

問題なさそうです。

ところで最初の条件文であるn%3 == 0 && n%5 == 0ですが、35の両方で割り切れるということは15で割り切れることと同義です。ここもあわせてリファクタリングしましょう。

// fizzbuzz.go

func Convert(n int) string {
        switch {
        case n%15 == 0: // ここを変更
                return "FizzBuzz"
        case n%5 == 0:
                return "Buzz"
        case n%3 == 0:
                return "Fizz"
        default:
                return strconv.Itoa(n)
        }
}

簡単な変更ですが、念のためテストを実行して壊していないことを確認しましょう。

おめでとうございます!

これですべてのタスクを消化し、FizzBuzzの実装を終えることができました!

ところで現状は123515の計5つのパターンしかテストしていません。これだけのテストパターンで不安はないでしょうか?とくにFizzとBuzzは割り切れる最初の値(35)しかテストしていないことがわかります。

さすがにこれだけでは不安だと感じるのでテストパターンを追加していきます。

今回は1〜15までを確認できれば十分安心だと思い、次のようにテストパターンを追加してテストを実行し、不安を解消しましょう!

func TestConvert(t *testing.T) {

        tests := []struct {
                n        int
                want string
        } {
                { n: 1, want: "1" },
                { n: 2, want: "2" },
                { n: 3, want: "Fizz" },
                { n: 4, want: "4" },
                { n: 5, want: "Buzz" },
                { n: 6, want: "Fizz" },
                { n: 7, want: "7" },
                { n: 8, want: "8" },
                { n: 9, want: "Fizz" },
                { n: 10, want: "Buzz" },
                { n: 11, want: "11" },
                { n: 12, want: "Fizz" },
                { n: 13, want: "13" },
                { n: 14, want: "14" },
                { n: 15, want: "FizzBuzz" },
        }
...

不安を解消できたら次に進みましょう!

現状でも十分なレベルに仕上がっていますが、最後にテストコードを少し改善して終わりにしましょう。

サブテストに分割する

今のテストコードでは1〜15の入力パターンに対してテストを行っていますが、コンソール上では1つのテストとして扱われていました。t.Runメソッドを利用するとそれらを個別のテストとして実行することが出来ます。

次のようにテストコードを変更してみましょう。

import (
        "fmt" // fmtパッケージを追加
        ...
}

func TestConvert(t *testing.T) {
...

        for _, tt := range tests {
                name := fmt.Sprintf("number:%v", tt.n) // テストの名前

                // サブテストとして実行
                t.Run(name, func(t *testing.T) {
                        got := fizzbuzz.Convert(n)
                        if got != tt.want {
                                t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
                        }
                })
        }
}

t.Runメソッドは、第1引数がテストケース名、第2引数がテストとして実行する関数となっています。

この状態でテストを実行すると次のような出力が得られます。

$ go test -v
=== RUN   TestConvert
=== RUN   TestConvert/number:1
=== RUN   TestConvert/number:2
=== RUN   TestConvert/number:3
=== RUN   TestConvert/number:4
=== RUN   TestConvert/number:5
=== RUN   TestConvert/number:6
=== RUN   TestConvert/number:7
=== RUN   TestConvert/number:8
=== RUN   TestConvert/number:9
=== RUN   TestConvert/number:10
=== RUN   TestConvert/number:11
=== RUN   TestConvert/number:12
=== RUN   TestConvert/number:13
=== RUN   TestConvert/number:14
=== RUN   TestConvert/number:15
--- PASS: TestConvert (0.00s)
    --- PASS: TestConvert/number:1 (0.00s)
    --- PASS: TestConvert/number:2 (0.00s)
    --- PASS: TestConvert/number:3 (0.00s)
    --- PASS: TestConvert/number:4 (0.00s)
    --- PASS: TestConvert/number:5 (0.00s)
    --- PASS: TestConvert/number:6 (0.00s)
    --- PASS: TestConvert/number:7 (0.00s)
    --- PASS: TestConvert/number:8 (0.00s)
    --- PASS: TestConvert/number:9 (0.00s)
    --- PASS: TestConvert/number:10 (0.00s)
    --- PASS: TestConvert/number:11 (0.00s)
    --- PASS: TestConvert/number:12 (0.00s)
    --- PASS: TestConvert/number:13 (0.00s)
    --- PASS: TestConvert/number:14 (0.00s)
    --- PASS: TestConvert/number:15 (0.00s)
PASS

今回のFizzBuzzでは単なる連番であるためそこまで重要性を感じないかもしれませんが、どういったパターンがテストされているのか分かるのは便利です。

テストを並列実行する

今回のテストパターンはそれぞれを独立して実行させても問題ありません。

以下のようにテストコードを変更して並列実行させてみましょう。

func TestConvert(t *testing.T) {
        ...

        for _, tt := range tests {
                tt := tt // ローカル変数を用意
                name := fmt.Sprintf("number:%v", tt.n)

                t.Run(name, func(t *testing.T) {
                        t.Parallel() // 並列実行するように
                        got := fizzbuzz.Convert(tt.n)
                        if got != tt.want {
                                t.Errorf(`Convert(%v) = %q but want %q`, tt.n, got, tt.want)
                        }
                })
        }
}

t.Runメソッドに与えている関数がループ毎のttをキャプチャできるようにループの先頭でローカル変数を用意しています。

また、t.Runメソッド内でt.Parallelメソッドを呼び出し並列実行するように指定しています。

このようにすることでテストが並列化されて実行されるようになります。

なおデフォルトではGOMAXPROCSの数(デフォルトではCPUのコア数)で並列化されますが、-parallel nオプションを実行時に与えることでnの数で並列化が行われるようになります。次は並列数として2を指定する例です。

$ go test -v -parallel 2

テスト用のヘルパー関数を用意する

最後にt.Runメソッド内で記述しているテスト用のコードをメソッドとして抽出して見通しを良くしてみましょう。

次のようにテストコードを変更します。

func TestConvert(t *testing.T) {
        ...

        for _, tt := range tests {
                tt := tt
                name := fmt.Sprintf("number:%v", tt.n)

                t.Run(name, func(t *testing.T) {
                        t.Parallel()
                        testFizzBuzz(t, tt.n, tt.want) // 関数を呼び出すように変更
                })
        }
}

// テスト関数として抽出
func testFizzBuzz(t *testing.T, n int, want string) {
        t.Helper()
        got := fizzbuzz.Convert(n)
        if got != want {
                t.Errorf(`Convert(%v) = %q but want %q`, n, got, want)
        }
}

testFizzBuzz関数の先頭でt.Helperメソッドを呼び出していますが、これはテストが失敗した時に報告されるエラー行をtestFizzBuzz関数の呼び出し元にする効果があります。これを呼び出さなかった場合にテストが失敗すると、報告されるエラー行は以下のようにtestFizzBuzz関数内でt.Errorfメソッドを呼び出している箇所になってしまいます。

--- FAIL: TestConvert/number:1 (0.00s)
    fizzbuzz_test.go:47: Convert(1) = "1" but expect "Foo"

#
# t.Helperを利用しない場合、t.Errorf関数の位置で失敗したと報告される
#

t.Errorf(`Convert(%v) = %q but want %q`, n, got, want) # L47

t.Helperメソッドを利用した場合、testFizzBuzz関数の呼び出し元で失敗したと報告されます。

--- FAIL: TestConvert/number:1 (0.00s)
    fizzbuzz_test.go:38: Convert(1) = "1" but expect "Foo"

#
# t.Helperを利用した場合、testFizzBuzz関数の位置で失敗したと報告される
#

testFizzBuzz(t, tt.n, tt.expect) #L38

最後にテストを実行して、問題なくパスすることを確認しましょう。

このコードラボを通じて、TDDのサイクルである Red / Green / Refactor を何度も回しながらFizzBuzzを完成させました!

実装コード・テストコードともに見通しがよくクリーンな状態になっていますし、自動化されたテストコードのおかげで仕様変更やリファクタリングも自信をもって行うことができるでしょう。

What you'll learn

それでは、よきTDDライフを!

One more thing

今回の題材はさすがに簡単すぎて退屈だったでしょうか?あるいは物足りないでしょうか?

TDDBC(TDD Bootcamp)というイベントで題材として使われている、ポーカーをTDDで実装してみるのも面白いでしょう。

TDDBC仙台07課題:ポーカー