Skip to content

テスト

開発者にとって、良好なテストはプログラム中のエラーを事前に発見し、メンテナンス不及时による Bug 発生による精神的負担を回避できるため、テストを適切に記述することは非常に重要です。Go はテストに関して非常に実用的なコマンドラインツール go test を提供しており、標準ライブラリや多くのオープンソースフレームワークでもテストの姿を見ることができます。このツールは非常に使いやすく、現在以下の種類のテストをサポートしています。

  • 例示テスト
  • ユニットテスト
  • ベンチマークテスト
  • ファジングテスト

Go では、大部分の API は標準ライブラリ testing によって提供されます。

TIP

コマンドラインで go help testfunc コマンドを実行すると、上記 4 つのテストタイプに関する Go 公式の説明を参照できます。

記述規範

テスト記述を開始する前に、いくつかの規範に注意する必要があります。これにより、後続の学習がより便利になります。

  • テストパッケージ:テストファイルは個別のパッケージに配置することをお勧めします。このパッケージは通常 test と命名されます。
  • テストファイル:テストファイルは通常 _test.go で終わります。例えば、ある機能をテストする場合は function_test.go と命名します。テストタイプによってさらに細かく分類したい場合は、テストタイプをファイルプレフィックスとして使用することもできます。例えば benchmark_marshaling_test.goexample_marshaling_test.go などです。
  • テスト関数:各テストファイルには、異なるテストに使用されるいくつかのテスト関数があります。異なるテストタイプに対して、テスト関数の命名スタイルも異なります。例えば、例示テストは ExampleXXXX、ユニットテストは TestXXXX、ベンチマークテストは BenchmarkXXXX、ファジングテストは FuzzXXXX です。これにより、コメントがなくてもどのようなタイプのテストかがわかります。

TIP

パッケージ名が testdata の場合、そのパッケージは通常テスト用の補助データを保存するためのもので、テスト実行時に Go は testdata という名前のパッケージを無視します。

上記の規範に従い、良好なテストスタイルを養成することで、日々のメンテナンスの手間を大幅に削減できます。

テスト実行

テスト実行は主に go test コマンドを使用します。以下は実際のコード例です。現在、テスト対象ファイル /say/hello.go があります。コードは以下の通りです。

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

およびテストファイル /test/example_test.go コードは以下の通りです。

go
package test

import (
  "golearn/say"
)

func ExampleHello() {
  say.Hello()
  // Output:
  // hello
}

func ExampleGoodBye() {
  say.GoodBye()
  // Output:
  // bye
}

func ExampleSay() {
  say.Hello()
  say.GoodBye()
  // Output:
  // hello
  // bye
}

これらのテストを実行するにはいくつかの方法があります。例えば、test パッケージ下のすべてのテストケースを実行したい場合は、test ディレクトリで以下のコマンドを実行するだけです。

sh
$ go test ./
PASS
ok      golearn/test    0.422s

./ は現在のディレクトリを示します。Go は test ディレクトリ下のすべてのテストファイルを再コンパイルした後、すべてのテストケースをすべて実行します。結果から、すべてのテストケースに合格したことがわかります。その後のパラメータには複数のディレクトリを指定することもできます。例えば、以下のコマンドでは、プロジェクトのメインディレクトリには明らかに実行可能なテストファイルがありません。

sh
$ go test ./ ../
ok      golearn/test
?       golearn [no test files]

TIP

実行パラメータに複数のパッケージがある場合、Go はすでに成功したテストケースを再実行しません。実行時に行末に (cached) を追加して、出力結果が前回のキャッシュであることを示します。テストフラグパラメータが以下のセットにある場合、Go はテスト結果をキャッシュします。そうでない場合はキャッシュしません。

-benchtime, -cpu,-list, -parallel, -run, -short, -timeout, -failfast, -v

キャッシュを無効にしたい場合は、-count=1 パラメータを追加します。

もちろん、特定のテストファイルを個別に指定して実行することもできます。

sh
$ go test example_test.go
ok      command-line-arguments  0.457s

または、特定のテストファイルの特定のテストケースを個別に指定することもできます。例えば

sh
$ go test -run ExampleSay
PASS
ok      golearn/test    0.038s

上記の 3 つの状況はすべてテストを完了しましたが、出力結果が簡潔すぎるため、-v パラメータを追加して出力結果をより詳細にできます。例えば

sh
$ go test ./ -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN   ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok      golearn/test    0.040s

これで、各テストケースの実行順序、所要時間、実行状況、および全体の所要時間を明確に確認できます。

TIP

go test コマンドはデフォルトですべてのユニットテスト、例示テスト、ファジングテストを実行します。-bench パラメータを追加すると、すべてのタイプのテストが実行されます。例えば、以下のコマンドです。

sh
$ go test -bench .

したがって、-run パラメータを使用して指定する必要があります。例えば、すべてのベンチマークテストのみを実行するコマンドは以下の通りです。

sh
$ go test -bench . -run ^$

よく使用されるパラメータ

Go テストには非常に多くのフラグパラメータがあります。以下ではよく使用されるパラメータのみを紹介します。より多くの詳細を知りたい場合は、go help testflag コマンドを使用して自行で参照することをお勧めします。

