Golangテスト周りに関する覚書
基本的なテストコード
サンプル1: 最小限のテストコード
package mypackage
func MyFunction(s string) string {
return s + s
}
package mypackage
import (
"testing"
)
func TestMyFunction(t *testing.T) {
input := "わーい"
got := MyFunction(input)
want := "わーいわーい"
if got != want {
t.Errorf("got:%s want: %s", got, want)
}
}
サンプル2: テーブル駆動テスト
func Test(t *testing.T) {
// テスト対象の初期化関数
initModel := func() *Model {
return &Model{
Name: "TEST"
Age: 0
}
}
tests := []struct {
name string // テストケース名
model func() *Model // テスト対象を加工する関数
want bool // 期待値
}{
{
name: "testCase_1",
model: func() *Model {
// ベースとなるモデルを取得
model := initModel()
// テスト内容に応じて加工
model.Age = 18
// 返却
return model
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// テスト対象を取得
model := tt.model()
// 検証
got := model.IsAdult()
if got != tt.want {
t.Error("Error")
}
})
}
}
-
initModel
はテストで使うモジュールの初期化を行う
- テスト結果に直接影響しないが、設定しておかないとテストが失敗する項目など
-
tests
にはでモジュールの加工を行う関数を設けておき、テスト内容に応じてinitModel
で初期化したモジュールのデータを加工する
-
initModel
で毎度モジュールのインスタンスを生成しているのは、変数を共用した場合、構造体内部でポインタ型が使用されているとあるテストの設定が別のテストに影響を与える恐れがあるからである
サンプル3: ドキュメント化を意識したテストコード
func Test(t *testing.T) {
t.Run("テストの分類1", func(t *testing.T) {
t.Run("検証内容1", func(t *testing.T) {
// 検証
if xxx != xxx {
t.Error("エラーメッセージ")
}
})
t.Run("検証内容2", func(t *testing.T) {
// 検証
if xxx != xxx {
t.Error("エラーメッセージ")
}
})
:
})
t.Run("テストの分類2", func(t *testing.T) {
t.Run("検証内容", func(t *testing.T) {
// 検証
if xxx != xxx {
t.Error("エラーメッセージ")
}
})
})
}
-
t.Run
を繰り返し記述することでJest風味のテストコードを書くことができる
- この書き方の場合、テストが失敗した際エラーメッセージに出力される失敗した行と検証コードが近くなるのでソースコードの可読性が上がる
- また、カテゴリ → バリエーションが明確になるのでテストコードがドキュメントとして機能する
- 例えば、「正常系 → 動作バリエーション」「異常系 → エラーバリエーション」というような(というか、ほぼその為に使う)
- テストの実行結果は↓のようにカテゴリ毎にインデントされるので結果もわかりやすい
=== RUN Test
=== RUN Test/テストの分類1
=== RUN Test/テストの分類1/検証内容1
=== RUN Test/テストの分類1/検証内容2
=== RUN Test/テストの分類2
=== RUN Test/テストの分類2/検証内容
--- PASS: Test (0.00s)
--- PASS: Test/テストの分類1 (0.00s)
--- PASS: Test/テストの分類1/検証内容1 (0.00s)
--- PASS: Test/テストの分類1/検証内容2 (0.00s)
--- PASS: Test/テストの分類2 (0.00s)
--- PASS: Test/テストの分類2/検証内容 (0.00s)
PASS
テストの実行
全てのテストを実行
go test
パッケージ単位で実行
関数単位で実行
ファイル名を指定して実行
カバレッジの計測
ベンチマークの計測
ビルドタグによるテスト対象の制御
- ビルドタグを定義することで
-tags
によるテスト実行の制御が可能
//go:build extratests
package mypackage
import (
"testing"
)
func TestMyFunction(t *testing.T) {
input := "わーい"
got := MyFunction(input)
want := "わーいわーい"
if got != want {
t.Errorf("got:%s want: %s", got, want)
}
}
go test -v -tags=extratests ./...
並列実行
テストコードを書く
前処理/後処理
並列実行
テストをスキップ
Exampleテスト
テストデータ
テストコードのパッケージ
ヘルパー関数
func helperFunction(t *testing.T, s string) string {
t.Helper() //
u, err := url.Parse(s)
if err != nil {
t.Fatal(err)
}
return u
}
func TestHello(t *testing.T) {
message := "ieeeeeee"
result := Echo(message)
expext := message
if result != expext {
t.Error("\n実際: ", result, "\n理想: ", expext)
}
t.Cleanup(func() {
t.Log("Cleanup")
})
}
T.Error
テーブル駆動テスト
非公開関数をテストする
benchmark
便利ライブラリ
参考資料
Appendix-1
type T
func (c *T) Cleanup(f func())
func (t *T) Deadline() (deadline time.Time, ok bool)
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (t *T) Setenv(key, value string)
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() string
T.Cleanup
- 全てのテストが完了した際に呼び出される関数を登録する
func TestHello(t *testing.T) {
message := "ieeeeeee"
result := Echo(message)
expext := message
if result != expext {
t.Error("\n実際: ", result, "\n理想: ", expext)
}
t.Cleanup(func() {
t.Log("Cleanup")
})
}