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

[インデックス 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 でテスト関数を終了させるという慣用的な方法が広く使われていました。しかし、この方法はいくつかの課題を抱えていました。

  1. 冗長性: スキップのたびにログ出力とリターンを記述する必要があり、コードが冗長になりがちでした。
  2. 不明瞭な結果: テストがスキップされた場合でも、テストスイートの最終的な結果としては PASS と表示されていました。これは、実際にテストが実行されなかったにもかかわらず、成功したかのように見えるため、テスト結果の解釈を曖昧にする可能性がありました。特に、CI/CD環境などでは、スキップされたテストが明確に識別できないと、問題の特定が困難になることがあります。
  3. 一貫性の欠如: スキップの理由をログに出力する形式が統一されておらず、開発者によって記述方法が異なる場合がありました。

このコミットは、これらの課題を解決するために、testing.TSkip および 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 型に新しいフィールドとメソッドを追加し、テストのスキップ機能を正式にサポートすることです。

  1. 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.
    }
    
  2. Skip および Skipf メソッドの追加: これらのメソッドは、テストをスキップするための主要なエントリポイントです。

    • Skip(args ...interface{}): t.Log() と同様に引数を結合してログに出力し、その後 t.SkipNow() を呼び出してテストの実行を停止します。
    • Skipf(format string, args ...interface{}): t.Logf() と同様にフォーマットされた文字列をログに出力し、その後 t.SkipNow() を呼び出してテストの実行を停止します。
  3. SkipNow() メソッドの追加: このメソッドは、テストをスキップ済みとしてマークし、現在のテスト関数の実行を直ちに停止します。

    • 内部的には t.skip() を呼び出して t.skipped フラグを true に設定します。
    • その後、runtime.Goexit() を呼び出して現在のゴルーチン(テスト関数)を終了させます。これにより、SkipNow() が呼び出された後のテストコードは実行されなくなります。
  4. skip() メソッドの追加: t.skipped フラグを安全に true に設定するための内部ヘルパーメソッドです。ミューテックス (t.mu) を使用して、並行アクセスから保護します。

  5. Skipped() メソッドの追加: テストがスキップされたかどうかを報告するためのメソッドです。t.skipped フラグの現在の値(読み取りロック付き)を返します。

  6. report() メソッドの変更: testing.Treport() メソッドが変更され、テストがスキップされた場合に 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 ファイルに集中しています。

  1. 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
    
  2. 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    ```
    
    
  3. 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()\
    
  4. 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    ```
    
    
  5. 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{}): このメソッドは、テストをスキップする際に開発者が直接呼び出すことを意図しています。

    1. t.log(fmt.Sprintln(args...)): t.Log() と同様に、引数をスペース区切りで結合し、改行を追加してテストのログに出力します。これにより、テストがスキップされた理由がログに記録されます。
    2. t.SkipNow(): ログ出力後、SkipNow() を呼び出してテストの実行を直ちに停止し、スキップ済みとしてマークします。
  • func (t *T) Skipf(format string, args ...interface{}): Skip メソッドのフォーマット文字列版です。

    1. t.log(fmt.Sprintf(format, args...)): t.Logf() と同様に、フォーマット文字列と引数を使用してメッセージを整形し、テストのログに出力します。
    2. t.SkipNow(): ログ出力後、SkipNow() を呼び出してテストの実行を直ちに停止し、スキップ済みとしてマークします。
  • func (t *T) SkipNow(): このメソッドは、テストをスキップ済みとしてマークし、現在のテスト関数の実行を停止する役割を担います。

    1. t.skip(): 内部ヘルパーメソッド skip() を呼び出し、t.skipped フラグを true に設定します。
    2. 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のテストフレームワークは、テストのスキップをより簡潔に、かつ明確に表現できるようになり、テスト結果の報告も改善されました。

関連リンク

参考にした情報源リンク

  • ./commit_data/14963.txt (コミット情報)
  • Google Groups 議論 (上記リンク)
  • Go CL (Code Review) (上記リンク)
  • Go言語 testing パッケージのドキュメント (一般的な知識として)
  • runtime.Goexit() のドキュメント (一般的な知識として)