パラメータ説明
-o fileコンパイル後のバイナリファイル名を指定
-cテストファイルのみをコンパイルし、実行しない
-jsonテストログを json 形式で出力
-exec xprogxprog を使用してテストを実行。go run と同等
-bench regexpregexp に一致するベンチマークテストを選択
-fuzz regexpregexp に一致するファジングテストを選択
-fuzztime tファジングテストが自動的に終了する時間。t は時間間隔。単位が x の場合、回数を示す。例えば 200x
-fuzzminimizetime tモードテストが実行される最小時間。規則は上記と同じ
-count nテストを n 回実行。デフォルトは 1 回
-coverテストカバレッジ分析を有効化
-covermode set,count,atomicカバレッジ分析のモードを設定
-cpuテスト実行のために GOMAXPROCS を実行
-failfast最初のテスト失敗後、新しいテストを開始しない
-list regexpregexp に一致するテストケースを一覧表示
-parallel nt.Parallel を呼び出したテストケースの並列実行を許可。n 値は並列実行の最大数
-run regexpregexp に一致するテストケースのみを実行
-skip regexpregexp に一致するテストケースをスキップ
-timeout d単一テストの実行時間が時間間隔 d を超えた場合、panic します。d は時間間隔。例:1s,1ms,1ns など
-shuffle off,on,Nテストの実行順序をシャッフル。N はランダムシード。デフォルトシードはシステム時間
-vより詳細なテストログを出力
-benchmemベンチマークテストのメモリ割り当てを統計
-blockprofile block.outテスト中のゴルーチンブロッキング状況を統計し、ファイルに書き込み
-blockprofilerate nゴルーチンブロッキング統計頻度を制御。詳細は go doc runtime.SetBlockProfileRate コマンドを参照
-coverprofile cover.outカバレッジテストの状況を統計し、ファイルに書き込み
-cpuprofile cpu.outCPU 状況を統計し、ファイルに書き込み
-memprofile mem.outメモリ割り当て状況を統計し、ファイルに書き込み
-memprofilerate nメモリ割り当て統計の頻度を制御。詳細は go doc runtime.MemProfileRate コマンドを参照
-mutexprofile mutex.outロック競合状況を統計し、ファイルに書き込み
-mutexprofilefraction nn 個のゴルーチンが 1 つの相互排他ロックを競合する状況を統計
-trace trace.out実行追跡状況をファイルに書き込み
-outputdir directory上記の統計ファイルの出力ディレクトリを指定。デフォルトは go test の実行ディレクトリ

例示テスト

例示テストは、他の 3 つのテストがプログラムの問題点を発見するためのものとは異なり、更多的是特定の機能の使用方法を示し、ドキュメントとしての役割を果たします。例示テストは公式に定義された概念ではなく、強制的な規範でもなく、むしろエンジニアリング上の慣習のようなもので、遵守するかどうかは開発者次第です。例示テストは標準ライブラリで非常に多く見られ、通常は公式が記述した標準ライブラリコードの例です。例えば、標準ライブラリ context/example_test.goExampleWithDeadline テスト関数は、DeadlineContext の基本的な使用方法を示しています。

go
// This example passes a context with an arbitrary deadline to tell a blocking
// function that it should abandon its work as soon as it gets to it.
func ExampleWithDeadline() {
   d := time.Now().Add(shortDuration)
   ctx, cancel := context.WithDeadline(context.Background(), d)

   // Even though ctx will be expired, it is good practice to call its
   // cancellation function in any case. Failure to do so may keep the
   // context and its parent alive longer than necessary.
   defer cancel()

   select {
   case <-time.After(1 * time.Second):
      fmt.Println("overslept")
   case <-ctx.Done():
      fmt.Println(ctx.Err())
   }

   // Output:
   // context deadline exceeded
}

表面的には、このテスト関数は普通の関数のようですが、例示テストは主に Output コメントによって体现されます。テスト対象関数が 1 行の出力のみの場合、Output コメントを使用して出力を検証します。まず hello.go という名前のファイルを作成し、以下のコードを記述します。

go
package say

import "fmt"

func Hello() {
  fmt.Println("hello")
}

func GoodBye() {
  fmt.Println("bye")
}

SayHello 関数はテスト対象関数です。次に、テストファイル example_test.go を作成し、以下のコードを記述します。

go
package test

import (
  "golearn/say"
)

func ExampleHello() {
  say.Hello()
  // Output:
  // hello
}

func ExampleGoodBye() {
  say.GoodBye()
  // Output:
  // bye
}

func ExampleSay() {
  say.Hello()
  say.GoodBye()
  // Output:
  // hello
  // bye
}

関数内の Output コメントは、関数の出力が hello かどうかを検証することを示しています。次に、テストコマンドを実行して結果を確認します。

sh
$ go test -v
=== RUN   ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN   ExampleGoodBye
--- PASS: ExampleGoodBye (0.00s)
=== RUN   ExampleSay
--- PASS: ExampleSay (0.00s)
PASS
ok      golearn/test    0.448s

結果から、すべてのテストに合格したことがわかります。Output にはいくつかの書き方があります。1 つ目は 1 行の出力のみで、この関数の出力が hello かどうかを検証することを意味します。

// Output:
// hello

2 つ目は複数行の出力で、順序通りに出力が一致するかどうかを検証します。

// Output:
// hello
// bye

3 つ目は順序なし出力で、順序を気にせずに複数行の出力が一致するかどうかを検証します。

// Unordered output:
// bye
// hello

テスト関数に関して、最後の数行が Output コメントである場合にのみ例示テストと見なされ、そうでない場合は普通の関数にすぎず、Go によって実行されないことに注意する必要があります。

ユニットテスト

ユニットテストは、ソフトウェア中の最小テスト可能ユニットをテストするものです。ユニットのサイズの定義は開発者次第で、構造体、パッケージ、関数、またはタイプである可能性があります。以下も例を通じて説明します。まず /tool/math.go ファイルを作成し、以下のコードを記述します。

go
package tool

