Open7

go/testingを読む

テラテラ

go/testingの概要を読む

目次

  • go/testingの説明
  • Benchmarks
  • Examples
  • Fuzzing
  • Skipping
  • Subtests and Sub-benchmarks
  • Main

go/testingの説明

Goのtestingは、Go パッケージの自動テストを行うためのパッケージです。"go test" コマンドで以下の形式の任意の関数を自動実行します。

func TestXxx(*testing.T)

新しいテストスイートを作成するためには、TestXxx関数を含み、名前が_test.goで終わるファイルを作成します。そのファイルをテストするものと同じパッケージにおきます。このファイルはパッケージビルドから除外されますが、"go test"コマンドを実行すると含まれるようになります。

テスト関数の例

func TestAbs(t *testing.T) {
        got := Abs(-1)
        if got != 1 {
             t.Errorf("Abs(-1) = %d; want 1", got)
        }
}

用語

  • テストスイート[test suite] : 似たようなテスト項目を一括りにしたもの
テラテラ

Benchmarks

関数の形式

func BenchmarkXxx(*testing.B)

"go test -bench"で実行される。
テストフラグの説明についてはhttps://golang.org/cmd/go/#hdr-Testing_flags を参照

関数例

func BenchmarkRandInt(b *testing.B) {
      for i := 0; i < b.N; i++ {
            rand.Int()
      }
}

ベンチマーク関数はターゲットコードをb.N回実行する必要があります。
b.Nはベンチマークの結果が安定するまで自動で増加する数値です。

出力

BenchmarkRandInt-8   	68453040	        17.8 ns/op

この出力は、BenchmarkRandIntという関数が、68453040回実行され、1試行あたり17.8 nsかかたことを意味します。

ベンチマークが並列環境での性能をテストする必要がある場合、RunParallelヘルパー関数を使用できます。このようなベンチマークはgo test -cpuフラグと一緒に使用することを意図しています。

func BenchmarkTemplateParallel(b *testing.B) {
    templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
    b.RunParallel(func(pb *testing.PB) {
        var buf bytes.Buffer
        for pb.Next() {
            buf.Reset()
            templ.Execute(&buf, "World")
        }
    })
}

ベンチマーク結果のフォーマットの詳細な仕様は、https://golang.org/design/14313-benchmark-format に記載されています。

ベンチマーク結果を扱うための標準的なツールは、https://golang.org/x/perf/cmd にあります。特に、https://golang.org/x/perf/cmd/benchstat は統計的にロバストな A/B 比較を実行します。

テラテラ

Examples

このパッケージでは、Exampleコードを実行し、検証を行います。
Example関数には、出力結果が"Output: "で始まる行コメントに含まれていることがあり、テスト実行時にその関数の標準出力と比較されます(比較は先頭と末尾のスペースを無視します)。
(比較では、先頭と末尾のスペースは無視されます。) これらは、例題の一例です。

func ExampleHello() {
    fmt.Println("hello")    // <=標準出力
    // Output: hello           // <=結果
}

//先頭と末尾のスペースは無視される
func ExampleSalutations() {
    fmt.Println("hello, and")
    fmt.Println("goodbye")
    // Output:
    // hello, and
    // goodbye
}

コメントプレフィックス "Unordered output: "は、"Output: "と似ていますが、任意の行順序にマッチします。
また、出力コメントのない関数例は、コンパイルはされるが実行はされません。

func ExamplePerm() {
    //Perm(5)で[0,1,2,3,4]の順列をランダムに並び変えて、スライスとして出力
    for _, value := range Perm(5) {             
          fmt.Println(value)
    }
    // Unordered output: 4
    // 2
    // 1
    // 3
    // 0
}

関数F、型T、型T上のメソッドMなどパッケージのexapmlesを宣言する命名規則は以下の通り

func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }

テストファイル全体は、1つのexample関数、少なくとも1つの他の関数、型、変数、定数宣言が含まれ、テスト関数やベンチマーク関数がない場合に、exampleとして表示されます。

用語

Perm : Goの関数Perm は,半開放区間[0,n]の整数の擬似ランダム並べ換えを,n個のintのスライスとしてデフォルトから返します Source.

テラテラ

