Go での単体テスト
実際に、Go で単体テストを行うにはどのようにするのかを見ていきます
テスト対象を作成
まずは、テスト対象の関数を作成します
ここでは例として、スライスの要素を逆順にしたものを返す Reverse()
関数を作成します
package slice
func Reverse(a []int) []int {
b := make([]int, 0, len(a))
copy(a, b)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return b
}
.
└── reverse.go
テストファイルを用意
Go でテストを行う際は _test
という接尾辞をつけたファイルを用意します
その中で testing
パッケージの testing.T
を使用して以下のように書きます
package slice
import "testing"
func TestReverse(t *testing.T) {
// ここにテストを書いていく
}
.
├── reverse.go
└── reverse_test.go
パッケージについて
上記の例では slice
パッケージにテストコードを書いていますが、テストの場合は package slice_test
のように接尾辞に _test
を付け加えたものにしても問題ありません
どちらの方法を取るのかは実装者やテスト内容によって変わります
以下にメリットデメリットをまとめたので、書く際の参考にしてみてください
- テスト対象と同じパッケージ名称でテストコードを書く
- メリット
- 非公開な変数を変えたり、非公開な関数を呼び出したりテストしたりできる
- デメリット
- 非公開な変数や関数にアクセスできてしまう
- 触る必要のない場合はアクセスできなくて良い
- 非公開な変数や関数にアクセスできてしまう
- メリット
- テスト対象とは別のパッケージ名称でテストコードを書く
- メリット
- テスト対象とテストコードが疎結合になる
- テスト対象を利用する側の意識を持ちやすい
- 初めて読んだ人も見やすい
- テスト対象とテストコードが疎結合になる
- デメリット
- 非公開な機能をテストしたりできない
- 一応テストする裏技はある
- Go Friday こぼれ話:非公開(unexported)な機能を使ったテスト
- 非公開な機能をテストしたりできない
- メリット
テストコードを書く
Go では テーブル駆動テスト(Table Driven Test) と呼ばれる手法がよく用いられます
テーブル駆動テストはテーブルとテストを用意して、 データとロジックの分離 を目指す手法です
まずは以下のようにテストケースを用意します(これがいわゆるテーブルです)
// テストケースを用意
tests := []struct {
name string
in []int
want []int
}{
{"空スライスを渡した場合、空スライスが返る", []int{}, []int{}},
{"1要素の場合、同じスライスが返る", []int{10}, []int{10}},
{"5要素の場合、逆順に返る", []int{1, 2, 3, 4, 5}, []int{5, 4, 3, 2, 1}},
}
次に、各テストケースに対してテストを走らせます
テストを走らせる際は testing.T.Run()
関数にテスト名と実行する関数を渡します
// テストケースに対してテストを実行
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
got := Reverse(test.in)
// reflect.DeepEqualを使ってもOK
// なにをしているのか追いやすいようにsame()を自前で用意
if !same(got, test.want) {
t.Errorf("want %v, but %v:", test.want, got)
}
})
}
test := testについて
これはforループの変数が使い回されることが原因で起こる意図しない挙動を防ぐために書かれています
雑に言ってしまうと、Go1.21まではこの文を書いておけば良いおまじないのようなものです
以下に詳しく説明します
これが無いとどういった挙動になってしまうのか
t.Run()
で渡しているクロージャ内で test
が使用されています
仮にテストを並列で行うように t.Parallel()
をそのクロージャ内で使用した場合、クロージャの宣言と実行のタイミングが異なるために、forループ終了時点でのテストケースで全てのテストが走ってしまいます
詳しくは、Goの並列テストでよくあるバグ(tt := tt忘れ)に対する対策 を見てみてください
Go1.22以降だとどうなるの?
Go1.22以降ではforループのセマンティクスが変更されるため、記述しなくてもよくなります
詳しくは、 Go1.22のリリース予定の機能を見る を見てみてください
テストコードの全体は以下のようになります
package slice
import "testing"
func TestReverse(t *testing.T) {
// テストケースを用意
tests := []struct {
name string
in []int
want []int
}{
{"空スライスを渡した場合、空スライスが返る", []int{}, []int{}},
{"1要素の場合、同じスライスが返る", []int{10}, []int{10}},
{"5要素の場合、逆順に返る", []int{1, 2, 3, 4, 5}, []int{5, 4, 3, 2, 1}},
}
// テストケースに対してテストを実行
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
got := Reverse(test.in)
// reflect.DeepEqualを使ってもOK
// なにをしているのか追いやすいようにsame()を自前で用意
if !same(got, test.want) {
t.Errorf("want %v, but %v:", test.want, got)
}
})
}
}
// スライスの要素が同じかどうかを判定する
func same(a, b []int) bool {
// nil check
if a == nil && b == nil {
return true
} else if a == nil || b == nil {
return false
}
// len check
if len(a) != len(b) {
return false
}
// value check
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
では、これを go test
コマンドで実行してみます
go test reverse.go reverse_test.go
--- FAIL: TestReverse (0.00s)
--- FAIL: TestReverse/1要素の場合、同じスライスが返る (0.00s)
reverse_test.go:27: want [10], but [0]:
--- FAIL: TestReverse/5要素の場合、逆順に返る (0.00s)
reverse_test.go:27: want [5 4 3 2 1], but [0 0 0 0 0]:
FAIL
FAIL command-line-arguments 0.002s
FAIL
なんとテストが失敗してしまっています
これはどういうことでしょうか
上記のメッセージには want [5 4 3 2 1], but [0 0 0 0 0]:
と出力されており、全ての要素が0のスライスが返ってきていることがわかります
改めて Reverse()
関数を見てみましょう
func Reverse(a []int) []int {
b := make([]int, 0, len(a))
copy(a, b)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return b
}
どうも組み込み関数 copy()
の挙動が怪しいです
ドキュメント を見てみると以下のように書いてあります
copy 組み込み関数は、要素をソース スライスから宛先スライスにコピーします。(特殊なケースとして、文字列からバイトのスライスにバイトをコピーすることもできます。) ソースと宛先は重複する場合があります。Copy は、コピーされた要素の数を返します。これは、len(src) と len(dst) の最小値になります。
(https://pkg.go.dev/builtin#copy より翻訳して引用)
つまり、copy(a, b)
ではなく copy(b, a)
と書くのが正しそうです
以下のように reverse.go のコードを変更します
func Reverse(a []int) []int {
b := make([]int, 0, len(a))
- copy(a, b)
+ copy(b, a)
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return b
}
これで改めて go test
コマンドを実行します
go test reverse.go reverse_test.go
ok command-line-arguments 0.001s
これでテストがちゃんと動いていそうです
なお、上記のテストでは補えていない部分の動作があります
それはどういったものでしょうか?ぜひ考えてみてください
想定解答
- 引数として渡したスライスが変更されていないか