テストを書く
読了目安 約3分
競プロのサンプルケース照合を自動化して恒久化したものとしてテストを捉え、go test とテーブル駆動テストを手元で書きます。
この章の目次
競プロでは、提出前にサンプルケースを流し、出力例と見比べます。 テストは、この照合を自動化して、コードと一緒に保存しておく仕組みです。
サンプル照合との違い
競プロの照合と違う点は 2 つあります。
- 自動化:期待値との比較をコードとして書くので、出力を目で見比べる代わりに、コマンド 1 つで全ケースを検証できます
- 恒久化:テストコードはリポジトリに残り続けるので、半年後に誰かがその関数を書き換えたときも、同じ検証がそのまま走ります
_test.go と go test
go test は、テストを専用の形式でビルドして実行するコマンドです。
右のエディタでは動かないので、この章は手元で進めます(環境がなければ 学習環境を整える へ)。
まず、1 章で覚えた形でプロジェクトを作ります。
mkdir calc-demo
cd calc-demo
go mod init example.com/calc-demoテストしたい関数を calc.go に書きます。
// calc.go
package calc
func Sum(nums []int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}テストは、同じディレクトリの、名前が _test.go で終わるファイルに書きます。
// calc_test.go
package calc
import "testing"
func TestSum(t *testing.T) {
got := Sum([]int{1, 2, 3})
if got != 6 {
t.Errorf("Sum([1 2 3]) = %d, want 6", got)
}
}ルールは 3 つです。
- ファイル名を
_test.goで終える - 関数名を
Testで始め、引数に*testing.Tを取る - 期待と違ったら
t.Errorfで報告する
比較のための特別な構文はなく、ただの if 文で書きます。
実行は go test です。
go test # 成功すると ok と表示される
go test -v # 個々のテストの成否も表示されるテストの価値は、わざと壊してみるとよく見えます。
total += n を total -= n に変えて go test を実行すると、次のように失敗します。
--- FAIL: TestSum (0.00s)
calc_test.go:8: Sum([1 2 3]) = -6, want 6
FAILどのファイルの何行目で、何を期待して何が返ったかが、そのまま出力されます。 確かめたら、元に戻しておいてください。
テーブル駆動テスト
ケースを増やすとき、TestSum2、TestSum3 と関数を増やしてはいきません。
テーブル駆動テストは、入力と期待値の一覧を struct の slice として持ち、ループで全ケースを検証する Go の定番の書き方です。
// calc_test.go
package calc
import "testing"
func TestSum(t *testing.T) {
tests := []struct {
name string
nums []int
want int
}{
{"正の数", []int{1, 2, 3}, 6},
{"空の slice", []int{}, 0},
{"負の数を含む", []int{5, -3}, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Sum(tt.nums)
if got != tt.want {
t.Errorf("Sum(%v) = %d, want %d", tt.nums, got, tt.want)
}
})
}
}ケースの追加は slice に 1 行足すだけなので、不具合を見つけるたびに「その不具合を再現するケース」を足していけます。
足したケースは以後ずっと検証されるので、同じ不具合が再発すれば go test が検出します。
動いているコードを変えるための道具
テストが必須とされる理由は、正しく書くためというより、速く変えるためです。 変更のたびに全機能を手で確かめるのは現実的でないので、壊していないことの確認を機械に任せます。 テストのないコードは、動いていても壊すのが怖くて触れず、変更の速度がそこで頭打ちになります。
ISUCON にも同じ構図があります。 高速化のためにアプリコードを書き換えるたびにベンチマーカーを回し、スコアと失敗の有無を確かめます(推測するな、計測せよ)。
試してみよう
- 上の calc-demo を作り、
go testが通ることを確かめる。 Sumをわざと壊して失敗の出力を読み、元に戻す。- 最大値を返す
Max(nums []int) (int, error)をcalc.goに追加する。空の slice にはエラーを返してください(5 章の復習です)。 Maxのテーブル駆動テストを書く。正常なケースに加えて、空の slice でエラーが返ることを確かめるケースも入れてください。
4 では、テーブルの struct に「エラーを期待するか」を表すフィールド(wantErr bool など)を足すのが定番です。