💪

Golang testing ことはじめ

2022/09/29に公開約5,600字

この記事に関して

Golangの標準テストパッケージtestingに関して、改めて整理を行なった備忘録

想定読者

  • golangのテストコードの書き方を学習している方
  • testingパッケージの機能を網羅的に整理したい方

書くこと

  • testingパッケージの基本的な機能
  • gotestsによるテストコード自動生成

書かないこと

  • ベンチマークに関するテスト
  • テストのフィクスチャ

Goのテストの基本

Goは標準機能のみでテストを行うことができ、go test <ファイル名>で実行できます
関数名はTestXxx、ファイル名はxxx_test.goとする必要があります
また、Table Driven Testというテスト形式を公式で推奨しており、以下のようなテストケースをまとめて書く形式

calc.go
package calc

func Add(a, b int) int {
	return a + b
}
calc_test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{
			name: "3+1",
			args: args{3, 1},
			want: 4,
		},
		{
			name: "3+10",
			args: args{3, 10},
			want: 13,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Add(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Add() = %v, want %v", got, tt.want)
			}
		})
	}
}

このように実装することで、TestSub関数の中でテストケースに名前を付けてサブテストを実行することができます ex) "3+1""3+10"
利点としては、「テストがFailになった時にどのケースがFailしたのか見やすくなる」、「テストケースを追加する際に追加の修正が少なくなる」などが挙げられます
では、テストを実行してみましょう
テストコードがあるディレクトリ配下で以下のコマンドを実行します

$ go test
PASS
ok      work/gotest     0.252s

問題なく成功している場合はこのように表示されます。しかしこのままでは、サブテスト形式で実装したメリットが得られないので-vオプションを付けて詳細表示します

$ go test -v
=== RUN   TestAdd
=== RUN   TestAdd/3+1
=== RUN   TestAdd/3+10
--- PASS: TestAdd (0.00s)
    --- PASS: TestAdd/3+1 (0.00s)
    --- PASS: TestAdd/3+10 (0.00s)
PASS

サブテスト名まで表示されていますね!では次に失敗した場合を確認してみます

calc_test.go
-     {
-	name: "3+10",
-	args: args{3, 10},
-	want: 13,
-      },
+     {
+	name: "3+10",
+	args: args{3, 10},
+	want: 130,
+      },
$ go test -v
=== RUN   TestAdd
=== RUN   TestAdd/3+1
=== RUN   TestAdd/3+10
    sum_test.go:30: Add() = 13, want 130
--- FAIL: TestAdd (0.00s)
    --- PASS: TestAdd/3+1 (0.00s)
    --- FAIL: TestAdd/3+10 (0.00s)
FAIL
exit status 1
FAIL    work/gotest     0.279s

どのサブテストで失敗しているか簡単に確認することができます
他の実行方法は後述の基本機能の中で説明します (サブテストの部分実行など)

gotests

上記のようなテストコードの形式は利点が多いが、毎回1つ1つ手で実装するのは時間のかかる作業であるし、基本的な雛形は変わらないためテストコードを楽に作成するツールgotestsを利用します

macos,vscodeで環境を準備する

  1. [Command]+[Shift]+[P]でコマンドパレットを開く
  2. go install toolsと入力して、Go: install update toolsを選択
  3. gotestsにチェックを入れてインストール

指定のテストファイルを開き、コマンドパレットからGo: Generate Unit Tests for Fileを選択することで以下のようなテストファイルが生成される

calc.go
package calc

func Add(a, b int) int {
	return a + b
}
calc_test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Add(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Add() = %v, want %v", got, tt.want)
			}
		})
	}
}

とても便利ですね!
あとは、// TODO: Add test cases.のコメント箇所にテストケースを実装することで、基本的なテストを実装することができる

また、テストコードする範囲を指定することができるGo: Generate Unit Tests for Functionを選択することで、カーソルを当てている関数のテストコードのみ生成することも可能

calc.go
package calc

func Add(a, b int) int {
	return a + b
}

// Subにカーソルを当てている想定
func Sub(a, b int) int {
	return a - b
}
calc_test.go
package calc

import "testing"

func TestSub(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Sub(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Sub() = %v, want %v", got, tt.want)
			}
		})
	}
}

testingの基本機能

テスト実行

func (t *T) Run(name string, f func(t *T)) bool
https://pkg.go.dev/testing#T.Run
上記の関数によってnameという名前でサブテストを実行します、fは別のゴルーチンで実行されfが戻るまでブロックされ、Runはfが成功したどうかを報告します

以下サンプルコード (Table Driven Test)

calc_test.go
// omit
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Sub(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Sub() = %v, want %v", got, tt.want)
			}
		})
	}

テスト失敗のログ

func (c *T) Error(args ...any)
https://pkg.go.dev/testing#T.Error

func (c *T) Fatal(args ...any)
https://pkg.go.dev/testing#T.Fatal

上記の関数などを用いて、開発者が失敗したことを実装する必要があります
2つの関数の違いは、関数呼び出し後の後続の処理を継続するかになります、Fatalの場合は後続の処理が行われません

前処理、後処理

テスト実行前に、DBのセットアップなど前処理を入れたい場合のユースケース

func TestMain(m *testing.M)

上記の関数を用いることで、テストを制御するとテストの前後に処理を入れることができます
テストコード内にTestMainの関数が含まれている場合、テストやベンチマークを直接実行する代わりにTestMain(m)を呼び出し、TestMainはmain goroutineで実行されます
m.Runはos.Exitに渡すことができる終了コードを返します

以下サンプルコード


func TestAdd(t *testing.T){
	// omit
}
func TestMain(m *testing.M) {
	setup()
	status := m.Run()

	os.Exit(status)
}

func setup() {
	// DB setupなど
	fmt.Println("setup")
}

また、後処理には以下の関数が便利です
func (c *T) Cleanup(f func())
https://pkg.go.dev/testing#T.Cleanup

t.Cleanupはテストが完了したタイミングで呼び出されるため、前処理などでファイルを作成しており、削除する処理を入れたい場合などに有用です
deferは、関数からreturnしたタイミングで呼び出されるため下記のような記述は行うことができず、t.Cleanupを利用した方が記述を簡略化できます

func setup(t *testing.T) string {
	content := []byte("前準備")
	file, err := ioutil.TempFile("", "test")
	if err != nil {
		t.Error(err)
	}
	t.Cleanup(func() { os.Remove(file.Name()) })

	if err = ioutil.WriteFile(file.Name(), content, 0644); err != nil {
		t.Error(err)
	}
	return file.Name()
}

テストのスキップ

準備中...

テストの実行方法

一部のテストケース

準備中...

一部のサブテスト

準備中...

ディレクトリ配下

準備中...

ヘルパー関数

準備中...

カバレッジ取得

準備中...

参考

https://trap.jp/post/1402/
https://future-architect.github.io/articles/20200601/

Discussion

ログインするとコメントできます