type Number interface {
  ~int8 | ~int16 | ~int32 | ~int64 | ~int
}

func SumInt[T Number](a, b T) T {
  return a + b
}

func Equal[T Number](a, b T) bool {
  return a == b
}

次に、テストファイル /tool_test/unit_test.go を作成します。ユニットテストの場合、命名は unit_test またはテストしたいパッケージまたは機能をファイルプレフィックスとして使用できます。

go
package test_test

import (
  "golearn/tool"
  "testing"
)

func TestSum(t *testing.T) {
  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }
}

func TestEqual(t *testing.T) {
  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Errorf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

ユニットテストの場合、各テストケースの命名スタイルは TestXXXX で、関数の引数は t *testing.T である必要があります。testing.Ttesting パッケージが提供するテストを容易にするための構造体で、多くの使用可能なメソッドを提供しています。例の t.Errorft.Logf と同等で、テスト失敗のログ情報をフォーマット出力するために使用されます。他のよく使用されるものには t.Fail があり、現在のケースをテスト失敗としてマークするために使用されます。同様の機能には t.FailNow があり、これもテスト失敗としてマークしますが、前者は失敗後も実行を継続し、後者は直接実行を停止します。以下の例では、予想結果を誤った結果に修正します。

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func TestSum(t *testing.T) {
  a, b := 10, 101
  expected := 110

  actual := tool.SumInt(a, b)
  if actual != expected {
        // Errorf 内部では t.Fail() を使用
    t.Errorf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }
  t.Log("test finished")
}

func TestEqual(t *testing.T) {
  a, b := 10, 101
  expected := true

  actual := tool.Equal(a, b)
  if actual != expected {
        // Fatalf 内部では t.FailNow() を使用
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
  t.Log("test finished")
}

上記のテストを実行すると、出力は以下の通りです。

sh
$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
    tool_test.go:16: test finished
--- FAIL: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL    command-line-arguments  0.037s

テストログから、TestSum ケースは失敗しても test finished を出力しますが、TestEqual は出力しないことがわかります。同様に t.SkipNow があり、現在のケースを SKIP としてマークし、実行を停止します。次のラウンドのテストでは继续して実行されます。

go
package tool_test

import (
   "golearn/tool"
   "testing"
)

func TestSum(t *testing.T) {
   a, b := 10, 101
   expected := 110

   actual := tool.SumInt(a, b)
   if actual != expected {
      t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
   }
   t.Log("test finished")
}

func TestEqual(t *testing.T) {
   a, b := 10, 101
   expected := true

   actual := tool.Equal(a, b)
   if actual != expected {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
   }
   t.Log("test finished")
}

テスト実行時に、テスト回数を 2 に修正します。

go
$ go test tool_test.go -v -count=2
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
=== RUN   TestSum
    tool_test.go:14: Sum(10,101) expected 110,actual is 111
--- SKIP: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:25: Sum(10,101) expected true,actual is false
--- FAIL: TestEqual (0.00s)
FAIL    command-line-arguments  0.468s

上記の例では、最後の行に test finished を出力してテスト完了を示していますが、実際には t.Cleanup を使用してクリーンアップ関数を登録してこれを行うことができます。この関数はテストケースの終了時に実行されます。以下の通りです。

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func finished(t *testing.T) {
  t.Log("test finished")
}

func TestSum(t *testing.T) {
  t.Cleanup(func() {
    finished(t)
  })

  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }

}

func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    finished(t)
  })

  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

テスト実行後の出力は以下の通りです。

$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:9: test finished
--- PASS: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:9: test finished
--- PASS: TestEqual (0.00s)
PASS
ok      command-line-arguments  0.462s

Helper

t.Helper() を使用して現在の関数をヘルパー関数としてマークできます。ヘルパー関数は単独のテストケースとして実行されず、ログを記録する際に出力される行番号もヘルパー関数の呼び出し元の行番号になるため、ログ分析時の位置特定がより正確になり、冗長な他の情報を回避できます。例えば、上記の t.Cleanup の例はヘルパー関数に修正できます。以下の通りです。

go
package tool_test

import (
   "golearn/tool"
   "testing"
)

func CleanupHelper(t *testing.T) {
   t.Helper()
   t.Log("test finished")
}

func TestSum(t *testing.T) {
   t.Cleanup(func() {
      CleanupHelper(t)
   })

   a, b := 10, 101
   expected := 111

   actual := tool.SumInt(a, b)
   if actual != expected {
      t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
   }

}

func TestEqual(t *testing.T) {
   t.Cleanup(func() {
      CleanupHelper(t)
   })

   a, b := 10, 101
   expected := false

   t.Helper()
   actual := tool.Equal(a, b)
   if actual != expected {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
   }
}

テスト実行後の出力情報は以下の通りです。以前との違いは、test finished の行番号が呼び出し元の行番号になったことです。

$ go test tool_test.go -v
=== RUN   TestSum
    tool_test.go:15: test finished
--- PASS: TestSum (0.00s)
=== RUN   TestEqual
    tool_test.go:30: test finished
--- PASS: TestEqual (0.00s)
PASS
ok      command-line-arguments  0.464s

TIP

上記の操作はメインテストでのみ実行可能で、直接実行されるテストケースです。サブテストで使用すると panic します。

サブテスト

一部の状況では、1 つのテストケース内で別のテストケースをテストする必要がある場合があります。このようなネストされたテストケースは一般にサブテストと呼ばれます。t.Run() メソッドを使用します。このメソッドのシグネチャは以下の通りです。

go
// Run メソッドはサブテストを実行するために新しいゴルーチンを開始し、関数 f の実行が完了するのを待ってから戻ります
// 戻り値はテストに合格したかどうか
func (t *T) Run(name string, f func(t *T)) bool