Fuzzing

'go test' と testing パッケージはfuzzingをサポートしています。
fuzzingとは、ランダムに生成された入力と一緒に関数を呼び出し、ユニットテストでは予期しないバグを発見するテスト手法のことです。

fuzzing関数の例

func FuzzXxx(*testing.F) {
   for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4} {
      f.Add(seed) //シード値の登録。引数の型はf.Fuzzの第2引数と同じ
   }
   f.Fuzz(func(t *testing.T, in []byte) {      //fuzzターゲット
      enc := hex.EncodeToString(in)
      out, err := hexDecodeString(enc)
      if err != nil {
         t.Fatalf("%v: decode: %v", in ,err)
      }
      if !bytes.Equal(in, out) {
         t.Fatalf("%v: not equal agter round trip: %v", in ,out)
      }
   }
}

ファズ テストは、シード コーパス、またはデフォルトで実行される一連の入力を維持し、入力生成をシードできます。シード入力は、(*F).Add を呼び出すか、ファズ テストを含むパッケージ内のディレクトリ testdata/fuzz/<Name> (<Name> はファズ テストの名前) にファイルを格納することによって登録できます。シード入力はオプションですが、コード カバレッジが良好な一連の小さなシード入力が提供されると、ファジング エンジンはより効率的にバグを検出できます。これらのシード入力は、ファジングによって特定されたバグの回帰テストとしても機能します。

fuzz テスト内で (*F).Fuzz に渡される関数は、fuzz ターゲットと見なされます。ファズ ターゲットは、*T パラメーターを受け入れ、その後にランダム入力用の 1 つ以上のパラメーターが続く必要があります。 (*F).Add に渡される引数の型は、これらのパラメーターの型と同じでなければなりません。 fuzz ターゲットは、テストと同じ方法で問題が見つかったことを通知する場合があります。つまり、T.Fail (または T.Error や T.Fatal のようにそれを呼び出す任意のメソッド) を呼び出すか、パニックに陥ります。

ファジングが有効な場合 (-fuzz フラグを特定のファジングテストにマッチする正規表現に設定することで)、ファジングターゲットは、シード入力に繰り返しランダムな変更を加えることで生成された引数で呼び出されます。サポートされているプラットフォームでは、'go test' はファジングカバレッジインスツルメンテーションでテスト実行ファイルをコンパイルします。ファジングエンジンはそのインスツルメンテーションを使用して、カバレッジを拡大する入力を見つけてキャッシュし、バグを発見する可能性を高くします。ファジングターゲットが与えられた入力に対して失敗した場合、ファジングエンジンは失敗の原因となった入力をパッケージディレクトリ内の testdata/fuzz/<Name> ディレクトリにあるファイルに書き込みます。このファイルは後にシード入力として機能します。ファイルを書き込むことができない場合(ディレクトリが読み取り専用であるなど)、ファジングエンジンは代わりにビルドキャッシュ内のファズキャッシュディレクトリにファイルを書き込みます。

ファジングが無効の場合,F.Addで登録したシード入力とtestdata/fuzz/<Name>のシード入力でファズターゲットが呼び出されます.このモードでは、ファズテストは通常のテストとほぼ同様に動作し、サブテストは T.Run の代わりに F.Fuzz で開始されます。

ファジングについてのドキュメントは https://go.dev/doc/fuzz を参照してください。

テラテラ

Skipping

Tまたは*BのSkipメソッドを呼び出すことで、テストやベンチマークを実行時にスキップすることができます。

func TestTimeConsuming(t *testing.T) {
   if testing.Short() {
      t.Skip("slipping test in short mode.")
   }
}

*TのSkipメソッドは、入力が無効な場合にファズターゲットで使用することができますが、失敗した入力と見なすべきではないでしょう。

func FuzzJSONMarshaling(f *testing.F) {
   f.Fuzz(func(t *testing.T, b []byte) {
      var v interface{}
      if err := json.Unmarshal(b, &v); err  != nil {
         t.Slip()
      }
      if _, err := json.Marshal(v); err != nil {
         t.Error("Marshal: %v", err)
      }
   })
}
テラテラ

Subtests and Sub-benchmarks

