Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14991] ファイルの概要

このコミットは、Go言語のテストフレームワークにおいて、テストが複数回実行された際に正しく動作するように修正を加えるものです。具体的には、go test -cpu=1,2,4 std のようなコマンドでテストを複数回実行しても、テストが失敗しないように改善されています。

コミット

commit 75104237c82f943bffc41334acd179cf28f30ea2
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sun Jan 27 00:24:09 2013 +0100

    all: make tests able to run multiple times.
    
    It is now possible to run "go test -cpu=1,2,4 std"
    successfully.
    
    Fixes #3185.
    
    R=golang-dev, dave, minux.ma, bradfitz
    CC=golang-dev
    https://golang.org/cl/7196052

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/75104237c82f943bffc41334acd179cf28f30ea2

元コミット内容

all: make tests able to run multiple times.

It is now possible to run "go test -cpu=1,2,4 std"
successfully.

Fixes #3185.

R=golang-dev, dave, minux.ma, bradfitz
CC=golang-dev
https://golang.org/cl/7196052

変更の背景

このコミットの背景には、Go言語のテスト実行における特定の課題がありました。Goのテストツール go test は、-cpu フラグを使用してテストを並行して実行するCPUの数を指定できます。例えば、go test -cpu=1,2,4 std は、標準ライブラリのテストをCPU数1、2、4のそれぞれで実行することを意味します。

しかし、この機能を使用すると、一部のテストが複数回実行された際に、以前の実行で残された状態(ファイル、グローバル変数など)が原因で失敗するという問題が発生していました。これは、テストが実行環境に副作用を残し、その副作用が次のテスト実行に影響を与えていたためです。

