フューチャー技術ブログ

Go 1.17のtesting新機能

TIGの伊藤真彦です。

この記事はGo1.17連載の3記事目です。

Go1.17からのtestingにおける新機能

Go1.17のリリースノートにこのような記載があります。

testing
Added a new testing flag -shuffle which controls the execution order of tests and benchmarks.

The new T.Setenv and B.Setenv methods support setting an environment variable for the
duration of the test or benchmark.

テストとベンチマークの実行順序を制御する新しいテストフラグ-shuffleを追加しました。

新しいT.SetenvおよびB.Setenvメソッドは、テストまたはベンチマークの期間中の環境変数の設定をサポートします。

意外と根深い課題を解決できる嬉しい機能ですね、今回はこの新機能の紹介をすることにします。

shuffleオプションについて

Goでテストを行う時はいくつかのフラグを追加することができます。-runで実行するテストを絞り込んだり、-coverでカバレッジを計測したりといった機能があります。

公式ドキュメントはこちらです。

それらオプション群にshuffleが追加されました。

shuffleオプションの使い方

ドキュメントに下記のように追記されています。

-shuffle off,on,N
Randomize the execution order of tests and benchmarks.
It is off by default. If -shuffle is set to on, then it will seed
the randomizer using the system clock. If -shuffle is set to an
integer N, then N will be used as the seed value. In both cases,
the seed will be reported for reproducibility.

go test -shuffle=onのように利用できます。
go test -shuffle=123のように整数値を指定することで、ランダムな値の生成などにおけるseed値を指定する事ができます。

テストをシャッフルすると何が嬉しいのか

テストの実行順番をランダムにする機能は、Ruby On Railsでのテストにおけるデファクトスタンダードであるrspecなど、他の言語、ライブラリでも実装されています。
順番をランダムにすることで、前に書いたテストの実行結果に依存する状態を検知し、回避できることが最大の目的です。

前に書いたテストの実行結果に依存する状態とは、下記のようなケースが該当します。

  • 前のテストケースでグローバル変数が宣言、変更された前提で次のテストケースが書かれている
  • 前のテストケースでデータベースに保存された内容を次のテストケースで利用している

これらの書き方は基本的にバッドプラクティスです。

何らかの事情でテストケースや実装に変更が加わった際に、一見無関係なテストが落ちて混乱を招くことになります。テストは各ケースの実行ごとにデータベースの内容を掃除するなど、クリーンな状態を保ちましょう。テストをランダム実行することで、上記のバッドプラクティスを早期に炙り出すことが可能になります。

当該機能を追加したissueにおいても、グローバル変数の状態が変わる事で、テストの実行順序が実行結果に影響が出る例が記載されています。

Those tests pass, everything looks fine, but they’re order dependent. Running them in another order will fail.
To prevent such hidden and hard to debug mistakes we need to make the order of test random for each test build.

これらのテストは合格し、すべてが正常に見えますが、順序によって異なります。それらを別の順序で実行すると失敗します。
このような隠れたデバッグの難しい間違いを防ぐために、テストビルドごとにテストの順序をランダムにする必要があります。

Goに限った話ではないので、テストを書くときは気を付けていきたいですね。

T.Setenv、B.Setenvについて

テストコード、並びにベンチマーク中に環境変数をセットする事ができるようになりました。osパッケージのSetenvとの違いは、テストが終了するとセットした内容が破棄され、環境変数が汚染されない事です。

元々の環境変数がセットされている場合は、きちんと元の値に戻ります、気軽に環境変数を変更できるようになりました。

t.Parallel実行後に利用すると環境変数の寿命の扱いが破綻するため、エラーが発生する点だけ要注意です。

サンプルを探したところ、下記のようにGo本体のテストでも早速大活躍していました。

test.go
func TestImportVendor(t *testing.T) {
testenv.MustHaveGoBuild(t) // really must just have source

t.Setenv("GO111MODULE", "off")

ctxt := Default
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
ctxt.GOPATH = filepath.Join(wd, "testdata/withvendor")
p, err := ctxt.Import("c/d", filepath.Join(ctxt.GOPATH, "src/a/b"), 0)
if err != nil {
t.Fatalf("cannot find vendored c/d from testdata src/a/b directory: %v", err)
}
want := "a/vendor/c/d"
if p.ImportPath != want {
t.Fatalf("Import succeeded but found %q, want %q", p.ImportPath, want)
}
}

私たちのチームでは、今まではテスト実行時にはMakefileで環境変数を一通り整備してからテストを実行する運用をしていました。また、必要な環境変数が存在しない場合はエラーで落ちるロジックが保険として書かれているのですが、複雑度が低いためテストできていない事を許容していました。

これらの課題をGo1.17に上げ事で簡単に解決できる希望が見えてきました。

まとめ

Go1.17ではテストの実装を改善する為、2点の新機能が追加されている。

  • 新しいテストフラグ-shuffle
  • 環境変数をモックするT.SetenvおよびB.Setenv

見落とさず活用していきたいなと感じたので、連載のトピックとして取り上げてみました。