18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go 1.15 testing パッケージの TestMain は os.Exit を呼ばなくてよくなった

Last updated at Posted at 2020-08-20

TL;DR

  • Go1.15 がリリースされました
  • testing パッケージの実装の変更にて TestMain で os.Exit を呼ばなくてよくなりました
  • testing.M の実装変更及び、 go test で生成される main 関数のテンプレート修正によって実現されている

Go 1.15

2020年8月11日、 The Go Blog より、 Go 1.15 のリリースが公表されました。

Release Note を読むと、さまざまな機能が追加されています。 

これを読んでいて、ふと Minor changes to the library から testing パッケージにいくつか変更があって、バージョンアップしたらちょっっっとテストの書き方が変わるなって思ったので深堀り確認してみました。

※ はてなブログで同じものを見たぞと思ったとしたら、それは同一の記事です。 https://khigashigashi.hatenablog.com/entry/2020/08/20/110803 ブログの書き先に迷子になった結果です。

testing の変更

testing パッケージには、いくつかマイナーな変更が加えられています。

  • T.Deadline の追加

The testing.T type now has a Deadline method that reports the time at which the test binary will have exceeded its timeout.

  • TestMain は os.Exit を呼ばなくて良くなった

A TestMain function is no longer required to call os.Exit. If a TestMain function returns, the test binary will call os.Exit with the value returned by m.Run.

  • T.TempDir / B.TempDir の追加

The new methods T.TempDir and B.TempDir return temporary directories that are automatically cleaned up at the end of the test.

  • go test -v の表示改善

go test -v now groups output by test name, rather than printing the test name on each line.

タイトルのとおりですが、この中の 2 つめに焦点を当ててみます。

TestMain は os.Exit を呼ばなくて良くなった

A TestMain function is no longer required to call os.Exit. If a TestMain function returns, the test binary will call os.Exit with the value returned by m.Run.

たとえば、TestMain を使うテストコードはGo 1.14 以前は次のようなコードだったと思います。

Go_1.14以前
package hoge_test

import (
	"log"
	"testing"
)

func TestMain(m *testing.M) {
	shutdown := setupDB()
	defer shutdown()

	os.Exit(m.Run())
}

func setupDB() func() {
	return func() {
		// shutdown
	}
}

testing.m.Run() は プログラムの終了コード に該当する int 値を返してくるので、それを os.Exit() に明示的に渡すようにしていました。

Go 1.15 以降は、この os.Exit が不要になりました。

Go_1.15以降
package hoge_test

import (
	"log"
	"testing"
)

func TestMain(m *testing.M) {
	shutdown := setupDB()
	defer shutdown()

	m.Run() // os.Exit は不要
}

func setupDB() func() {
	return func() {
		// shutdown
	}
}

使い手としてはこれだけです。マイナーな変更ですね。まぁそれだけでは、へぇそうなんだねって話なので、実際中ではどういう変更があったのってところを見たいなと思います。

Dive into testing.M

Issue

この変更は、次の issue で 2019年9月6日に提案されているものです。

issue を読んでいると、よく Buggy なテストが生まれることが観測されていたと、Russ Cox氏が言及しています。

This mistake does happen a lot, and the testing package that called TestMain and prepared the m can certainly record the result of m.Run for use in its own outer os.Exit.

実際にどういうミスかと言うと、こういうテストコードの場合です。

hoge_test.go
package sample_test

import (
	"os"
	"testing"
)

func TestMain(m *testing.M) {
	var exitCode int
	defer os.Exit(exitCode)

	setup()
	defer teardown()
	exitCode = m.Run()
}

func setup() {
	//
}

func teardown() {
	//
}

func TestHoge(t *testing.T) {
	t.Fail()
}

TestMain() では、deferでos.Exit をcallするようにしています。そして当該パッケージ内には失敗するテストコードがあるので、これは失敗するはずです。これを実行すると次のようになります。

$ go test -v hoge_test.go 
=== RUN   TestHoge
--- FAIL: TestHoge (0.00s)
FAIL
ok      command-line-arguments  0.369s

ok となってしまいました。終了コードがうまく反映されていません。

このような失敗していてもテストコマンドが静かにそのバグを報告しなくてなってしまうことに対して、 Accept されたのは以下でした。

  1. m.Run records its own result in an unexported field of m, and then
  2. if TestMain(m) returns, the outer test func main harness calls os.Exit with that code.

1. m.Run records its own result in an unexported field of m

この課題に対して実際に実装された結果は 下記の commit から見て取れます。

L220 にてコメントは次のように変更されています。

src/testing/testing.go
// and teardown is necessary around a call to m.Run. m.Run will return an exit
// status that may be passed to os.Exit. If TestMain returns, the test wrapper
// will pass the result of m.Run to os.Exit itself. When TestMain is called,
// flag.Parse has not been run. If TestMain depends on command-line flags,
// including those of the testing package, it should call flag.Parse explicitly.

If TestMain returns, the test wrapper will pass the result of m.Run to os.Exit itself.

TestMain が return したら、 the test wrapperさんがそれをos.Exitに渡してくれるというものです。
そのためにここでは、 「1. testing.M の構造体に、終了コードが追加」し、「2. testing.M.Run は M.exitCode に終了コードを設定」しています。

  1. testing.M の構造体に、終了コードが追加された。
src/testing/testing.go
// M is a type passed to a TestMain function to run the actual tests.
type M struct {
	deps       testDeps
	tests      []InternalTest
	benchmarks []InternalBenchmark
	examples   []InternalExample
	timer     *time.Timer
	afterOnce sync.Once

	numRun int

	// value to pass to os.Exit, the outer test func main
	// harness calls os.Exit with this code. See #34129.
	exitCode int // <= これが今回増えました
}
  1. testing.M.Run は M.exitCode に終了コードを設定する

return する際、これまで終了コードを返していただけでしたが、 M.exitCode に設定するようにしています。

src/testing/testing.go
	if *parallel < 1 {
		fmt.Fprintln(os.Stderr, "testing: -parallel can only be given a positive integer")
		flag.Usage()
		// return 2 これがもともと、普通に終了コードを返答していた
		m.exitCode = 2 // m.e
		return
	}

※ 関数冒頭に defer func で m.exitCode を戻り値として返却するように記述しています。

src/testing/testing.go
func (m *M) Run() (code int) {
	defer func() {
		code = m.exitCode
	}()

2. if TestMain(m) returns, the outer test func main harness calls os.Exit with that code.

TestMain が終了後、外の main 関数が m.exitCode を見て、os.Exit をcallするようにしています。これは、以下のコミットにその反映を見ることが出来ます。

この変更は、 go testによって生成される main 関数のテンプレートの修正です。

TestMain の場合は、 reflect パッケージを追加し、 構造体 M の exitCode の field をとっています。

src/cmd/go/internal/load/test.go
os.Exit(int(reflect.ValueOf(m).Elem().FieldByName("exitCode").Int()))

この変更によって、testing.M の内部の unexported な exitCode の値を使って、 os.Exit をコールしています。

以上

Go 1.15 におけるテストの書き方の変更と、その背景・内部実装の深堀りでした。 Congrats Go 1.15 release!!!

(以上)

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?