以下は例です。

go
func TestTool(t *testing.T) {
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
}

実行後の結果は以下の通りです。

sh
$ go test -run TestTool -v
=== RUN   TestTool
=== RUN   TestTool/tool.Sum(10,101)
    tool_test.go:15: test finished
=== RUN   TestTool/tool.Equal(10,101)
    tool_test.go:30: test finished
--- PASS: TestTool (0.00s)
    --- PASS: TestTool/tool.Sum(10,101) (0.00s)
    --- PASS: TestTool/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.449s

出力を通じて、親子の階層構造がはっきりとわかります。上記の例では、最初のサブテストが完了するまで 2 番目のサブテストは実行されません。t.Parallel() を使用してテストケースを並列実行可能としてマークできます。これにより、出力順序は不定になります。

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

func CleanupHelper(t *testing.T) {
  t.Helper()
  t.Log("test finished")
}

func TestSum(t *testing.T) {
  t.Parallel()
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := 111

  actual := tool.SumInt(a, b)
  if actual != expected {
    t.Skipf("Sum(%d,%d) expected %d,actual is %d", a, b, expected, actual)
  }

}

func TestEqual(t *testing.T) {
  t.Parallel()
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := false

  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

func TestToolParallel(t *testing.T) {
  t.Log("setup")
  t.Run("tool.Sum(10,101)", TestSum)
  t.Run("tool.Equal(10,101)", TestEqual)
  t.Log("teardown")
}

テスト実行後の出力は以下の通りです。

$ go test -run TestTool -v
=== RUN   TestToolParallel
    tool_test.go:46: setup
=== RUN   TestToolParallel/tool.Sum(10,101)
=== PAUSE TestToolParallel/tool.Sum(10,101)
=== RUN   TestToolParallel/tool.Equal(10,101)
=== PAUSE TestToolParallel/tool.Equal(10,101)
=== NAME  TestToolParallel
    tool_test.go:49: teardown
=== CONT  TestToolParallel/tool.Sum(10,101)
=== CONT  TestToolParallel/tool.Equal(10,101)
=== NAME  TestToolParallel/tool.Sum(10,101)
    tool_test.go:16: test finished
=== NAME  TestToolParallel/tool.Equal(10,101)
    tool_test.go:32: test finished
--- PASS: TestToolParallel (0.00s)
    --- PASS: TestToolParallel/tool.Sum(10,101) (0.00s)
    --- PASS: TestToolParallel/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.444s

テスト結果から、ブロッキング待機プロセスがあることがはっきりとわかります。テストケースを並列実行する際、上記の例は明らかに正常に実行できません。後続のコードが同期実行されることが保証されないためです。この場合、t.Run() をもう 1 つネストすることを選択できます。以下の通りです。

go
func TestToolParallel(t *testing.T) {
  t.Log("setup")
  t.Run("process", func(t *testing.T) {
    t.Run("tool.Sum(10,101)", TestSum)
    t.Run("tool.Equal(10,101)", TestEqual)
  })
  t.Log("teardown")
}

再度実行すると、正常な実行結果が確認できます。

$ go test -run TestTool -v
=== RUN   TestToolParallel
    tool_test.go:46: setup
=== RUN   TestToolParallel/process
=== RUN   TestToolParallel/process/tool.Sum(10,101)
=== PAUSE TestToolParallel/process/tool.Sum(10,101)
=== RUN   TestToolParallel/process/tool.Equal(10,101)
=== PAUSE TestToolParallel/process/tool.Equal(10,101)
=== CONT  TestToolParallel/process/tool.Sum(10,101)
=== CONT  TestToolParallel/process/tool.Equal(10,101)
=== NAME  TestToolParallel/process/tool.Sum(10,101)
    tool_test.go:16: test finished
=== NAME  TestToolParallel/process/tool.Equal(10,101)
    tool_test.go:32: test finished
=== NAME  TestToolParallel
    tool_test.go:51: teardown
--- PASS: TestToolParallel (0.00s)
    --- PASS: TestToolParallel/process (0.00s)
        --- PASS: TestToolParallel/process/tool.Sum(10,101) (0.00s)
        --- PASS: TestToolParallel/process/tool.Equal(10,101) (0.00s)
PASS
ok      golearn/tool_test       0.450s

テーブルスタイル

上記のユニットテストでは、テストの入力データはすべて手動で宣言された個々の変数でした。データ量が少ない場合は問題ありませんが、複数のデータセットをテストしたい場合に、変数を宣言してテストデータを作成するのは現実的ではありません。したがって、一般的な状況では構造体スライスの形式を採用することが多く、構造体は一時宣言された匿名構造体です。このようなコーディングスタイルはテーブルのように見えるため、table-driven と呼ばれます。以下は例です。これは手動で複数の変数を宣言してテストデータを作成する例です。データが複数ある場合、直感的ではないため、テーブルスタイルに修正します。

go
func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  a, b := 10, 101
  expected := false
  actual := tool.Equal(a, b)
  if actual != expected {
    t.Fatalf("Sum(%d,%d) expected %t,actual is %t", a, b, expected, actual)
  }
}

修正後のコードは以下の通りです。

go
func TestEqual(t *testing.T) {
  t.Cleanup(func() {
    CleanupHelper(t)
  })

  // table driven style
  testData := []struct {
    a, b int
    exp  bool
  }{
    {10, 101, false},
    {5, 5, true},
    {30, 32, false},
    {100, 101, false},
    {2, 3, false},
    {4, 4, true},
  }

  for _, data := range testData {
    if actual := tool.Equal(data.a, data.b); actual != data.exp {
      t.Fatalf("Sum(%d,%d) expected %t,actual is %t", data.a, data.b, data.exp, actual)
    }
  }
}

