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

ありがとうございました