具体的には、Issue 3185(https://golang.org/issue/3185)で報告された問題に対応しています。この問題は、テストがグローバルな状態や一時ファイルなどを適切にクリーンアップしないために、複数回のテスト実行で不整合が生じることを指摘していました。このコミットは、これらのテストが独立して、かつ複数回実行されても安定してパスするように、テストコードの堅牢性を向上させることを目的としています。

前提知識の解説

Go言語のテストフレームワーク

Go言語には、標準ライブラリに testing パッケージが用意されており、これを用いてユニットテストやベンチマークテストを記述します。テストファイルは通常、テスト対象のソースファイルと同じディレクトリに _test.go というサフィックスを付けて配置されます。

  • go test コマンド: Goのテストを実行するためのコマンドです。
    • -cpu N: テストを並行して実行するCPUの数を指定します。カンマ区切りで複数の値を指定すると、それぞれのCPU数でテストが実行されます。
    • std: 標準ライブラリのパッケージを対象とします。
  • *testing.T: テスト関数に渡される構造体で、テストの失敗を報告したり、ヘルパー関数を呼び出したりするために使用されます。
  • テストの独立性: 理想的なテストは、他のテストの実行順序や結果に依存せず、常に同じ結果を返す「独立性」を持つべきです。これにより、テストの信頼性が高まり、デバッグが容易になります。
  • 副作用: プログラムが外部の状態(ファイルシステム、グローバル変数、ネットワークなど)を変更することを副作用と呼びます。テストにおいて副作用は、テストの独立性を損なう原因となることがあります。
  • クリーンアップ処理: テストが副作用を持つ場合、テストの実行後にその副作用を元に戻す(クリーンアップする)処理が必要です。一時ファイルの削除やグローバル変数のリセットなどがこれに該当します。Goのテストでは、defer ステートメントや testing.TCleanup メソッド(このコミットの時点では Cleanup は存在しない可能性が高い)などを用いてクリーンアップ処理を記述します。

グローバル変数とテスト

Go言語ではグローバル変数を定義できますが、テストにおいてグローバル変数が使用されると、テストの独立性を損なう可能性があります。あるテストがグローバル変数の値を変更し、その変更が別のテストに影響を与えることで、テストの実行順序によって結果が変わる「テストの順序依存性」が発生することがあります。これを避けるためには、テストごとにグローバル変数をリセットするか、テスト内でローカル変数を使用するなどの対策が必要です。

defer ステートメント

Go言語の defer ステートメントは、そのステートメントを含む関数がリターンする直前に実行される関数呼び出しをスケジュールします。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、テスト後のクリーンアップ処理に非常に便利です。defer を使用することで、関数のどこからリターンしても確実にクリーンアップ処理が実行されるようになります。

技術的詳細

このコミットは、主に以下の技術的なアプローチでテストの複数回実行時の安定性を向上させています。

  1. グローバル状態のリセット:

    • src/pkg/expvar/expvar_test.go では、RemoveAll() 関数を導入し、各テスト関数の冒頭で呼び出すことで、expvar パッケージが管理するグローバルなメトリック(Int, Float, String, Map など)をテストごとにリセットするようにしています。これにより、前のテスト実行で登録されたメトリックが次のテストに影響を与えることを防ぎます。
    • src/pkg/flag/flag_test.go では、ResetForTesting(nil) を呼び出すことで、flag パッケージのグローバルなフラグ状態をリセットしています。これにより、前のテストで定義されたコマンドラインフラグが次のテストに影響を与えないようにします。また、以前はグローバル変数として定義されていたテスト用のフラグ変数を、各テスト関数内でローカルに定義するように変更し、グローバルな副作用を排除しています。
    • src/pkg/go/parser/error_test.go では、fsetErrs という token.FileSet のグローバル変数を導入し、TestErrors 関数の冒頭で token.NewFileSet() を呼び出して初期化するように変更しています。これにより、テストごとに新しいファイルセットが使用され、以前のテスト実行の状態が引き継がれることを防ぎます。
    • src/pkg/go/types/check_test.go では、testBuiltinsDeclared というブール型のグローバル変数を導入し、ビルトイン関数の宣言が一度だけ行われるように制御しています。これにより、複数回テストが実行されても、ビルトイン関数が重複して宣言されることによるエラーを防ぎます。
  2. 一時ファイルの適切なクリーンアップ:

    • src/pkg/debug/gosym/pclntab_test.go では、endtest() 関数を導入し、defer endtest() を各テスト関数に追加することで、テスト中に作成された一時ディレクトリ pclineTempDir を確実に削除するようにしています。これにより、テスト実行後に不要なファイルが残り、次のテスト実行に影響を与えることを防ぎます。
  3. テストデータのローカル化:

    • src/pkg/net/http/responsewrite_test.go では、以前グローバル変数として定義されていた respWriteTests スライスを、TestResponseWrite 関数内でローカル変数として定義するように変更しています。これにより、テストデータがテストごとに独立して管理され、他のテストからの意図しない変更を防ぎます。
  4. テスト環境の分離:

    • src/pkg/net/http/serve_test.go では、httptest.NewServer(nil) ではなく httptest.NewServer(mux) を使用するように変更しています。これは、テストサーバーが特定の ServeMux インスタンスを使用するように明示的に指定することで、テストサーバーの動作がグローバルな http.DefaultServeMux の状態に依存しないようにするためです。これにより、テストの独立性が向上します。
  5. ファイルディスクリプタのリークチェックの改善:

    • src/pkg/os/exec/exec_test.go では、testedAlreadyLeaked というグローバル変数を導入し、ファイルディスクリプタのリークチェックが一度だけ行われるようにしています。これにより、複数回のテスト実行で同じチェックが繰り返し行われることによる冗長性を排除し、テストの効率を向上させています。

これらの変更は、Goのテストがより堅牢で信頼性の高いものになるように、テストの独立性を高め、副作用を適切に管理することに焦点を当てています。

コアとなるコードの変更箇所

このコミットでは、以下のファイルが変更されています。

  • src/pkg/debug/gosym/pclntab_test.go
  • src/pkg/exp/locale/collate/build/builder_test.go
  • src/pkg/expvar/expvar_test.go
  • src/pkg/flag/flag_test.go
  • src/pkg/go/parser/error_test.go
  • src/pkg/go/types/check_test.go
  • src/pkg/net/http/responsewrite_test.go
  • src/pkg/net/http/serve_test.go
  • src/pkg/os/exec/exec_test.go

主要な変更は、テスト関数内でのグローバル状態のリセット、一時ファイルのクリーンアップ、およびテストデータのローカル化です。

src/pkg/debug/gosym/pclntab_test.go

--- a/src/pkg/debug/gosym/pclntab_test.go
+++ b/src/pkg/debug/gosym/pclntab_test.go
@@ -52,6 +52,14 @@ func dotest() bool {
 	return true
 }
 
+func endtest() {
+	if pclineTempDir != "" {
+		os.RemoveAll(pclineTempDir)
+		pclineTempDir = ""
+		pclinetestBinary = ""
+	}
+}
+
 func getTable(t *testing.T) *Table {
 	f, tab := crack(os.Args[0], t)
 	f.Close()
@@ -95,6 +103,7 @@ func TestLineFromAline(t *testing.T) {
 	if !dotest() {
 		return
 	}
+	defer endtest()
 
 	tab := getTable(t)
 
@@ -142,6 +151,7 @@ func TestLineAline(t *testing.T) {
 	if !dotest() {
 		return
 	}
+	defer endtest()
 
 	tab := getTable(t)
 
@@ -183,7 +193,7 @@ func TestPCLine(t *testing.T) {
 	if !dotest() {
 		return
 	}
-	defer os.RemoveAll(pclineTempDir)
+	defer endtest()
 
 	f, tab := crack(pclinetestBinary, t)
 	text := f.Section(".text")

src/pkg/expvar/expvar_test.go

--- a/src/pkg/expvar/expvar_test.go
+++ b/src/pkg/expvar/expvar_test.go
@@ -18,6 +18,7 @@ func RemoveAll() {
 }
 
 func TestInt(t *testing.T) {
+	RemoveAll()
 	reqs := NewInt("requests")
 	if reqs.i != 0 {
 		t.Errorf("reqs.i = %v, want 0", reqs.i)
@@ -43,6 +44,7 @@ func TestInt(t *testing.T) {
 }
 
 func TestFloat(t *testing.T) {
+	RemoveAll()
 	reqs := NewFloat("requests-float")
 	if reqs.f != 0.0 {
 		t.Errorf("reqs.f = %v, want 0", reqs.f)
@@ -68,6 +70,7 @@ func TestFloat(t *tc.T) {
 }
 
 func TestString(t *testing.T) {
+	RemoveAll()
 	name := NewString("my-name")
 	if name.s != "" {
 		t.Errorf("name.s = %q, want \"\"", name.s)
@@ -84,6 +87,7 @@ func TestString(t *testing.T) {
 }
 
 func TestMapCounter(t *testing.T) {
+	RemoveAll()
 	colors := NewMap("bike-shed-colors")
 
 	colors.Add("red", 1)
@@ -123,6 +127,7 @@ func TestMapCounter(t *testing.T) {
 }
 
 func TestFunc(t *testing.T) {
+	RemoveAll()
 	var x interface{} = []string{"a", "b"}
 	f := Func(func() interface{} { return x })
 	if s, exp := f.String(), `["a","b"]`; s != exp {

src/pkg/flag/flag_test.go

--- a/src/pkg/flag/flag_test.go
+++ b/src/pkg/flag/flag_test.go
@@ -15,17 +15,6 @@ import (
 	"time"
 )
 
-var (
-	test_bool     = Bool("test_bool", false, "bool value")
-	test_int      = Int("test_int", 0, "int value")
-	test_int64    = Int64("test_int64", 0, "int64 value")
-	test_uint     = Uint("test_uint", 0, "uint value")
-	test_uint64   = Uint64("test_uint64", 0, "uint64 value")
-	test_string   = String("test_string", "0", "string value")
-	test_float64  = Float64("test_float64", 0, "float64 value")
-	test_duration = Duration("test_duration", 0, "time.Duration value")
-)
-
 func boolString(s string) string {
 	if s == "0" {
 		return "false"
@@ -34,6 +23,16 @@ func boolString(s string) string {
 }
 
 func TestEverything(t *testing.T) {
+	ResetForTesting(nil)
+	Bool("test_bool", false, "bool value")
+	Int("test_int", 0, "int value")
+	Int64("test_int64", 0, "int64 value")
+	Uint("test_uint", 0, "uint value")
+	Uint64("test_uint64", 0, "uint64 value")
+	String("test_string", "0", "string value")
+	Float64("test_float64", 0, "float64 value")
+	Duration("test_duration", 0, "time.Duration value")
+
 	m := make(map[string]*Flag)
 	desired := "0"
 	visitor := func(f *Flag) {

src/pkg/net/http/responsewrite_test.go

--- a/src/pkg/net/http/responsewrite_test.go
+++ b/src/pkg/net/http/responsewrite_test.go
@@ -15,83 +15,83 @@ type respWriteTest struct {
 	Raw  string
 }
 
-var respWriteTests = []respWriteTest{
-	// HTTP/1.0, identity coding; no trailer
-	{
-		Response{
-			StatusCode:    503,
-			ProtoMajor:    1,
-			ProtoMinor:    0,
-			Request:       dummyReq("GET"),
-			Header:        Header{},
-			Body:          ioutil.NopCloser(bytes.NewBufferString("abcdef")),
-			ContentLength: 6,
-		},
+func TestResponseWrite(t *testing.T) {
+	respWriteTests := []respWriteTest{
+		// HTTP/1.0, identity coding; no trailer
+		{
+			Response{
+				StatusCode:    503,
+				ProtoMajor:    1,
+				ProtoMinor:    0,
+				Request:       dummyReq("GET"),
+				Header:        Header{},
+				Body:          ioutil.NopCloser(bytes.NewBufferString("abcdef")),
+				ContentLength: 6,
+			},
 
-		"HTTP/1.0 503 Service Unavailable\r\n" +
-			"Content-Length: 6\r\n\r\n" +
-			"abcdef",
-	},
-	// Unchunked response without Content-Length.
-	{
-		Response{
-			StatusCode:    200,
-			ProtoMajor:    1,
-			ProtoMinor:    0,
-			Request:       dummyReq("GET"),
-			Header:        Header{},
-			Body:          ioutil.NopCloser(bytes.NewBufferString("abcdef")),
-			ContentLength: -1,
+			"HTTP/1.0 503 Service Unavailable\r\n" +
+				"Content-Length: 6\r\n\r\n" +
+				"abcdef",
 		},
-		"HTTP/1.0 200 OK\r\n" +
-			"\r\n" +
-			"abcdef",
-	},
-	// HTTP/1.1, chunked coding; empty trailer; close
-	{
-		Response{
-			StatusCode:       200,
-			ProtoMajor:       1,
-			ProtoMinor:       1,
-			Request:          dummyReq("GET"),
-			Header:           Header{},
-			Body:             ioutil.NopCloser(bytes.NewBufferString("abcdef")),
-			ContentLength:    6,
-			TransferEncoding: []string{"chunked"},
-			Close:            true,
+		// Unchunked response without Content-Length.
+		{
+			Response{
+				StatusCode:    200,
+				ProtoMajor:    1,
+				ProtoMinor:    0,
+				Request:       dummyReq("GET"),
+				Header:        Header{},
+				Body:          ioutil.NopCloser(bytes.NewBufferString("abcdef")),
+				ContentLength: -1,
+			},
+			"HTTP/1.0 200 OK\r\n" +
+				"\r\n" +
+				"abcdef",
 		},
-
-		"HTTP/1.1 200 OK\r\n" +
-			"Connection: close\r\n" +
-			"Transfer-Encoding: chunked\r\n\r\n" +
-			"6\r\nabcdef\r\n0\r\n\r\n",
-	},
-
-	// Header value with a newline character (Issue 914).
-	// Also tests removal of leading and trailing whitespace.
-	{
-		Response{
-			StatusCode: 204,
-			ProtoMajor: 1,
-			ProtoMinor: 1,
-			Request:    dummyReq("GET"),
-			Header: Header{
-				"Foo": []string{" Bar\nBaz "},
+		// HTTP/1.1, chunked coding; empty trailer; close
+		{
+			Response{
+				StatusCode:       200,
+				ProtoMajor:       1,
+				ProtoMinor:       1,
+				Request:          dummyReq("GET"),
+				Header:           Header{},
+				Body:             ioutil.NopCloser(bytes.NewBufferString("abcdef")),
+				ContentLength:    6,
+				TransferEncoding: []string{"chunked"},
+				Close:            true,
 			},
-			Body:             nil,
-			ContentLength:    0,
-			TransferEncoding: []string{"chunked"},
-			Close:            true,
+
+			"HTTP/1.1 200 OK\r\n" +
+				"Connection: close\r\n" +
+				"Transfer-Encoding: chunked\r\n\r\n" +
+				"6\r\nabcdef\r\n0\r\n\r\n",
 		},
-
-		"HTTP/1.1 204 No Content\r\n" +
-			"Connection: close\r\n" +
-			"Foo: Bar Baz\r\n" +
-			"\r\n",
-	},
-}
-
-func TestResponseWrite(t *testing.T) {
+
+		// Header value with a newline character (Issue 914).
+		// Also tests removal of leading and trailing whitespace.
+		{
+			Response{
+				StatusCode: 204,
+				ProtoMajor: 1,
+				ProtoMinor: 1,
+				Request:    dummyReq("GET"),
+				Header: Header{
+					"Foo": []string{" Bar\nBaz "},
+				},
+				Body:             nil,
+				ContentLength:    0,
+				TransferEncoding: []string{"chunked"},
+				Close:            true,
+			},
+
+			"HTTP/1.1 204 No Content\r\n" +
+				"Connection: close\r\n" +
+				"Foo: Bar Baz\r\n" +
+				"\r\n",
+		},
+	}
 	for i := range respWriteTests {
 		tt := &respWriteTests[i]
 		var braw bytes.Buffer

コアとなるコードの解説

src/pkg/debug/gosym/pclntab_test.go の変更

  • endtest() 関数の追加:
    • この関数は、テスト中に作成された一時ディレクトリ pclineTempDir を削除し、関連するグローバル変数をリセットする役割を担います。
  • defer endtest() の導入:
    • TestLineFromAline, TestLineAline, TestPCLine の各テスト関数に defer endtest() が追加されました。これにより、これらのテスト関数が終了する際に、endtest() が確実に呼び出され、一時ファイルがクリーンアップされるようになります。以前は defer os.RemoveAll(pclineTempDir) が直接呼び出されていましたが、endtest() にまとめることで、クリーンアップロジックの一元化と、関連するグローバル変数のリセットも同時に行えるようになりました。

src/pkg/expvar/expvar_test.go の変更

  • RemoveAll() の呼び出し:
    • TestInt, TestFloat, TestString, TestMapCounter, TestFunc の各テスト関数の冒頭に RemoveAll() が追加されました。expvar パッケージは、アプリケーションのメトリックをグローバルに公開するために使用されます。RemoveAll() は、これらのグローバルなメトリックをクリアする関数です。各テストの開始時にこれを呼び出すことで、前のテストで登録されたメトリックが次のテストに影響を与えることを防ぎ、テストの独立性を確保しています。

src/pkg/flag/flag_test.go の変更

  • グローバル変数の削除と ResetForTesting(nil) の導入:
    • 以前はグローバル変数として定義されていた test_bool, test_int などのフラグ変数が削除されました。
    • TestEverything 関数の冒頭で ResetForTesting(nil) が呼び出されるようになりました。flag.ResetForTesting は、flag パッケージの内部状態(登録されたフラグなど)をリセットするための関数です。これにより、各テスト実行前にフラグの状態が初期化され、前のテストで定義されたフラグが次のテストに影響を与えることがなくなります。
    • 削除されたグローバルフラグ変数の代わりに、TestEverything 関数内で Bool, Int などの関数が直接呼び出され、ローカルなスコープでフラグが定義されるようになりました。これにより、フラグの定義がテストごとに独立し、グローバルな副作用が排除されます。

src/pkg/net/http/responsewrite_test.go の変更

  • テストデータのローカル化:
    • 以前はグローバル変数として定義されていた respWriteTests スライスが、TestResponseWrite 関数内に移動され、ローカル変数として定義されるようになりました。これにより、respWriteTests の内容がテストごとに独立して管理され、他のテストからの意図しない変更や、複数回実行された際のデータ汚染を防ぎます。

その他のファイルの変更

  • src/pkg/exp/locale/collate/build/builder_test.go: new(entry) とループを使って e.elems を構築するように変更され、テストデータの初期化がより堅牢になりました。
  • src/pkg/go/parser/error_test.go: fsetErrs というグローバル変数を導入し、TestErrors 関数内で token.NewFileSet() を呼び出して初期化するように変更されました。これにより、テストごとに新しいファイルセットが使用され、以前のテスト実行の状態が引き継がれることを防ぎます。
  • src/pkg/go/types/check_test.go: testBuiltinsDeclared というグローバル変数を導入し、ビルトイン関数の宣言が一度だけ行われるように制御しています。これにより、複数回テストが実行されても、ビルトイン関数が重複して宣言されることによるエラーを防ぎます。
  • src/pkg/net/http/serve_test.go: httptest.NewServer(nil) ではなく httptest.NewServer(mux) を使用するように変更され、テストサーバーが特定の ServeMux インスタンスを使用するように明示的に指定することで、テストサーバーの動作がグローバルな http.DefaultServeMux の状態に依存しないようにしています。
  • src/pkg/os/exec/exec_test.go: testedAlreadyLeaked というグローバル変数を導入し、ファイルディスクリプタのリークチェックが一度だけ行われるようにしています。

これらの変更は、Goのテストがより堅牢で信頼性の高いものになるように、テストの独立性を高め、副作用を適切に管理することに焦点を当てています。

関連リンク

参考にした情報源リンク

  • Go Issue 3185: https://golang.org/issue/3185 (このコミットが修正した問題の報告)
  • Go CL 7196052: https://golang.org/cl/7196052 (このコミットの変更リスト)
  • Go言語の公式ドキュメント (testing, flag, expvar, net/http パッケージの解説)
  • Go言語のテストに関する一般的なベストプラクティスに関する記事やドキュメント