このようにすると、テストデータがはるかに直感的になります。

ベンチマークテスト

ベンチマークテストはパフォーマンステストとも呼ばれ、通常はプログラムのメモリ占有、CPU 使用状況、実行時間などのパフォーマンス指標をテストするために使用されます。ベンチマークテストの場合、テストファイルは通常 bench_test.go で終わり、テストケースの関数は BenchmarkXXXX 形式である必要があります。

以下は、文字列連結の例のパフォーマンス比較をベンチマークテストの例として使用します。まず /tool/strConcat.go ファイルを作成します。周知の通り、文字列を直接使用して + 連結するのはパフォーマンスが非常に低く、strings.Builder を使用するのははるかに良いです。/tool/strings.go ファイルで 2 つの関数を作成して、2 つの方法の文字列連結を実行します。

go
package tool

import "strings"


func ConcatStringDirect(longString string) {
   res := ""
   for i := 0; i < 100_000.; i++ {
      res += longString
   }
}

func ConcatStringWithBuilder(longString string) {
   var res strings.Builder
   for i := 0; i < 100_000.; i++ {
      res.WriteString(longString)
   }
}

次に、テストファイル /tool_test/bench_tool_test.go を作成します。コードは以下の通りです。

go
package tool_test

import (
  "golearn/tool"
  "testing"
)

var longString = "longStringlongStringlongStringlongStringlongStringlongStringlongStringlongString"

func BenchmarkConcatDirect(b *testing.B) {
  for i := 0; i < b.N; i++ {
    tool.ConcatStringDirect(longString)
  }
}

func BenchmarkConcatBuilder(b *testing.B) {
  for i := 0; i < b.N; i++ {
    tool.ConcatStringWithBuilder(longString)
  }
}

テストコマンドを実行します。コマンドでは詳細ログとメモリ分析を有効化し、使用する CPU コア数リストを指定し、各テストケースを 2 回実行します。出力は以下の通りです。

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=2
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         277771375 ns/op        4040056736 B/op    10000 allocs/op
BenchmarkConcatDirect-2                4         278500125 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-4                1        1153796000 ns/op        4040068784 B/op    10126 allocs/op
BenchmarkConcatDirect-4                1        1211017600 ns/op        4040073104 B/op    10171 allocs/op
BenchmarkConcatDirect-8                2         665460800 ns/op        4040077760 B/op    10219 allocs/op
BenchmarkConcatDirect-8                2         679774450 ns/op        4040080064 B/op    10243 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            3428            344530 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3579            351858 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4            2448            736177 ns/op         4128185 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1688            662993 ns/op         4128185 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1958            550333 ns/op         4128199 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2174            552113 ns/op         4128196 B/op         29 allocs/op
PASS
ok      golearn/tool_test       21.381s

以下は、ベンチマークテストの出力結果の説明です。goos は実行中のオペレーティングシステムを表し、goarh は CPU アーキテクチャを表し、pkg はテストが所在するパッケージを表し、cpu は CPU に関するいくつかの情報です。以下の各テストケースの結果は、各ベンチマークテストの名前で区切られます。最初の列 BenchmarkConcatDirect-2 の 2 は使用する CPU コア数を表し、2 番目の列の 4 はコード内の b.N のサイズ、つまりベンチマークテスト内のループ回数を表し、3 番目の列 277771375 ns/op は各ループに消費される時間を表し、ns はナノ秒です。4 番目の列 4040056736 B/op は各ループで割り当てられるメモリのバイトサイズを表し、5 番目の列 10000 allocs/op は各ループのメモリ割り当て回数を表します。

明らかに、テスト結果によると、strings.Builder を使用するパフォーマンスは + を使用して文字列を連結するよりもはるかに高いです。直感的なデータ比較を通じてパフォーマンスを比較するのがベンチマークテストの目的です。

benchstat

benchstat はオープンソースのパフォーマンステスト分析ツールです。上記のパフォーマンステストのサンプル数は 2 グループのみで、サンプルが多くなると人工分析は非常に時間と労力がかかります。このツールはパフォーマンステストの分析問題を解決するために生まれました。

まず、このツールをダウンロードする必要があります。

sh
$ go install golang.org/x/perf/benchstat

ベンチマークテストを 2 回実行します。今回はサンプル数を 5 に修正し、それぞれ old.txtnew.txt ファイルに出力して比較します。1 回目の実行結果

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a old.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         290535650 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         298974625 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         299637800 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         276487000 ns/op        4040056784 B/op    10001 allocs/op
BenchmarkConcatDirect-2                4         356465275 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-4                2         894723200 ns/op        4040077424 B/op    10216 allocs/op
BenchmarkConcatDirect-4                2         785830400 ns/op        4040078288 B/op    10225 allocs/op
BenchmarkConcatDirect-4                2         743634000 ns/op        4040077568 B/op    10217 allocs/op
BenchmarkConcatDirect-4                2         953802700 ns/op        4040075408 B/op    10195 allocs/op
BenchmarkConcatDirect-4                2         953028750 ns/op        4040077520 B/op    10217 allocs/op
BenchmarkConcatDirect-8                2         684023150 ns/op        4040086784 B/op    10313 allocs/op
BenchmarkConcatDirect-8                2         634380250 ns/op        4040090528 B/op    10352 allocs/op
BenchmarkConcatDirect-8                2         685030600 ns/op        4040090768 B/op    10355 allocs/op
BenchmarkConcatDirect-8                2         817909650 ns/op        4040089808 B/op    10345 allocs/op
BenchmarkConcatDirect-8                2         600078100 ns/op        4040095664 B/op    10406 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            2925            419651 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2961            423899 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2714            422275 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2848            452255 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            2612            454452 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4             974           1158000 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1098           1068682 ns/op         4128192 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1042           1056570 ns/op         4128194 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1280            978213 ns/op         4128191 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1538           1162108 ns/op         4128190 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1744            700824 ns/op         4128203 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2235            759537 ns/op         4128201 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1556            736455 ns/op         4128204 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1592            825794 ns/op         4128201 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2263            717285 ns/op         4128203 B/op         29 allocs/op
PASS
ok      golearn/tool_test       56.742s

