目次
目次
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 up
Skip
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/op
StartTimer, 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