Goの基本的なTest手法をFizzBuzzで試してみた

はじめに

今回はGoの標準testing packageを使った基本的なテストの方法を試していきたいと思います。 テストの題材としてFizzBuzz問題を利用してテストを書いていきます。

FizzBuzzの要件

FizzBuzz問題の要件を簡単に整理します。以下の要件を確認するためのテストケースを実装します。

  • 数値を文字列に変換する
  • 3の倍数のときFizzと出力する
  • 5の倍数のときBuzzと出力する
  • 15の倍数のとき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を追加して、最初の要件の通りの挙動になっているか確認します。

  • 数値を文字列に変換する
  • 3の倍数のときFizzと出力する
  • 5の倍数のときBuzzと出力する
  • 15の倍数のときFizzBuzzと出力する
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には他にも様々な手法があるのでそのうち紹介できればと思います!