2 回目の実行結果

sh
$ go test -v -benchmem -bench . -run bench_tool_test.go -cpu=2,4,8 -count=5 | tee -a new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkConcatDirect
BenchmarkConcatDirect-2                4         285074900 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         291517150 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         281901975 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         292320625 ns/op        4040056592 B/op     9999 allocs/op
BenchmarkConcatDirect-2                4         286723000 ns/op        4040056952 B/op    10002 allocs/op
BenchmarkConcatDirect-4                1        1188983000 ns/op        4040071856 B/op    10158 allocs/op
BenchmarkConcatDirect-4                1        1080713900 ns/op        4040070800 B/op    10147 allocs/op
BenchmarkConcatDirect-4                1        1203622300 ns/op        4040067344 B/op    10111 allocs/op
BenchmarkConcatDirect-4                1        1045291300 ns/op        4040070224 B/op    10141 allocs/op
BenchmarkConcatDirect-4                1        1123163300 ns/op        4040070032 B/op    10139 allocs/op
BenchmarkConcatDirect-8                2         790421300 ns/op        4040076656 B/op    10208 allocs/op
BenchmarkConcatDirect-8                2         659047300 ns/op        4040079488 B/op    10237 allocs/op
BenchmarkConcatDirect-8                2         712991800 ns/op        4040077184 B/op    10213 allocs/op
BenchmarkConcatDirect-8                2         706605350 ns/op        4040078000 B/op    10222 allocs/op
BenchmarkConcatDirect-8                2         656195700 ns/op        4040085248 B/op    10297 allocs/op
BenchmarkConcatBuilder
BenchmarkConcatBuilder-2            2726            386412 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3439            335358 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3376            338957 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            3870            326301 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-2            4285            339596 ns/op         4128176 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1663            671535 ns/op         4128187 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1507            744885 ns/op         4128191 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1353           1097800 ns/op         4128187 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1388           1006019 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-4            1635            993764 ns/op         4128189 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1332            783599 ns/op         4128198 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1818            729821 ns/op         4128202 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1398            780614 ns/op         4128202 B/op         29 allocs/op
BenchmarkConcatBuilder-8            1526            750513 ns/op         4128204 B/op         29 allocs/op
BenchmarkConcatBuilder-8            2164            704798 ns/op         4128204 B/op         29 allocs/op
PASS
ok      golearn/tool_test       50.387s

benchstat を使用して比較します。

sh
$ benchstat old.txt new.txt
goos: windows
goarch: amd64
pkg: golearn/tool_test
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
    old.txt               new.txt
    sec/op    sec/op      vs base
ConcatDirect-2     299.0m ± ¹    286.7m ± ¹        ~ (p=0.310 n=5)
ConcatDirect-4     894.7m ± ¹   1123.2m ± ¹  +25.53% (p=0.008 n=5)
ConcatDirect-8     684.0m ± ¹    706.6m ± ¹        ~ (p=0.548 n=5)
ConcatBuilder-2    423.9µ ± ¹    339.0µ ± ¹  -20.04% (p=0.008 n=5)
ConcatBuilder-4   1068.7µ ± ¹    993.8µ ± ¹        ~ (p=0.151 n=5)
ConcatBuilder-8    736.5µ ± ¹    750.5µ ± ¹        ~ (p=0.841 n=5)
geomean            19.84m          19.65m         -0.98%
¹ need >= 6 samples for confidence interval at level 0.95

    old.txt                new.txt
     B/op     B/op       vs base
ConcatDirect-2    3.763Gi ± ¹   3.763Gi ± ¹       ~ (p=1.000 n=5)
ConcatDirect-4    3.763Gi ± ¹   3.763Gi ± ¹  -0.00% (p=0.008 n=5)
ConcatDirect-8    3.763Gi ± ¹   3.763Gi ± ¹  -0.00% (p=0.008 n=5)
ConcatBuilder-2   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-4   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=0.079 n=5)
ConcatBuilder-8   3.937Mi ± ¹   3.937Mi ± ¹       ~ (p=0.952 n=5)
geomean           123.2Mi         123.2Mi        -0.00%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

   old.txt               new.txt
  allocs/op  allocs/op    vs base
ConcatDirect-2    9.999k ± ¹   9.999k ± ¹       ~ (p=1.000 n=5)
ConcatDirect-4    10.22k ± ¹   10.14k ± ¹  -0.74% (p=0.008 n=5)
ConcatDirect-8    10.35k ± ¹   10.22k ± ¹  -1.26% (p=0.008 n=5)
ConcatBuilder-2    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-4    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
ConcatBuilder-8    29.00 ± ¹    29.00 ± ¹       ~ (p=1.000 n=5) ²
geomean            543.6          541.7        -0.33%
¹ need >= 6 samples for confidence interval at level 0.95
² all samples are equal

