Goの基本的なTest手法をFizzBuzzで試してみた
はじめに
今回はGoの標準testing packageを使った基本的なテストの方法を試していきたいと思います。 テストの題材としてFizzBuzz問題を利用してテストを書いていきます。
FizzBuzzの要件
FizzBuzz問題の要件を簡単に整理します。以下の要件を確認するためのテストケースを実装します。
準備
以下のファイルを作成します。
数値を文字列に変換する実装(サブテスト)
まずは最初の要件として、数値を文字列へ変換する
を確認するためのテストを実装します。
import "strconv" func Stringify(num int) string { return strconv.Itoa(num) }
import "testing" func Test_Stringify(t *testing.T) { if actual, expect := Stringify(1), "1"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }
> go test PASS ok github.com/y-zumi/tdd 0.005s
これで、数値を文字列へ変換する
を実装することができました。では、他の数値でも変換されるかを試してみます。
子テストを追加するためにはサブテストを利用します。サブテストはtesting.T.Run
メソッドを使うと書くことができます。
import "testing" func Test_Stringify(t *testing.T) { t.Run("when number is 1", func(t *testing.T) { if actual, expect := Stringify(1), "1"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) t.Run("when number is 2", func(t *testing.T) { if actual, expect := Stringify(2), "2"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) t.Run("when number is 100", func(t *testing.T) { if actual, expect := Stringify(100), "100"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) }
> go test -v === RUN Test_Stringify === RUN Test_Stringify/when_number_is_1 === RUN Test_Stringify/when_number_is_2 === RUN Test_Stringify/when_number_is_100 --- PASS: Test_Stringify (0.00s) --- PASS: Test_Stringify/when_number_is_1 (0.00s) --- PASS: Test_Stringify/when_number_is_2 (0.00s) --- PASS: Test_Stringify/when_number_is_100 (0.00s) PASS ok github.com/y-zumi/tdd 0.005s
サブテストはテストケースに名前をつけることができるため、テストの可読性が高くなります。
3の倍数のときFizzと出力する実装(サブテストによる構造化)
次に3の倍数のときFizzと出力する
を実装します。この実装でテストケースが増えるため、各サブテストを数値を文字列へ変換する
と3の倍数のときFizzと出力する
にまとめます。
サブテストを入れ子にするためには、testing.T.Run
でサブテスト群をまとめます。
import "strconv" func Stringify(num int) string { if num%3 == 0 { return "fizz" } return strconv.Itoa(num) }
import "testing" func Test_Stringify(t *testing.T) { t.Run("normal number", func(t *testing.T) { t.Run("when number is 1", func(t *testing.T) { if actual, expect := Stringify(1), "1"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) t.Run("when number is 2", func(t *testing.T) { if actual, expect := Stringify(2), "2"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) t.Run("when number is 100", func(t *testing.T) { if actual, expect := Stringify(100), "100"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) }) t.Run("Multiple of 3", func(t *testing.T) { t.Run("when number is 3", func(t *testing.T) { if actual, expect := Stringify(3), "fizz"; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) }) }
> go test -v === RUN Test_Stringify === RUN Test_Stringify/normal_number === RUN Test_Stringify/normal_number/number_is_1 === RUN Test_Stringify/normal_number/number_is_101 === RUN Test_Stringify/Multiple_of_3 === RUN Test_Stringify/Multiple_of_3/number_is_3 === RUN Test_Stringify/Multiple_of_3/number_is_99 === RUN Test_Stringify/Multiple_of_5 === RUN Test_Stringify/Multiple_of_5/number_is_5 === RUN Test_Stringify/Multiple_of_5/number_is_100 --- PASS: Test_Stringify (0.00s) --- PASS: Test_Stringify/normal_number (0.00s) --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s) --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s) --- PASS: Test_Stringify/Multiple_of_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s) PASS ok github.com/y-zumi/tdd 0.005s
5の倍数のときBuzzを出力する実装(テーブル駆動テスト)
テストケースが多くなってきたのでテーブル駆動テストを使用します。
テーブル駆動テストのメリットは以下のとおりです。
- インプットとアウトプットの値の見通しが良い
- テストケースの追加が容易
それではテーブル駆動テストで実装してみましょう。
import "testing" type TestCase struct { description string input int output string } func Test_Stringify(t *testing.T) { t.Run("normal number", func(t *testing.T) { cases := []TestCase{ {"number is 1", 1, "1"}, {"number is 101", 101, "101"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) t.Run("Multiple of 3", func(t *testing.T) { cases := []TestCase{ {"number is 3", 3, "fizz"}, {"number is 99", 99, "fizz"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) t.Run("Multiple of 5", func(t *testing.T) { cases := []TestCase{ {"number is 5", 5, "buzz"}, {"number is 100", 100, "buzz"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) }
=== RUN Test_Stringify === RUN Test_Stringify/normal_number === RUN Test_Stringify/normal_number/number_is_1 === RUN Test_Stringify/normal_number/number_is_101 === RUN Test_Stringify/Multiple_of_3 === RUN Test_Stringify/Multiple_of_3/number_is_3 === RUN Test_Stringify/Multiple_of_3/number_is_99 === RUN Test_Stringify/Multiple_of_5 === RUN Test_Stringify/Multiple_of_5/number_is_5 === RUN Test_Stringify/Multiple_of_5/number_is_100 --- PASS: Test_Stringify (0.00s) --- PASS: Test_Stringify/normal_number (0.00s) --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s) --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s) --- PASS: Test_Stringify/Multiple_of_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s) --- PASS: Test_Stringify/Multiple_of_5 (0.00s) --- PASS: Test_Stringify/Multiple_of_5/number_is_5 (0.00s) --- PASS: Test_Stringify/Multiple_of_5/number_is_100 (0.00s) PASS ok github.com/y-zumi/tdd 0.005s
15の倍数のときFizzBuzzを出力する実装
それでは最後に15の倍数のときFizzBuzzを出力する
を実装します。
import "strconv" func Stringify(num int) string { var result string if num%3 == 0 { result += "Fizz" } if num%5 == 0 { result += "Buzz" } if result == "" { result = strconv.Itoa(num) } return result }
import "testing" type TestCase struct { description string input int output string } func Test_Stringify(t *testing.T) { t.Run("normal number", func(t *testing.T) { cases := []TestCase{ {"number is 1", 1, "1"}, {"number is 101", 101, "101"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) t.Run("Multiple of 3", func(t *testing.T) { cases := []TestCase{ {"number is 3", 3, "Fizz"}, {"number is 99", 99, "Fizz"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) t.Run("Multiple of 5", func(t *testing.T) { cases := []TestCase{ {"number is 5", 5, "Buzz"}, {"number is 100", 100, "Buzz"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) t.Run("Multiple of 15", func(t *testing.T) { cases := []TestCase{ {"number is 15", 15, "FizzBuzz"}, {"number is 105", 105, "FizzBuzz"}, } for _, c := range cases { t.Run(c.description, func(t *testing.T) { if actual, expect := Stringify(c.input), c.output; actual != expect { t.Errorf("actual=%s, expect=%s", actual, expect) } }) } }) }
=== RUN Test_Stringify === RUN Test_Stringify/normal_number === RUN Test_Stringify/normal_number/number_is_1 === RUN Test_Stringify/normal_number/number_is_101 === RUN Test_Stringify/Multiple_of_3 === RUN Test_Stringify/Multiple_of_3/number_is_3 === RUN Test_Stringify/Multiple_of_3/number_is_99 === RUN Test_Stringify/Multiple_of_5 === RUN Test_Stringify/Multiple_of_5/number_is_5 === RUN Test_Stringify/Multiple_of_5/number_is_100 === RUN Test_Stringify/Multiple_of_15 === RUN Test_Stringify/Multiple_of_15/number_is_15 === RUN Test_Stringify/Multiple_of_15/number_is_105 --- PASS: Test_Stringify (0.00s) --- PASS: Test_Stringify/normal_number (0.00s) --- PASS: Test_Stringify/normal_number/number_is_1 (0.00s) --- PASS: Test_Stringify/normal_number/number_is_101 (0.00s) --- PASS: Test_Stringify/Multiple_of_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_3 (0.00s) --- PASS: Test_Stringify/Multiple_of_3/number_is_99 (0.00s) --- PASS: Test_Stringify/Multiple_of_5 (0.00s) --- PASS: Test_Stringify/Multiple_of_5/number_is_5 (0.00s) --- PASS: Test_Stringify/Multiple_of_5/number_is_100 (0.00s) --- PASS: Test_Stringify/Multiple_of_15 (0.00s) --- PASS: Test_Stringify/Multiple_of_15/number_is_15 (0.00s) --- PASS: Test_Stringify/Multiple_of_15/number_is_105 (0.00s) PASS ok github.com/y-zumi/tdd 0.007s
動作確認
これまでテストを実装して動作確認をしました。
最終確認としてmain.go
を追加して、最初の要件の通りの挙動になっているか確認します。
import "fmt" func main() { for i := 1; i <= 110; i++ { fmt.Println(Stringify(i)) } }
> go run main.go 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz ︙ 98 Fizz Buzz 101 Fizz 103 104 FizzBuzz 106 107 Fizz 109 Buzz
期待通りの結果を得ることができました :)
まとめ
本記事ではGoのtestに関する以下の2つの事柄を扱いました。
- サブテスト
- テーブル駆動テスト
基本的なことですがGoでテストをするには大事なことなので紹介しました。Goのtestには他にも様々な手法があるのでそのうち紹介できればと思います!