背景
最近techschool/simplebankの写経を行っている。関連動画[Backend #13] Mock DB for testing HTTP API in Go and achieve 100% coverage - YouTubeでカバレッジの計測方法を説明しているのだが、個人的に難しく感じた。テストおよびカバレッジの計測方法は以前から興味があったので、簡単な例を用いて練習する。
はじめに
最終的には以下のようなフォルダ構成となる。
$ tree
.
├── fizzbuzz
│ ├── fizzbuzz.go
│ └── fizzbuzz_test.go
├── go.mod
└── Makefile
この記事は下記バージョンで動作を確認している。
$ go version
go version go1.17.3 linux/amd64
同じ内容をn-hachi/fizzbuzz-goにも配置した。
手順
Step1:テスト対象作成
ここではfizzbuzzをテストする。下記内容をファイル名fizzbuzz/fizzbuzz.go
で保存する。
package fizzbuzz
import "fmt"
// Fizzbuzz takes one postive value and return string "Fizz", "Buzz", "FizzBuzz" or number.
func Fizzbuzz(n uint64) string {
switch {
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
case n%15 == 0:
return "FizzBuzz"
default:
return fmt.Sprintf("%d", n)
}
}
Step2:テストコードのベース作成
続いてfizzbuzz/fizzbuzz.go
をテストするためのコードを示す。ちなみにテストコードはファイル名の末尾が_test.go
である必要がある。このテストコードを少しずつ改善していく。
はじめに何もしないテストコードを示す。下記内容をファイル名fizzbuzz/fizzbuzz_test.go
で保存する。
package fizzbuzz
import "testing"
func TestFizzbuzz(t *testing.T) {
}
以上で最低限のコードを生成したことになる
Step3:テストの実行
この状態でコマンドgo test ./... -v
を実行する
$ go test ./... -v
go: cannot find main module, but found .git/config in /home/n_hachi/src/github.com/n-hachi/fizzbuzz-go
to create a module there, run:
go mod init
少なくとも私の環境ではこのように出力されたことからテストが失敗しているようだ(go mod
が実装されていないバージョンのgoコマンドを使う場合は結果が変わるかもしれない)。コメントにあるように以下コマンドを実行する。
$ go mod init
go: creating new go.mod: module github.com/n-hachi/fizzbuzz-go
go: to add module requirements and sums:
go mod tidy
上記コマンドを実行することでgo.mod
が生成される。この状態で再度go test ./... -v
を実行する
=== RUN TestFizzbuzz
--- PASS: TestFizzbuzz (0.00s)
PASS
ok github.com/n-hachi/fizzbuzz-go/fizzbuzz 0.008s
今度はテストが成功する。ついでにgo test ./... -v
の実行を簡略化するため(というか私がコマンドを忘れるので)下記内容のMakefile
を作成する
test:
go test ./... -v
.PHONY: test
以降はmake test
と入力することでテストが走る。
Step4:テストケースを一つ追加
テストを実装する。fizzbuzz/fizzbuzz_test.go
を以下のように編集する。
package fizzbuzz
import "testing"
func TestFizzbuzz(t *testing.T) {
// Check number string
n1 := uint64(1)
s1 := "1"
if s1 != Fizzbuzz(n1) {
t.Errorf("Fizzbuzz(1) = %v but expected %v\n", Fizzbuzz(n1), s1)
}
}
テスト関数はTest
を先頭に持つ必要がある。上記テスト関数は「関数Fizzbuzzに値1を与えたら、文字列"1"が帰ってくる」ことを期待したテストである。この状態でmake test
を実行すると以下のようになる。
$ go test ./... -v
=== RUN TestFizzbuzz
--- PASS: TestFizzbuzz (0.00s)
PASS
ok github.com/n-hachi/fizzbuzz-go/fizzbuzz (cached)
処理時間(上の例では0.00s)は環境によって変わるかもしれないが、およそ同じ結果になるだろう。上記内容だとStep3の結果と違いがないので正しくテストされているか判断できない。そこで意図的にfizzbuzz/fizzbuzz_test.go
の内容を変更してみる。
s1 := "1"
となっている一行をs1 := "2"
と一時的に変更し、再度make test
を実行する。
$ go test ./... -v
=== RUN TestFizzbuzz
fizzbuzz_test.go:10: Fizzbuzz(1) = 1 but expected 2
--- FAIL: TestFizzbuzz (0.00s)
FAIL
FAIL github.com/n-hachi/fizzbuzz-go/fizzbuzz 0.002s
FAIL
make: *** [Makefile:2: test] Error 1
上記から正しくテストできていると判断できる。次のステップに進む前に一時的な変更をもとに戻す。
Step5:カバレッジ計測
Step4のテストだと関数Fizzbuzzに与える値が1の場合は正しく動作しそうだが、引数の値が「3の倍数」、「5の倍数」、「15の倍数」の場合に正しく動くか判断できない。このケースは単純なので予想可能だが、複雑になった場合に全テストケースを人力で網羅するのは骨が折れる。そこでカバレッジが重要になってくる。カバレッジを表示するためにMakefile
を以下のように変更する
test:
go test -cover ./... -v
coverage:
rm -fr coverage
mkdir coverage
go test -coverprofile=coverage/cover.out ./...
go tool cover -html=coverage/cover.out -o=coverage/cover.html
.PHONY: test coverage
Makefile
を変更したら先ずmake test
を実行してみる。
❯ make test
go test -cover ./... -v
=== RUN TestFizzbuzz
--- PASS: TestFizzbuzz (0.00s)
PASS
coverage: 40.0% of statements
ok github.com/n-hachi/fizzbuzz-go/fizzbuzz (cached) coverage: 40.0% of statements
今までと異なりcoverage: 40.0% of statements
と記載される。以上から「全体の40%はテストでカバーされている」ことがわかる。言い換えると「60%はテストされていない」ということになる。この値を100%に近づけることが正しくテストされていることの一つの指標となる。
続いてmake coverage
と実行する
$ make coverage
rm -fr coverage
mkdir coverage
go test -coverprofile=coverage/cover.out ./...
ok github.com/n-hachi/fizzbuzz-go/fizzbuzz 0.002s coverage: 40.0% of statements
go tool cover -html=coverage/cover.out -o=coverage/cover.html
上記を実行するとファイルcoverage/cover.html
が生成される。これをブラウザで開くと以下のようになる。
緑色の部分がテストされている箇所であり、赤色の部分がテストされていない箇所である。 このようにツールを使うことで、どのパスがテストされていないかを把握できる。
Step6:テストケース追加
テストケースを追加していく。[Backend #13] Mock DB for testing HTTP API in Go and achieve 100% coverage - YouTubeで学んだことはテストケースをSliceで定義するとテストケースを後で追加することが簡単になるようだ。具体的に示す。
package fizzbuzz
import "testing"
func TestFizzbuzz(t *testing.T) {
testCases := []struct {
name string
input uint64
expected string
}{
{
// neither a multiple of 3 nor a multiple of 5, case1
name: "normal, case1",
input: uint64(1),
expected: "1",
},
{
// neither a multiple of 3 nor a multiple of 5, case2
name: "normal, case2",
input: uint64(11),
expected: "11",
},
{
// a multiple of 3, case1
name: "multiple of 3, case1",
input: uint64(3),
expected: "Fizz",
},
{
// a multiple of 3, case2
name: "multiple of 3, case2",
input: uint64(33),
expected: "Fizz",
},
{
// a multiple of 5, case1
name: "multiple of 5, case1",
input: uint64(5),
expected: "Buzz",
},
{
// a multiple of 5, case2
name: "multiple of 5, case2",
input: uint64(50),
expected: "Buzz",
},
{
// a multiple of 15, case1
name: "multiple of 15, case1",
input: uint64(15),
expected: "FizzBuzz",
},
{
// a multiple of 15, case2
name: "multiple of 5, case2",
input: uint64(150),
expected: "FizzBuzz",
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
if tc.expected != Fizzbuzz(tc.input) {
t.Errorf("Fizzbuzz(%v) = %v but expected %v\n", tc.input, Fizzbuzz(tc.input), tc.expected)
}
})
}
}
上記例では無名の構造体を作成し、その型を満たすテストケースを追加している。このようにすることで後でテストケースに不足があった場合に追加が簡単になる。
この状態でmake test
を実行する。
$ make test
go test -cover ./... -v
=== RUN TestFizzbuzz
=== RUN TestFizzbuzz/normal,_case1
=== RUN TestFizzbuzz/normal,_case2
=== RUN TestFizzbuzz/multiple_of_3,_case1
=== RUN TestFizzbuzz/multiple_of_3,_case2
=== RUN TestFizzbuzz/multiple_of_5,_case1
=== RUN TestFizzbuzz/multiple_of_5,_case2
=== RUN TestFizzbuzz/multiple_of_15,_case1
fizzbuzz_test.go:65: Fizzbuzz(15) = Fizz but expected FizzBuzz
=== RUN TestFizzbuzz/multiple_of_5,_case2#01
fizzbuzz_test.go:65: Fizzbuzz(150) = Fizz but expected FizzBuzz
--- FAIL: TestFizzbuzz (0.00s)
--- PASS: TestFizzbuzz/normal,_case1 (0.00s)
--- PASS: TestFizzbuzz/normal,_case2 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_3,_case1 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_3,_case2 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_5,_case1 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_5,_case2 (0.00s)
--- FAIL: TestFizzbuzz/multiple_of_15,_case1 (0.00s)
--- FAIL: TestFizzbuzz/multiple_of_5,_case2#01 (0.00s)
FAIL
coverage: 80.0% of statements
FAIL github.com/n-hachi/fizzbuzz-go/fizzbuzz 0.003s
FAIL
make: *** [Makefile:2: test] Error 1
15の倍数を関数Fizzbuzzに与えたら文字列"Fizzbuzz"が出てほしいが、実際には"Fizz"と出力されていることがわかる。fizzbuzz/fizzbuzz.go
を以下のように修正する。
package fizzbuzz
import "fmt"
// Fizzbuzz takes one postive value and return string "Fizz", "Buzz", "FizzBuzz" or number.
func Fizzbuzz(n uint64) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return fmt.Sprintf("%d", n)
}
}
switchの順序をcase n%15==0
が先頭に来るように入れ替えた。再度make test
を実行する。
$ make test
go test -cover ./... -v
=== RUN TestFizzbuzz
=== RUN TestFizzbuzz/normal,_case1
=== RUN TestFizzbuzz/normal,_case2
=== RUN TestFizzbuzz/multiple_of_3,_case1
=== RUN TestFizzbuzz/multiple_of_3,_case2
=== RUN TestFizzbuzz/multiple_of_5,_case1
=== RUN TestFizzbuzz/multiple_of_5,_case2
=== RUN TestFizzbuzz/multiple_of_15,_case1
=== RUN TestFizzbuzz/multiple_of_5,_case2#01
--- PASS: TestFizzbuzz (0.00s)
--- PASS: TestFizzbuzz/normal,_case1 (0.00s)
--- PASS: TestFizzbuzz/normal,_case2 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_3,_case1 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_3,_case2 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_5,_case1 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_5,_case2 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_15,_case1 (0.00s)
--- PASS: TestFizzbuzz/multiple_of_5,_case2#01 (0.00s)
PASS
coverage: 100.0% of statements
ok github.com/n-hachi/fizzbuzz-go/fizzbuzz 0.002s coverage: 100.0% of statements
上記からテストが通った事がわかる。またcoverage: 100.0% of statements
からカバレッジが100%に到達できたことが明らかになった。
最後に
このサイズならテストなしでも問題ないかもしれないが、規模が大きくなるとやはりテストなしだと厳しいと思う。今後もこれをベースにテストやカバレッジ計測を実施していきたい。
参考
テストに関して
カバレッジに関して