deep dive into testing/synctest
by
Daiki Kubo
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
deep dive into testing/synctest 久保大貴
Slide 2
Slide 2 text
Daiki Kubo CyberAgent / Tapple プロフィールを見る 久保大貴 クボ ダイキ #イラスト バックエンドエンジニア 職種 紹介 趣味 CA.go や go conference JP 等の運営メンバー ゲーム・AI分野において JSAI や IEEE Conference On Games 等での 研究論文も出している 登壇系は苦手な分野で今年は苦手な登壇を克服するを目標にしてい ました。その中で今年は Go Conference に登壇する目標をおいていた ので、無事叶って嬉しいです!頑張ります! #ゲーム 24年度入社 入社年次
Slide 3
Slide 3 text
1.同期 vs 非同期 これらのテストの課題とは 2.testing/synctestの紹介 3.Test(t *testing.T, f func t(*testing.T)) 4.Wait() 5.Durably Blockedとは
Slide 4
Slide 4 text
注意事項 - 説明用にコードを簡略化しているところがあります。 - スライドにコードを載せるのに限界があり・・ - スライドは発表終了後に slideshare で公開します。 - 後の席の方などで見にくかった場合は slideshare などで見ていただけると嬉 しいです!
Slide 5
Slide 5 text
同期 vs 非同期 これらのテストの課題とは
Slide 6
Slide 6 text
同期テストの典型例 - 同期的なプログラムの結果は予測可能 - 呼ぶ → 処理 → 返る
Slide 7
Slide 7 text
同期テストの典型例 - 同期的なプログラムの結果は予測可能 - 呼ぶ → 処理 → 返る
Slide 8
Slide 8 text
非同期の典型フローに関して - 非同期的なコードは結果の予測をしにくい場面が多い - 呼ぶ → 返る → 何かが起きる など
Slide 9
Slide 9 text
非同期テストの課題 - 例えば、context.WithDeadlineを使用したテスト - キャンセルされるかのテストをしたい
Slide 10
Slide 10 text
非同期テストの課題 - 1秒後にタイムアウトする contextを作成 - timeoutまで待つ - その後、コンテキストがキャンセルされているか確認する
Slide 11
Slide 11 text
非同期テストの課題 - 1秒後にタイムアウトする contextを作成 - timeoutまで待つ - その後、コンテキストがキャンセルされているか確認する 成功してい る!
Slide 12
Slide 12 text
非同期テストの課題 - 1秒後にタイムアウトする contextを作成 - timeoutまで待つ - その後、コンテキストがキャンセルされているか確認する あれ、何回か やったら失敗 してる・・
Slide 13
Slide 13 text
非同期テストの課題 - 成功したり、失敗したり不安定な挙動になってしまう
Slide 14
Slide 14 text
非同期テストの課題 - これに対して、 sleep時間をもう少し長く持つことも可能 - ただ、テストの実行時間は長くなってしまう なんか失敗する から時間を延長 してみよう!
Slide 15
Slide 15 text
非同期テストの課題 ● 安定さは増すがテストの実行時間は遅くなる ○ Clock型を作成してテストの時間進行を操作等はできるが複雑性は増す ● 不安定さは必ずしも消えるとは言い切れない ○ 実際の時間やスケジューリング等の外部影響を受ける可能性がある ■ GCによる一時停止 ■ OSスケジューラの遅延 ■ システムコールの遅延 ■ CI環境やチーム開発内等 ■ etc…なんか失敗するから時間を延長してみよう!
Slide 16
Slide 16 text
非同期テストの課題 例2:定期的に pollingする - 動画のトランスコード - 動画トランスコードの状態 を定期的に pollingしjobが 完了するまで待つ - tickの度にトランスコード のjobの状態を確認し、完 了したらHLS配信用を永続 化
Slide 17
Slide 17 text
非同期テストの課題 例2:定期的に pollingする テストしたいケース : - 実際にtickするたびにどの状態に切り替わるか - tickerの中の全ての動作を網羅的にテストしたい 課題 - 毎tick毎の状態を確認したり、 timeout後のエラー処理などを sleepなどで待 つ必要がありテストで確認しづらい
Slide 18
Slide 18 text
非同期テストの課題 例3:ゴルーチンの完了を待機する - サムネイル画像の生成 - 動画IDに対してサムネ画像を生成 する - 全てのゴルーチンが完了するまで 待機する
Slide 19
Slide 19 text
非同期テストの課題 例3:ゴルーチンの完了を待機する テストしたいケース: - 動画のNフレーム目の画像が生成されるか - 画像の生成などを含めて一貫した動作の確認をしたい 課題 - ゴルーチンがいつ完了するかわからない
Slide 20
Slide 20 text
非同期テストの課題 - 非同期処理のテストにおいて、タイミングや時間を制御するのが難しい。 - 実際のシステムクロックとの同期の機構に依存しているため、複数のGoroutineが並行して動作する ため、実行のタイミングや順序を制御しにくい - これによりテスト毎に異なる結果が生じる可能性があり、安定性の低いテスト(不確実なテスト) となってしまう - 実行タイミングを制御するために、time.Sleepなどを使い無理やり同期させる必要があったり ・・ - テストロジックがTimerの状態を確認する等で複雑になる → 本来のテストのロジックにfocusできな い
Slide 21
Slide 21 text
非同期テストの課題 - その結果、トレードオフを考えないといけなくなる - シンプルさを保てているか - テストの実行時間を考慮できているか - 安定性を考慮できているか (Flakyなテストではないか) - テストを安定させるために長い待機時間を入れる → テストが遅くなる - テストを高速化するために待機時間を短くする → テストが不安定になる - テストを安定させるためにテストのための余分なロジックを追加する → シン プルさに欠ける - どちらも同時に達成するのは困難 で、どちらかを犠牲にするトレードオフが発 生する
Slide 22
Slide 22 text
新たな選択肢 - Go 1.24でexperimentalの状態で synctest というものが現れた - Go 1.25 で正式リリース
Slide 23
Slide 23 text
非同期テストの課題 例2:定期的に pollingする テストしたいケース : - 実際にtickするたびにどの状態に切り替わるか - tickerの中の全ての動作を網羅的にテストしたい 課題 - 毎tick毎の状態を確認したり、 timeout後のエラー処理などを sleepなどで待 つ必要がありテストで確認しづらい
Slide 24
Slide 24 text
非同期テストの課題 sleepを使ったテスト synctestを使ったテスト
Slide 25
Slide 25 text
非同期テストの課題 sleepを使ったテスト(35s) synctestを使ったテスト(0s)
Slide 26
Slide 26 text
testing/synctest
Slide 27
Slide 27 text
go 1.24時点のtesting/synctest ● Go 1.24時点では実験的にリリースされた新しいパッケージ ○ 実験的にリリースされているので、GOEXPERIMENT=synctest を 設定する必要があった
Slide 28
Slide 28 text
Go 1.24時点のtesting/synctest
Slide 29
Slide 29 text
Go 1.24時点のtesting/synctest RunとWait 二つの APIが提供されてい る
Slide 30
Slide 30 text
Go 1.25でのtesting/synctest
Slide 31
Slide 31 text
Go 1.25でのtesting/synctest Go 1.24と Go 1.25での差分に気が付きましたでしょうか?
Slide 32
Slide 32 text
Go 1.25でのtesting/synctest Go 1.24と Go 1.25での差分に気が付きましたでしょうか?
Slide 33
Slide 33 text
Go 1.25でのtesting/synctest RunはDeprecatedになりました。 Testを代わりに使 うよう推奨されています。 Run自体は Go 1.26で削除される予定です
Slide 34
Slide 34 text
なぜRunはdeprecatedになったのか ただ名前を変更しただけでなく、内部も変わっている synctest.Runはtestingパッケージとうまく噛み合っていなかった: - T.Cleanup: bubble内で登録した cleanup がbubble外で実行される。cleanup をbubble内 goroutine の終了に使うと問題になる - T.Context: T.Context は“bubbleに紐付かない Done”を持つ context を返す ため、bubble内で使いたい挙動ではない
Slide 35
Slide 35 text
testing/synctestが提供するAPI
Slide 36
Slide 36 text
testing/synctestが提供するAPI
Slide 37
Slide 37 text
Test(t *testing.T, f func t(*testing.T))
Slide 38
Slide 38 text
testing/synctest Test - Test(t, f)は新しいbubbleを作成しfを実行し、bubble内の 全てのゴルーチンの終了またはデッドロック検出までを担当 する - T.Cleanupはbubble内で実行し、T.Context()はbubble内の Contextを返します。 - T.Run や T.Parallel や T.Deadlineをbubble内で呼び出すの は禁止されています
Slide 39
Slide 39 text
testing/synctest Test - T.Run や T.Parallel や T.Deadlineを bubble内で呼び 出すのは禁止さ れています go/src/testing/testing.go
Slide 40
Slide 40 text
testing/synctest Test - T.Run や T.Parallel や T.Deadlineを bubble内で呼び出 すのは禁止されて います go/src/testing/testing.go
Slide 41
Slide 41 text
testing/synctest Test - T.Run や T.Parallel や T.Deadlineを bubble内で呼び出 すのは禁止されて います go/src/testing/testing.go
Slide 42
Slide 42 text
testing/synctest Test - bubbleの中に存在するゴルーチンは仮想時間を使います - 初期時間はUTC の 2000-01-01
Slide 43
Slide 43 text
testing/synctest Test - この仮想時間はbubble内の全てのゴルーチンがブロックさ れた時に初めて時間が進みます
Slide 44
Slide 44 text
testing/synctest Test - Test は関数 f を新しいゴ ルーチンの中で実行する - bubble 構造体を作成し、 root=gp、total=1、 running=1 をセット - gp.bubble=bubbleで呼び 出し元のゴルーチンを bubble内に隔離する src/runtime/synctest.go
Slide 45
Slide 45 text
testing/synctest Test - Test は関数 f を新しいゴ ルーチンの中で実行する - bubble 構造体を作成し、 root=gp、total=1、 running=1 をセット - gp.bubble=bubbleで呼び 出し元のゴルーチンを bubble内に隔離する src/runtime/synctest.go
Slide 46
Slide 46 text
testing/synctest Test - newprocでfを実行する新規 ゴルーチン(main)を作成し スケジューリング src/runtime/synctest.go
Slide 47
Slide 47 text
testing/synctest Test - 次に前進する時間がな い場合やbubble.main のゴルーチンの処理が 終了するまでループさ れます (bubble.done) src/runtime/synctest.go
Slide 48
Slide 48 text
testing/synctest Test - bubbleの中の全ての ゴルーチンがブロック された時にbubble内 のrootのゴルーチンが 返される src/runtime/synctest.go
Slide 49
Slide 49 text
testing/synctest Test src/runtime/synctest.go
Slide 50
Slide 50 text
testing/synctest Test - 次に進む時間を取得 し、その分の時間を進 める src/runtime/synctest.go
Slide 51
Slide 51 text
testing/synctest Test - bubbleに所属してい る、ゴルーチンは bubble内の仮想時間 を見る src/runtime/time.go
Slide 52
Slide 52 text
testing/synctest Test
Slide 53
Slide 53 text
testing/synctest Test まとめ - Test は関数 f を新しいゴルーチンの中で実行する - 新しいゴルーチンとそれから間接的に開始されたゴルーチン は bubble を形成します - Test は bubble の中のすべてのゴルーチンの終了または Deadlockの検出をするまで待つ - bubbleの中に存在するゴルーチンは仮想時間を使います - 初期時間はUTC の 2000-01-01 - この仮想時間はbubble内のゴルーチンがブロックされた時 に初めて時間が進みます
Slide 54
Slide 54 text
Wait()
Slide 55
Slide 55 text
testing/synctest Wait - Wait は、生成されたbubbleにおいて現在のゴルーチン以外 の全てのゴルーチンがDurably Blockedという状態になるま でブロックし続けます。 - 同一bubble内で同時に複数のWaitを呼び出す場合 bubble 外の別ゴルーチンの呼び出しなどはパニックする - bubble 内の他のゴルーチンによってのみブロックを解除で きる場合、そのゴルーチンは Durably Blocked な状態とさ れる
Slide 56
Slide 56 text
testing/synctest Wait 1: タイマーがnow以前に到 来済みならrootを起こし て、タイマー処理を進める 2: ゴルーチンがWait()によ りブロックされている場合 は起こす 3: bubble内の全てのゴルー チンがdurably blockedなら rootのゴルーチンを返す src/runtime/synctest.go
Slide 57
Slide 57 text
testing/synctest Wait 1: タイマーがnow以前に到 来済みならrootを起こし て、タイマー処理を進める 2: ゴルーチンがWait()によ りブロックされている場合 は起こす 3: bubble内の全てのゴルー チンがdurably blockedなら rootのゴルーチンを返す src/runtime/synctest.go
Slide 58
Slide 58 text
testing/synctest Wait
Slide 59
Slide 59 text
testing/synctest Wait
Slide 60
Slide 60 text
testing/synctest Wait
Slide 61
Slide 61 text
testing/synctest Wait
Slide 62
Slide 62 text
testing/synctest Wait
Slide 63
Slide 63 text
testing/synctest Wait
Slide 64
Slide 64 text
testing/synctest Wait
Slide 65
Slide 65 text
testing/synctest Wait
Slide 66
Slide 66 text
Durably Blocked とは
Slide 67
Slide 67 text
testing/synctest durably blockedの定義 - bubble外のイベントでは解除されないブロック状態 - 例えば: - time.Sleep - bubble内のchannelに対してのsend/receive - selectのケースがbubble内のchannel - sync.Cond.Wait - など・・
Slide 68
Slide 68 text
testing/synctest durably blockedの定義 Durably Blockedではないもの: - bubble外のイベントやゴルーチンによって解除されうるも の これによって何が起きるか: - 仮想時間が進まず、panicやタイムアウトで終了する - bubble がidleとみなされず、waitは永遠にブロックされる
Slide 69
Slide 69 text
testing/synctest non durably blockedの例
Slide 70
Slide 70 text
testing/synctest Expect: 100-continue
Slide 71
Slide 71 text
testing/synctest Expect: 100-continue https://pkg.go.dev/testing/synctest#hdr-Example__HTTP_100_Continue
Slide 72
Slide 72 text
終わりに
Slide 73
Slide 73 text
まとめ - 隔離性 (Bubble) - 決定性 (Durably Blocked) - 非同期処理のテストにおけるFlakyや実行時間問題の解消
Slide 74
Slide 74 text
ありがとうございました