結果から、benchstat がこれを 3 つのグループに分割していることがわかります。それぞれ所要時間、メモリ占有、メモリ割り当て回数です。その中で geomean は平均値で、p はサンプルの有意水準で、臨界区間は通常 0.05 で、0.05 より高いとあまり信頼できません。その中の 1 つのデータを以下に示します。

          │    sec/op     │    sec/op      vs base               │
ConcatDirect-4     894.7m ± ∞ ¹   1123.2m ± ∞ ¹  +25.53% (p=0.008 n=5)

old の実行所要時間は 894.7ms で、new の実行所要時間は 1123.2ms で、比較すると 25.53% の所要時間が増加しています。

ファジングテスト

ファジングテストは GO1.18 で導入された新機能で、ユニットテストとベンチマークテストの一種の拡張です。違いは、前者 2 つのテストデータは開発者が手動で記述する必要があるのに対し、ファジングテストはコーパスを通じてランダムなテストデータを生成できることです。Go のファジングテストに関する詳細な概念は Go Fuzzing で参照できます。ファジングテストの利点は、固定されたテストデータに比べて、ランダムデータはプログラムの境界条件をよりよくテストできることです。以下は公式チュートリアルの例を使用して説明します。今回は文字列を反転する関数をテストします。まず /tool/strings.go ファイルを作成し、以下のコードを記述します。

go
package tool

func Reverse(s string) string {
  b := []byte(s)
  for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
    b[i], b[j] = b[j], b[i]
  }
  return string(b)
}

ファジングテストファイル /tool_test/fuzz_tool_test.go を作成し、以下のコードを記述します。

go
package tool

import (
  "golearn/tool"
  "testing"
  "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
  testdata := []string{"hello world!", "nice to meet you", "good bye!"}
  for _, data := range testdata {
    f.Add(data)
  }

  f.Fuzz(func(t *testing.T, str string) {
    first := tool.Reverse(str)
    second := tool.Reverse(first)
    t.Logf("str:%q,first:%q,second:%q", str, first, second)
    if str != second {
      t.Errorf("before: %q, after: %q", str, second)
    }
    if utf8.ValidString(str) && !utf8.ValidString(first) {
      t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
    }
  })
}

ファジングテストでは、まずコーパスシードライブラリにデータを追加する必要があります。例では f.Add() を使用して追加し、後続のランダムなテストデータの生成に役立てます。次に f.Fuzz(fn) を使用してテストを実行します。関数シグネチャは以下の通りです。

go
func (f *F) Fuzz(ff any)

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

fn はユニットテスト関数のロジックに似ており、関数の最初の引数は t *testing.T である必要があり、その後に生成したいパラメータが続きます。入力される文字列は予測できないため、ここでは 2 回反転する方法を使用して検証します。以下のコマンドを実行します。

sh
$ go test -run Fuzz -v
=== RUN   FuzzReverse
=== RUN   FuzzReverse/seed#0
    fuzz_tool_test.go:18: str:"hello world!",first:"!dlrow olleh",second:"hello world!"
=== RUN   FuzzReverse/seed#1
    fuzz_tool_test.go:18: str:"nice to meet you",first:"uoy teem ot ecin",second:"nice to meet you"
=== RUN   FuzzReverse/seed#2
    fuzz_tool_test.go:18: str:"good bye!",first:"!eyb doog",second:"good bye!"
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/seed#0 (0.00s)
    --- PASS: FuzzReverse/seed#1 (0.00s)
    --- PASS: FuzzReverse/seed#2 (0.00s)
PASS
ok      golearn/tool_test       0.539s

パラメータに -fuzz がない場合、ランダムなテストデータは生成されず、コーパスからのデータのみがテスト関数に渡されます。結果から、テストがすべて合格したことがわかりますが、これはユニットテストと同等ですが、実際には問題があります。以下に -fuzz パラメータを追加して再度実行します。

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/217 completed
fuzz: minimizing 91-byte failing input file
fuzz: elapsed: 0s, gathering baseline coverage: 15/217 completed
--- FAIL: FuzzReverse (0.13s)
    --- FAIL: FuzzReverse (0.00s)
        fuzz_tool_test.go:18: str:"𐑄",first:"\x84\x91\x90\xf0",second:"𐑄"
        fuzz_tool_test.go:23: Reverse produced invalid UTF-8 string "𐑄" "\x84\x91\x90\xf0"

    Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
    To re-run:
    go test -run=FuzzReverse/d856c981b6266ba2
=== NAME
FAIL
exit status 1
FAIL    golearn/tool_test       0.697s

TIP

ファジングテストで失敗したケースは、現在のテストフォルダ下の testdata ディレクトリ下の特定のコーパスファイルに出力されます。例えば、上記の例の

Failing input written to testdata\fuzz\FuzzReverse\d856c981b6266ba2
To re-run:
go test -run=FuzzReverse/d856c981b6266ba2

testdata\fuzz\FuzzReverse\d856c981b6266ba2 は出力されるコーパスファイルのパスで、ファイルの内容は以下の通りです。

go test fuzz v1
string("𐑄")

今回は合格しなかったことがわかります。理由は文字列が反転後に非 utf8 形式になったためです。したがって、ファジングテストを通じてこの問題点を発見できました。一部の文字は 1 バイト以上を占有するため、バイト単位で反転すると間違いなく文字化けします。したがって、テスト対象のソースコードを以下に修正し、文字列を []rune に変換することで、上記の問題を回避できます。

go
func Reverse(s string) string {
    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)
}

次に、前回のファジングテストで失敗したケースを直接実行します。