TとBのRunメソッドでは、サブテストやサブベンチマークを定義することができ、それぞれ別の関数を定義する必要がありません。これにより、テーブルドリブンベンチマークや階層型テストの作成などが可能になります。また、セットアップやティアダウンのコードを共通化することも可能です。

func TestFoo(t *testing.T) {
   // <setup code>
   t.Run("A=1", func(t *testing.T) { ... })
   t.Run("A=1", func(t *testing.T) { ... })
   t.Run("A=1", func(t *testing.T) { ... })
   // <tear-down code>
}

各サブテストおよびサブベンチマークは一意の名前を持ちます。トップレベルのテストの名前とRunに渡された一連の名前の組み合わせをスラッシュで区切り、曖昧さをなくすためにオプションで末尾にシーケンス番号を付けます。

run、-bench、-fuzz コマンドラインフラグの引数は、テストの名前にマッチするアンカーなしの正規表現である。サブテストなど、複数のスラッシュ区切り要素を持つテストの場合、 引数はそれ自体がスラッシュ区切りになり、 それぞれの名前要素に順番にマッチする式が作成されます。アンカーがないため、空の式は任意の文字列にマッチします。たとえば、"matching" 使用すること "whose name contains" を意味します。

go test -run ''        # Run all tests.
go test -run Foo       # Run top-level tests matching "Foo", such as "TestFooBar".
go test -run Foo/A=    # For top-level tests matching "Foo", run subtests matching "A=".
go test -run /A=1      # For all top-level tests, run subtests matching "A=1".
go test -fuzz FuzzFoo  # Fuzz the target matching "FuzzFoo"

また、-run引数は、デバッグのために、シードコーパスの特定の値を実行するために使用することができます。

go test -run=FuzzFoo/9ddb952d9814

fuzz と -run フラグは、ターゲットをファジングしつつ、他のすべてのテストの実行をスキップするために、両方設定することができます。

サブテストは、並列性を制御するためにも使用できます。親テストが完了するのは、そのサブテストがすべて完了したときだけです。この例では、他のトップレベルのテストが定義されている場合でも、 すべてのテストは互いに並行して実行されます。

func TestGroupParallel(t *tetsing.T) {
   for _, tc := range tests {
      tc := tc // capture range variable
      t.Run(tc.Name, func(t *testing.T) {
         t.Parallel()
         ...
      })
   }
}

並行するサブテストが終了するまでRunは戻らないので、並行するテスト群のclean upをすることができます。

func TestTeardownParallel(t *testing.T) {
    // This Run will not return until the parallel tests finish.
    t.Run("group", func(t *testing.T) {
        t.Run("Test1", parallelTest1)
        t.Run("Test2", parallelTest2)
        t.Run("Test3", parallelTest3)
    })
    // <tear-down code>
}

用語

tear-down code : プログラムの終了時に実行されるコード
Seed corpus : ?

テラテラ

Main

テストプログラムやベンチマークプログラムでは、実行の前後に特別なセットアップやティアダウンが必要な場合があります。また、どのコードがメインスレッドで実行されるかを制御する必要がある場合もあります。これらのケースをサポートするために、テストファイルに関数:

func TestMain(m *testing.M)

を呼び出すと、生成されたテストはテストやベンチマークを直接実行するのではなく、TestMain(m)を呼び出します。TestMain は main goroutine の中で実行され、m.Run の呼び出しに必要なセットアップとティアダウンができます。 m.Run は os.Exit に渡されるかもしれない終了コードを返します。TestMainが戻った場合、テストラッパーはm.Runの結果をos.Exit自体に渡します。

TestMain が呼び出されたとき、flag.Parse はまだ実行されていません。TestMain が、testing パッケージのものを含むコマンドラインフラグに依存している場合、明示的に flag.Parse を呼び出す必要があります。コマンドライン・フラグは test や benchmark 関数が実行されるまでに常に解析されます。

TestMainの簡単な実装は以下の通りです。

func TestMain(m *testting.M) {
   // call flag.Parse() here if TestMain uses flags
   os.Exit(m.Run())
}

TestMainは低レベルのプリミティブであり、通常のテスト関数で十分なカジュアルなテストのニーズには必要ないはずです。