[インデックス 14963] ファイルの概要
このコミットは、Go言語の標準ライブラリ testing
パッケージに、テストをスキップするための新しいメソッド Skip
および Skipf
を追加するものです。これにより、テストの実行を途中で中断し、そのテストがスキップされたことを明示的に報告する機能が導入されました。
コミット
commit f7ea900a7bfe88cdfc42448d10a4f9bfa83cc36f
Author: Dave Cheney <dave@cheney.net>
Date: Wed Jan 23 10:22:33 2013 +1100
testing: add Skip/Skipf
This proposal adds two methods to *testing.T, Skip(string) and Skipf(format, args...). The intent is to replace the existing log and return idiom which currently has 97 cases in the standard library. A simple example of Skip would be:
func TestSomethingLong(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
// not reached
}
... time consuming work
}
Additionally tests can be skipped anywhere a *testing.T is present. An example adapted from the go.crypto/ssh/test package would be:
// setup performs some before test action and returns a func()
// which should be defered by the caller for cleanup.
func setup(t *testing.T) func() {
...
cmd := exec.Command("sshd", "-f", configfile, "-i")
if err := cmd.Run(); err != nil {
t.Skipf("could not execute mock ssh server: %v", err)
}
...
return func() {
// stop subprocess and cleanup
}\n }
func TestDialMockServer(t *testing.T) {
cleanup := setup(t)
defer cleanup()\n ...
}
In verbose mode tests that are skipped are now reported as a SKIP, rather than PASS.
Link to discussion: https://groups.google.com/d/topic/golang-nuts/BqorNARzt4U/discussion
R=adg, rsc, r, n13m3y3r
CC=golang-dev, minux.ma
https://golang.org/cl/6501094
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f7ea900a7bfe88cdfc42448d10a4f9bfa83cc36f
元コミット内容
このコミットは、*testing.T
型に Skip(string)
と Skipf(format, args...)
の2つのメソッドを追加することを提案しています。これらのメソッドの目的は、標準ライブラリ内で97箇所に存在する、既存の「ログを出力してリターンする」というテストスキップの慣用句を置き換えることです。
Skip
の簡単な例としては、testing.Short()
モードで時間のかかるテストをスキップするケースが挙げられます。また、*testing.T
が利用可能な場所であればどこでもテストをスキップできるようになります。例えば、テストのセットアップ中に外部コマンドの実行に失敗した場合などにも利用できます。
この変更により、詳細モード (-v
フラグ) で実行された場合、スキップされたテストは PASS
ではなく SKIP
として報告されるようになります。
変更の背景
Go言語のテストフレームワークでは、これまでテストを条件付きでスキップする際に、t.Log("skipping test")
のようにログを出力し、その後に return
でテスト関数を終了させるという慣用的な方法が広く使われていました。しかし、この方法はいくつかの課題を抱えていました。
- 冗長性: スキップのたびにログ出力とリターンを記述する必要があり、コードが冗長になりがちでした。
- 不明瞭な結果: テストがスキップされた場合でも、テストスイートの最終的な結果としては
PASS
と表示されていました。これは、実際にテストが実行されなかったにもかかわらず、成功したかのように見えるため、テスト結果の解釈を曖昧にする可能性がありました。特に、CI/CD環境などでは、スキップされたテストが明確に識別できないと、問題の特定が困難になることがあります。 - 一貫性の欠如: スキップの理由をログに出力する形式が統一されておらず、開発者によって記述方法が異なる場合がありました。
このコミットは、これらの課題を解決するために、testing.T
に Skip
および Skipf
メソッドを導入することを提案しました。これにより、テストのスキップがより簡潔に、かつ明確に表現できるようになり、テスト結果の報告も改善されます。特に、SKIP
という新しいステータスが導入されることで、テストが意図的に実行されなかったことが一目でわかるようになります。
前提知識の解説
このコミットを理解するためには、Go言語の標準テストパッケージ testing
の基本的な概念と、テストの実行に関する知識が必要です。
testing
パッケージ: Go言語に組み込まれているテストフレームワークです。ユニットテスト、ベンチマークテスト、例 (Examples) を記述するために使用されます。*testing.T
: テスト関数に渡される構造体で、テストの実行状態を管理し、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。- テスト関数:
func TestXxx(t *testing.T)
の形式で定義される関数で、go test
コマンドによって実行されます。 testing.Short()
:go test -short
フラグが指定された場合にtrue
を返す関数です。時間のかかるテストを、開発中の短いテスト実行時にはスキップするために利用されます。go test -v
: テストを詳細モードで実行するためのフラグです。各テストの実行結果(PASS/FAIL/SKIP)やログ出力が詳細に表示されます。runtime.Goexit()
: 現在のゴルーチンを終了させる関数です。panic
とは異なり、defer関数は実行されますが、呼び出し元の関数には制御が戻りません。テストのスキップにおいて、テスト関数の残りの部分が実行されないようにするために使用されます。
技術的詳細
このコミットの主要な技術的変更点は、testing.T
型に新しいフィールドとメソッドを追加し、テストのスキップ機能を正式にサポートすることです。
-
skipped
フィールドの追加:testing.T
構造体にskipped bool
という新しいフィールドが追加されました。このフィールドは、現在のテストがスキップされたかどうかを示すフラグとして機能します。type T struct { common name string // Name of test. startParallel chan bool // Parallel tests will wait on this. skipped bool // Test has been skipped. }
-
Skip
およびSkipf
メソッドの追加: これらのメソッドは、テストをスキップするための主要なエントリポイントです。Skip(args ...interface{})
:t.Log()
と同様に引数を結合してログに出力し、その後t.SkipNow()
を呼び出してテストの実行を停止します。Skipf(format string, args ...interface{})
:t.Logf()
と同様にフォーマットされた文字列をログに出力し、その後t.SkipNow()
を呼び出してテストの実行を停止します。
-
SkipNow()
メソッドの追加: このメソッドは、テストをスキップ済みとしてマークし、現在のテスト関数の実行を直ちに停止します。- 内部的には
t.skip()
を呼び出してt.skipped
フラグをtrue
に設定します。 - その後、
runtime.Goexit()
を呼び出して現在のゴルーチン(テスト関数)を終了させます。これにより、SkipNow()
が呼び出された後のテストコードは実行されなくなります。
- 内部的には
-
skip()
メソッドの追加:t.skipped
フラグを安全にtrue
に設定するための内部ヘルパーメソッドです。ミューテックス (t.mu
) を使用して、並行アクセスから保護します。 -
Skipped()
メソッドの追加: テストがスキップされたかどうかを報告するためのメソッドです。t.skipped
フラグの現在の値(読み取りロック付き)を返します。 -
report()
メソッドの変更:testing.T
のreport()
メソッドが変更され、テストがスキップされた場合にSKIP
というステータスを出力するようになりました。 以前は、テストが失敗しなかった場合は常にPASS
と報告されていましたが、この変更により、t.Skipped()
がtrue
を返す場合はSKIP
と表示されるようになります。func (t *T) report() { // ... (既存のコード) if t.Failed() { fmt.Printf(format, "FAIL", t.name, tstr, t.output) } else if *chatty { // -v フラグが指定されている場合 if t.Skipped() { // 新しく追加された条件 fmt.Printf(format, "SKIP", t.name, tstr, t.output) } else { fmt.Printf(format, "PASS", t.name, tstr, t.output) } } }
これらの変更により、Goのテストフレームワークは、テストのスキップをより明確かつ効率的に処理できるようになり、テスト結果の可読性も向上しました。
コアとなるコードの変更箇所
変更は src/pkg/testing/testing.go
ファイルに集中しています。
-
testing.go
のコメント更新: ファイルの冒頭のコメントに、t.Skip
を使用したテストスキップの例が追加されました。--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -10,6 +10,14 @@ // [a-z]) and serves to identify the test routine. // These TestXxx routines should be declared within the package they are testing. // +// Tests may be skipped if not applicable like this:\n+// func TestTimeConsuming(t *testing.T) {\n+// if testing.Short() {\n+// t.Skip("skipping test in short mode.")\n+// }\n+// ...\n+// }\n+// // Functions of the form // func BenchmarkXxx(*testing.B) // are considered benchmarks, and are executed by the "go test" command when
-
T
構造体へのskipped
フィールド追加:T
構造体にskipped bool
フィールドが追加されました。--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -185,6 +193,7 @@ type T struct {\n common\n name string // Name of test.\n startParallel chan bool // Parallel tests will wait on this.\n+ skipped bool // Test has been skipped.\n }\n ```
-
Failed
メソッドのコメント修正:Failed
メソッドのコメントが "returns whether the function has failed." から "reports whether the function has failed." に変更されました。これは小さな変更ですが、より正確な表現になっています。--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -194,7 +203,7 @@ func (c *common) Fail() {\n c.failed = true\n }\n \n-// Failed returns whether the function has failed.\n+// Failed reports whether the function has failed.\n func (c *common) Failed() bool {\n c.mu.RLock()\n defer c.mu.RUnlock()\
-
report()
メソッドの変更: テスト結果の出力ロジックが変更され、t.Skipped()
がtrue
の場合にSKIP
と表示されるようになりました。--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -328,10 +337,46 @@ func (t *T) report() {\n if t.Failed() {\n fmt.Printf(format, "FAIL", t.name, tstr, t.output)\n } else if *chatty {\n- fmt.Printf(format, "PASS", t.name, tstr, t.output)\n+ if t.Skipped() {\n+ fmt.Printf(format, "SKIP", t.name, tstr, t.output)\n+ } else {\n+ fmt.Printf(format, "PASS", t.name, tstr, t.output)\n+ }\n }\n }\n ```
-
Skip
,Skipf
,SkipNow
,skip
,Skipped
メソッドの追加: これらの新しいメソッドがT
型に追加されました。--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -328,10 +337,46 @@ func (t *T) report() {\n if t.Failed() {\n fmt.Printf(format, "FAIL", t.name, tstr, t.output)\n } else if *chatty {\n- fmt.Printf(format, "PASS", t.name, tstr, t.output)\n+ if t.Skipped() {\n+ fmt.Printf(format, "SKIP", t.name, tstr, t.output)\n+ } else {\n+ fmt.Printf(format, "PASS", t.name, tstr, t.output)\n+ }\n }\n }\n \n+// Skip is equivalent to Log() followed by SkipNow().\n+func (t *T) Skip(args ...interface{}) {\n+\tt.log(fmt.Sprintln(args...))\n+\tt.SkipNow()\n+}\n+\n+// Skipf is equivalent to Logf() followed by SkipNow().\n+func (t *T) Skipf(format string, args ...interface{}) {\n+\tt.log(fmt.Sprintf(format, args...))\n+\tt.SkipNow()\n+}\n+\n+// SkipNow marks the function as having been skipped and stops its execution.\n+// Execution will continue at the next test or benchmark. See also, t.FailNow.\n+func (t *T) SkipNow() {\n+\tt.skip()\n+\truntime.Goexit()\n+}\n+\n+func (t *T) skip() {\n+\tt.mu.Lock()\n+\tdefer t.mu.Unlock()\n+\tt.skipped = true\n+}\n+\n+// Skipped reports whether the function was skipped.\n+func (t *T) Skipped() bool {\n+\tt.mu.RLock()\n+\tdefer t.mu.RUnlock()\n+\treturn t.skipped\n+}\n+\n func RunTests(matchString func(pat, str string) (bool, error), tests []InternalTest) (ok bool) {\n ok = true\n if len(tests) == 0 && !haveExamples {\n```
コアとなるコードの解説
追加された各メソッドとフィールドの役割は以下の通りです。
-
skipped bool
(フィールド):*testing.T
のインスタンスに新しく追加されたブーリアン型のフィールドです。このテストがスキップされた場合にtrue
に設定されます。テストのライフサイクルを通じて、このフラグがテストの最終的な状態(スキップされたかどうか)を保持します。 -
func (t *T) Skip(args ...interface{})
: このメソッドは、テストをスキップする際に開発者が直接呼び出すことを意図しています。t.log(fmt.Sprintln(args...))
:t.Log()
と同様に、引数をスペース区切りで結合し、改行を追加してテストのログに出力します。これにより、テストがスキップされた理由がログに記録されます。t.SkipNow()
: ログ出力後、SkipNow()
を呼び出してテストの実行を直ちに停止し、スキップ済みとしてマークします。
-
func (t *T) Skipf(format string, args ...interface{})
:Skip
メソッドのフォーマット文字列版です。t.log(fmt.Sprintf(format, args...))
:t.Logf()
と同様に、フォーマット文字列と引数を使用してメッセージを整形し、テストのログに出力します。t.SkipNow()
: ログ出力後、SkipNow()
を呼び出してテストの実行を直ちに停止し、スキップ済みとしてマークします。
-
func (t *T) SkipNow()
: このメソッドは、テストをスキップ済みとしてマークし、現在のテスト関数の実行を停止する役割を担います。t.skip()
: 内部ヘルパーメソッドskip()
を呼び出し、t.skipped
フラグをtrue
に設定します。runtime.Goexit()
: 現在のゴルーチン(つまり、テスト関数を実行しているゴルーチン)を終了させます。これにより、SkipNow()
が呼び出された後のテスト関数のコードは実行されません。defer
ステートメントで登録された関数はGoexit()
の前に実行されます。
-
func (t *T) skip()
:t.skipped
フィールドをtrue
に設定するためのプライベートなヘルパーメソッドです。t.mu.Lock()
とdefer t.mu.Unlock()
を使用してミューテックスロックを取得・解放しており、t
構造体への並行アクセスからskipped
フィールドの更新を保護しています。これにより、複数のゴルーチンから同時にskip()
が呼び出された場合でも、データ競合が発生しないようにしています。 -
func (t *T) Skipped() bool
: このメソッドは、テストがスキップされたかどうかを外部から確認するために使用されます。t.mu.RLock()
とdefer t.mu.RUnlock()
を使用して読み取りロックを取得・解放しており、t.skipped
フィールドの値を安全に読み取ります。 -
func (t *T) report()
の変更: このメソッドは、テストの最終結果を標準出力に報告する役割を担っています。変更点では、*chatty
(つまり-v
フラグが指定されている場合) の条件分岐内で、t.Skipped()
がtrue
の場合にSKIP
という文字列を出力するようにロジックが追加されました。これにより、詳細モードでテストを実行した際に、スキップされたテストが明確にSKIP
と表示されるようになり、テスト結果の可読性が大幅に向上しました。
これらの変更により、Goのテストフレームワークは、テストのスキップをより簡潔に、かつ明確に表現できるようになり、テスト結果の報告も改善されました。
関連リンク
- Google Groups 議論: https://groups.google.com/d/topic/golang-nuts/BqorNARzt4U/discussion
- Go CL (Code Review): https://golang.org/cl/6501094
参考にした情報源リンク
./commit_data/14963.txt
(コミット情報)- Google Groups 議論 (上記リンク)
- Go CL (Code Review) (上記リンク)
- Go言語
testing
パッケージのドキュメント (一般的な知識として) runtime.Goexit()
のドキュメント (一般的な知識として)