メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

新機能

testing/synctestパッケージに、time.Sleepとsynctest.Waitをまとめて呼ぶ便利関数Sleepを追加する

testing/synctest

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

testing/synctestパッケージに新しい関数Sleepを追加する。これはtime.Sleepsynctest.Waitを呼ぶだけの単純な関数だが、synctestバブル内のテストコードで両者をセットで呼ぶという頻出パターンを1回の呼び出しに置き換えられるようにするものである。

導入経緯

Issue #77169にて提案された。提案者は、synctestを使った実験の中で「time.Sleepだけ呼んでsynctest.Waitの呼び出しを忘れ、テストがracyになる」という間違いが頻発することに気づき、両者をまとめる便利関数の追加を提案した。

議論のハイライト

  • synctestパッケージの開発チームからは、この関数は元々のsynctest提案から「あまりに些細だから」という理由で外されていた経緯があるとコメントされた。一方でsynctestの開発者自身は「synctestを使うほぼすべてのテストでこの関数を書いている」と述べている。
  • レビュー会では「time.Sleepに自動的にWaitを含められないか」も検討されたが、Waitは同一バブル内で複数goroutineから同時に呼べない制約があるため採用されなかった。同様に「ルートgoroutineのみ特別扱いする」案も、synctestがどのgoroutineも特別扱いしない設計方針と矛盾するため見送られた。
  • ドキュメントでは、この関数に(synctestの他の機能と違い)「魔法」は一切なく、単に2つの呼び出しをまとめただけであることを明記する必要があるという指摘があった。
  • 議論を経て likely accept となり、その後 accepted として承認された。実装はgo.dev/cl/740066で行われている。

使用例

Before

package synctest_test

import (
	"context"
	"testing"
	"testing/synctest"
	"time"
)

func TestContextWithTimeout(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
		defer cancel()

		// タイムアウト直前まで待機する。
		time.Sleep(5*time.Second - time.Nanosecond)
		synctest.Wait() // 呼び忘れやすい
		if err := ctx.Err(); err != nil {
			t.Fatalf("timeout前にエラー: %v", err)
		}

		// 残り時間を待機する。
		time.Sleep(time.Nanosecond)
		synctest.Wait() // これも呼び忘れやすい
		if err := ctx.Err(); err != context.DeadlineExceeded {
			t.Fatalf("timeout後のエラーが期待と異なる: %v", err)
		}
	})
}

After

package synctest_test

import (
	"context"
	"testing"
	"testing/synctest"
	"time"
)

func TestContextWithTimeout(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
		defer cancel()

		// タイムアウト直前まで待機する。
		synctest.Sleep(5*time.Second - time.Nanosecond)
		if err := ctx.Err(); err != nil {
			t.Fatalf("timeout前にエラー: %v", err)
		}

		// 残り時間を待機する。
		synctest.Sleep(time.Nanosecond)
		if err := ctx.Err(); err != context.DeadlineExceeded {
			t.Fatalf("timeout後のエラーが期待と異なる: %v", err)
		}
	})
}

移行時の注意

synctest.Sleep(d)time.Sleep(d)に続けてsynctest.Wait()を呼ぶのと完全に等価であり、挙動を変えるものではない。既存のtime.Sleep+synctest.Waitの組み合わせは今後も動作し続けるため、置き換えは任意である。

実装解説

実装はsrc/testing/synctest/synctest.goにあり、内容は次の3行のみである。

func Sleep(d time.Duration) {
	time.Sleep(d)
	Wait()
}

同ファイル内のWait(src/testing/synctest/synctest.goの310〜312行目)は内部パッケージinternal/synctestWaitをそのまま呼び出しており、Sleepはそのさらに薄いラッパーとして追加されている。新しいランタイム機構は導入されておらず、既存のtime.SleepWaitを単純に合成しただけの実装である。

関連リンク