Skip to content

Go testingパッケージ Walkthrough

2023/03/04

Go

目次

目次

  1. testing パッケージについて
  2. Common (Log / Skip / Error)
    1. Error
    2. CleanUp
    3. Skip
    4. Log
  3. ベンチマーク
    1. StartTimer, StopTimer
    2. Elapsed
  4. ファジング
    1. シードコーパス
    2. testing.F
  5. アロケーション回数の測定

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

type B struct

type F struct

type T struct {
	common
	...
}

type B struct {
	common
	...
}

type F struct {
	common
}

Error

ErrorFatal を呼ぶことでテストの失敗を伝えることが出来ます。
これらのメソッドの違いは次の表の通りです。

関数名内部で呼ばれる関数挙動
func (c *T) Error(args …any)Fail実行中のテストが失敗したことをマークしますが、実行は継続されます
func (c *T) Errorf(format string, args …any)FailError と同じですが、ログの出力に Sprintf を利用します
func (c *T) Fatal(args …any)FailNow実行中のテストのゴルーチンを停止します。他のゴルーチンには影響がありません。
func (c *T) Fatalf(format string, args …any)FailNowFatal と同じですが、ログの出力に Sprintf を利用します

func (c *T) Fail()

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
}

func (c *T) FailNow()

runtime.Goexitは実行中のテストのゴルーチンを終了させます。そのため、他のゴルーチンには影響がありません。

func (c *common) FailNow() {
	c.checkFuzzFn("FailNow")
	c.Fail()

	c.mu.Lock()
	c.finished = true
	c.mu.Unlock()
	runtime.Goexit()
}

CleanUp

func (c *B) Cleanup(f func())

テストが完了したときに呼びされる関数を定義できます。

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 up

Skip

func (c *T) Skip(args …any)

skipped,finishedtrueにマークし、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

ベンチマーク

type B struct

n 回実行しパフォーマンスを測定します。

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/op

StartTimer, StopTimer

func (b *B) StartTimer()

この関数はベンチマークが始まる前に自動的に呼び出されます。

現在時刻、ヒープ領域に割り当てられた累積バイト数(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()

テスト開始前との差分を格納します。

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)で詳しく解説されています。

シードコーパス

func (f *F) Add(args …any)

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