sh
$ go test -run=FuzzReverse/d856c981b6266ba2 -v
=== RUN   FuzzReverse
=== RUN   FuzzReverse/d856c981b6266ba2
    fuzz_tool_test.go:18: str:"𐑄",first:"𐑄",second:"𐑄"
--- PASS: FuzzReverse (0.00s)
    --- PASS: FuzzReverse/d856c981b6266ba2 (0.00s)
PASS
ok      golearn/tool_test       0.033s

今回はテストに合格したことがわかります。再度ファジングテストを実行して、まだ問題があるかどうかを確認します。

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: minimizing 70-byte failing input file
failure while testing seed corpus entry: FuzzReverse/d97214ce235bfcf5
fuzz: elapsed: 0s, gathering baseline coverage: 2/219 completed
--- FAIL: FuzzReverse (0.15s)
    --- FAIL: FuzzReverse (0.00s)
        fuzz_tool_test.go:18: str:"\xe4",first:"",second:""
        fuzz_tool_test.go:20: before: "\xe4", after: ""

=== NAME
FAIL
exit status 1
FAIL    golearn/tool_test       0.184s

またエラーが発生したことがわかります。今回は文字列を 2 回反転後に等しくならない問題です。元の文字は \xe4 で、期待される結果は 4ex\ ですが、結果は文字化けです。以下

go
func main() {
  fmt.Println("\xe4")
  fmt.Println([]byte("\xe4"))
  fmt.Println([]rune("\xe4"))
  fmt.Printf("%q\n", "\xe4")
  fmt.Printf("%x\n", "\xe4")
}

実行結果は以下の通りです。


[65533]
"\xe4"
e4

原因を究明すると、\xe4 は 1 バイトを表しますが、有効な UTF-8 シーケンスではありません(UTF-8 エンコーディングでは \xe4 は 3 バイト文字の開始ですが、後の 2 バイトが不足しています)。[]rune に変換すると、Golang は自動的にそれを単一の Unicode 文字を含む []rune{"\uFFFD"} に変換し、反転後も仍是 []rune{"\uFFFD"} で、string に戻すと該 Unicode 文字は再びその UTF-8 エンコーディング \xef\xbf\xbd に置換されます。したがって、1 つの解決策は、入力された文字列が非 utf8 文字列の場合、直接エラーを返すことです。

go
func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    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
}

テストコードも少し修正する必要があります。

go
func FuzzReverse(f *testing.F) {
  testdata := []string{"hello world!", "nice to meet you", "good bye!"}
  for _, data := range testdata {
    f.Add(data)
  }

  f.Fuzz(func(t *testing.T, str string) {
    first, err := tool.Reverse(str)
    if err != nil {
      t.Skip()
    }
    second, err := tool.Reverse(first)
    if err != nil {
      t.Skip()
    }
    t.Logf("str:%q,first:%q,second:%q", str, first, second)
    if str != second {
      t.Errorf("before: %q, after: %q", str, second)
    }
    if utf8.ValidString(str) && !utf8.ValidString(first) {
      t.Errorf("Reverse produced invalid UTF-8 string %q %q", str, first)
    }
  })
}

反転関数が error を返す場合、テストをスキップします。再度ファジングテストを実行します。

sh
$ go test -fuzz . -fuzztime 30s -run Fuzz -v
=== RUN   FuzzReverse
fuzz: elapsed: 0s, gathering baseline coverage: 0/219 completed
fuzz: elapsed: 0s, gathering baseline coverage: 219/219 completed, now fuzzing with 16 workers
fuzz: elapsed: 3s, execs: 895571 (297796/sec), new interesting: 32 (total: 251)
fuzz: elapsed: 6s, execs: 1985543 (363120/sec), new interesting: 37 (total: 256)
fuzz: elapsed: 9s, execs: 3087837 (367225/sec), new interesting: 38 (total: 257)
fuzz: elapsed: 12s, execs: 4090817 (335167/sec), new interesting: 40 (total: 259)
fuzz: elapsed: 15s, execs: 5132580 (346408/sec), new interesting: 44 (total: 263)
fuzz: elapsed: 18s, execs: 6248486 (372185/sec), new interesting: 45 (total: 264)
fuzz: elapsed: 21s, execs: 7366827 (373305/sec), new interesting: 46 (total: 265)
fuzz: elapsed: 24s, execs: 8439803 (358059/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 27s, execs: 9527671 (361408/sec), new interesting: 47 (total: 266)
fuzz: elapsed: 30s, execs: 10569473 (348056/sec), new interesting: 48 (total: 267)
fuzz: elapsed: 30s, execs: 10569473 (0/sec), new interesting: 48 (total: 267)
--- PASS: FuzzReverse (30.16s)
=== NAME
PASS
ok      golearn/tool_test       30.789s

これで、より完全なファジングテスト出力ログが得られます。その中のいくつかの概念の説明は以下の通りです。

  • elapsed: 1 つのラウンド完了後に経過した時間
  • execs: 実行された入力の総数。297796/sec は 1 秒間に何個の入力を示します
  • new interesting: テスト中に、コーパスに追加された「面白い」入力の総数。(面白い入力とは、コードカバレッジを既存のコーパスがカバーできる範囲を超えて拡大できる入力を指します。カバレッジ範囲の拡大に伴い、その増加傾向は全体的に鈍化します)

TIP

-fuzztime パラメータで時間を制限しない場合、ファジングテストは永遠に実行され続けます。

サポートされるタイプ

Go Fuzz でサポートされるタイプは以下の通りです。

  • string, []byte
  • int, int8, int16, int32/rune, int64
  • uint, uint8/byte, uint16, uint32, uint64
  • float32, float64
  • bool

Golang by www.golangdev.cn edit