目次
目次
testing パッケージについて
testing package は Go パッケージのテストを提供します。
$ gocloc --not-match=test bytes
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Go                              15            358            673           2514
-------------------------------------------------------------------------------
TOTAL                           15            358            673           2514
-------------------------------------------------------------------------------Common (Log / Skip / Error)
common構造体はT(Testing),F(Fazzing),B(Benchmark)に含まれており、共通のメソッドが実装されています。
type T struct {
	common
	...
}
type B struct {
	common
	...
}
type F struct {
	common
}Error
Error、Fatal を呼ぶことでテストの失敗を伝えることが出来ます。
これらのメソッドの違いは次の表の通りです。
| 関数名 | 内部で呼ばれる関数 | 挙動 | 
|---|---|---|
| func (c *T) Error(args …any) | Fail | 実行中のテストが失敗したことをマークしますが、実行は継続されます | 
| func (c *T) Errorf(format string, args …any) | Fail | Error と同じですが、ログの出力に Sprintf を利用します | 
| func (c *T) Fatal(args …any) | FailNow | 実行中のテストのゴルーチンを停止します。他のゴルーチンには影響がありません。 | 
| func (c *T) Fatalf(format string, args …any) | FailNow | Fatal と同じですが、ログの出力に Sprintf を利用します | 
c.failed = trueに変更し、関数が失敗したことをマークしますが、実行は継続されます。
func (c *common) Fail() {
	if c.parent != nil {
		c.parent.Fail()
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.done {
		panic("Fail in goroutine after " + c.name + " has completed")
	}
	c.failed = true
}runtime.Goexitは実行中のテストのゴルーチンを終了させます。そのため、他のゴルーチンには影響がありません。
func (c *common) FailNow() {
	c.checkFuzzFn("FailNow")
	c.Fail()
	c.mu.Lock()
	c.finished = true
	c.mu.Unlock()
	runtime.Goexit()
}CleanUp
テストが完了したときに呼びされる関数を定義できます。
func TestCleanUp(t *testing.T) {
	t.Cleanup(func() {
		fmt.Println("clean up")
	})
	testCases := []struct {
		name string
	}{
		{name: "tanaka"},
		{name: "tarou"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			fmt.Println(tc.name)
		})
	}
}
// tanaka
// tarou
// clean upSkip
skipped,finishedをtrueにマークし、runtime.Goexit()を呼ぶことで実行中のゴルーチンを中止します。
func (c *common) SkipNow() {
	c.checkFuzzFn("SkipNow")
	c.mu.Lock()
	c.skipped = true
	c.finished = true
	c.mu.Unlock()
	runtime.Goexit()
}func (c *T) Skipf(format string, args …any)
Skipと処理は同じですが、ログの出力にSprintfを利用します。
Log
テストが失敗したときにログを出力できます。
func TestLog(t *testing.T) {
	t.Logf("Start: %s", t.Name())
	t.Log("End")
	t.Fail()
}
// Start: TestLog
// Endベンチマーク
n 回実行しパフォーマンスを測定します。
- コア数 -8
- 実行回数 78332391
- 実行時間 14.33 ns/op
- 実行あたりのヒープ領域に割り当てられた累積バイト数 8 B/op
- ヒープオブジェクトが割り当てられた累積数 1 allocs/op
func BenchmarkExp(b *testing.B) {
	x := big.NewInt(1)
	y := big.NewInt(64 + 1)
	for i := 0; i < b.N; i++ {
		new(big.Int).Exp(x, y, nil)
	}
}
// BenchmarkExp-8   	78332391	        14.33 ns/op	       8 B/op	       1 allocs/opStartTimer, StopTimer
この関数はベンチマークが始まる前に自動的に呼び出されます。
現在時刻、ヒープ領域に割り当てられた累積バイト数(TotalAlloc)、ヒープオブジェクトが割り当てられた累積数(mallocs)を格納します。
func (b *B) StartTimer() {
	if !b.timerOn {
		runtime.ReadMemStats(&memStats)
		b.startAllocs = memStats.Mallocs
		b.startBytes = memStats.TotalAlloc
		b.start = time.Now()
		b.timerOn = true
	}
}テスト開始前との差分を格納します。
func (b *B) StopTimer() {
	if b.timerOn {
		b.duration += time.Since(b.start)
		runtime.ReadMemStats(&memStats)
		b.netAllocs += memStats.Mallocs - b.startAllocs
		b.netBytes += memStats.TotalAlloc - b.startBytes
		b.timerOn = false
	}
}Elapsed
func (b *B) Elapsed() time.Duration
ベンチマークの経過時間を取得できます。
func BenchmarkElapsed(b *testing.B) {
	x := big.NewInt(1)
	y := big.NewInt(64 + 1)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		new(big.Int).Exp(x, y, nil)
	}
	b.StopTimer()
	e := b.Elapsed()
	fmt.Println("Elapsed:", e)
}
// Elapsed: 6.083µs
// Elapsed: 147.083µs
// Elapsed: 15.048292ms
// Elapsed: 1.168364792sファジング
fuzzing とはランダムに生成された入力で関数を呼び出し、ユニットテストで予想されないバグを発見するテスト技法です。
境界値テストやエッジケースの検証に有効です。
関数名はFuzzXxxである必要があります。
Go Fuzzing(Go Blog)で詳しく解説されています。
シードコーパス
Addメソッドでシードコーパスを指定できます。
テストコーパス(テストケースのコレクション) https://ja.wikipedia.org/wiki/ファジング から引用
引数は次のタイプのみ許可しています。
supportedTypes = map[reflect.Type]bool
var supportedTypes = map[reflect.Type]bool{
	reflect.TypeOf(([]byte)("")):  true,
	reflect.TypeOf((string)("")):  true,
	reflect.TypeOf((bool)(false)): true,
	reflect.TypeOf((byte)(0)):     true,
	reflect.TypeOf((rune)(0)):     true,
	reflect.TypeOf((float32)(0)):  true,
	reflect.TypeOf((float64)(0)):  true,
	reflect.TypeOf((int)(0)):      true,
	reflect.TypeOf((int8)(0)):     true,
	reflect.TypeOf((int16)(0)):    true,
	reflect.TypeOf((int32)(0)):    true,
	reflect.TypeOf((int64)(0)):    true,
	reflect.TypeOf((uint)(0)):     true,
	reflect.TypeOf((uint8)(0)):    true,
	reflect.TypeOf((uint16)(0)):   true,
	reflect.TypeOf((uint32)(0)):   true,
	reflect.TypeOf((uint64)(0)):   true,
}testing.F
go test example_test.go -fuzz=Fuzz -v -fuzztime=10sのコマンドで実行できます。fuzztimeにタイムアウトする時間を指定できます。
func Reverse(s string) (string, error) {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r), nil
}
func FuzzReverse(f *testing.F) {
	testcases := []string{"Hello, world", " ", "!12345"}
	for _, tc := range testcases {
		f.Add(tc) // Use f.Add to provide a seed corpus
	}
	f.Fuzz(func(t *testing.T, orig string) {
		rev, err1 := Reverse(orig)
		if err1 != nil {
			return
		}
		doubleRev, err2 := Reverse(rev)
		if err2 != nil {
			return
		}
		if orig != doubleRev {
			t.Errorf("Before: %q, after: %q", orig, doubleRev)
		}
		if utf8.ValidString(orig) && !utf8.ValidString(rev) {
			t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
		}
	})
}テストに失敗した場合、テストファイルのあるディレクトリから./testdata/fuzz/FuzzXxx/seedにファイルが生成され、失敗したケースの args が出力されます。
今回のケースではutf8.ValidStringで失敗したケースの文字列が出力されます。
go test fuzz v1
string("Ή")アロケーション回数の測定
func AllocsPerRun(runs int, f func()) (avg float64)
AllocsPerRunは、1 実行あたりのアロケーション(メモリー領域の確保)回数を測定します。
Warm up として1度実行し、 runtime.MemStats でメモリ情報を取得、関数実行後に mallocs の差分を計算します。
runs 引数で渡した回数を実行後に割り平均を返します。
new 関数は新しくメモリを割り当てるため出力は 1 になります。
var global any
func TestAllocsPerRun(t *testing.T) {
	allocs := testing.AllocsPerRun(100, func() {
		global = new(*byte)
	})
	fmt.Println(allocs)
}
// 1