494 Views
December 12, 24
スライド概要
GoのプロダクトにおけるFlaky Testの事例紹介です。
DMM meetup #40 ~DMM.go × Think! FrontEnd~ の発表資料です。
https://dmm.connpass.com/event/335033/
突然発生した Flaky Testとの戦い DMM.go #9 21新卒 登石 拓磨(Toishi Takuma) 2024-11-05 © DMM CONFIDENTIAL
自己紹介 ● 2021年新卒 ● DMM.comプラットフォーム開発 本部 第三開発部 基盤開発 ● Gen2-GW の開発・運用を しています Takuma Toishi 登石 拓磨 2
目次 1. 導入 ○ ○ Gen2-GWとは Flaky Testとは 2. Flaky Testのコード例 3. 調査方法で工夫した点 3
Gen2-GWのテスト事情 ● Gen2-GWとは? ○ ○ 自社で開発しているAPI Gatewayです Go言語で実装しています ● API Gatewayを立てて設定の挙動を確認するテストがある ○ ○ ○ テストごとに、実際にHTTPサーバーが起動する なぜ必要か:API Gatewayの起動時に設定を読み込む ここにFlaky Testが発生した 4
Flaky Testとは何か ● 実行ごとに結果が変わる不安定なテスト ○ コードに変更がなくても、失敗したり成功したりする ● Flaky Testを放置するとどうなるか ○ ○ 成功するまでテストを実行し直すことで、開発速度に悪影響 テストの失敗に対して鈍感になっていく 5
2. Flaky Testのコード例
問題となったテスト func TestHoge(t *testing.T) { // Gatewayを起動する // 設定を読み込む go runGateway(ctx, “:30000”) resp, err := http.Get("http://localhost:30000/ping") // 色々テスト } cancel() 7
問題となったテスト
// ctxで停止できるサーバ
// 実際にはサードパーティのライブラリにある関数
func runGateway(ctx context.Context, addr string) {
srv := &http.Server{Addr: addr}
// サーバ起動処理(略)
}
<-ctx.Done()
srv.Shutdown()
8
問題となったテスト package pkg func TestHoge(t *testing.T){...} func TestFuga(t *testing.T){...} func TestPiyo(t *testing.T){...} ● ● ● サーバーを立てるテストが複数並んでいた 同一パッケージのテストは直列に実行されるので、 TestHoge→TestFuga→TestPiyoのように実行される 全てのテストで同じポートが使われていた 9
どこに問題があるか(再掲) func TestHoge(t *testing.T) { go runGateway(ctx, “:30000”) // テストごとに設定(挙動)が異なる resp, err := http.Get("http://localhost:30000/ping") cancel() } // テストがすぐに終了する // サーバー終了処理が完了する前かもしれない 次のTestFugaが実行される → TestHogeで立てた Gatewayにリクエストが飛んでいく 10
どうすればよかったのか ● テストごとに異なるポートを使う ○ ○ ○ サーバを立てる部分がライブラリでラップされているので、 ランダムに割り当てるのは難しい 手動で異なるポートを使うように実装することはできるが、 人間が重複しないように気をつけないといけない ただこれをやらないと、テストの並列実行ができない問題は残る ● サーバの終了を待つ ○ ○ 良さそう 今回のケースではこれを実装しました 11
修正後 (channelを使う)
func TestHoge(t *testing.T) {
closed := make(chan struct{})
go func() {
runGateway(ctx, “:30000”)
close(closed)
}
resp, err := http.Get("http://localhost:30000/ping")
cancel()
}
// サーバーの終了を待つ
<-closed
12
t.Cleanupを使ってさらにまとめる
runをテストごとに呼び出せば良い
func run(t *testing.T, ctx context.Context) {
closed := make(chan struct{})
go func() {
runGateway(ctx, “:30000”)
close(closed)
}
}
t.Cleanup(func() { // テスト終了時に実行
cancel()
<-closed
})
13
3. 調査方法で工夫した点
調査の取り組み方(工夫) 1. CIで大量にテストを実行して、再現を試みる 2. (気合い)テストのログをよく見る 15
CIで大量にテストを実行して、再現を試みる ● 手元のPCでは再現しなかった ● 20回中2回のテストが失敗した CircleCIでJobを並列に実行 16
(気合い)テストのログをよく見る ● go testのログは人間には読みづらい ● CI上でのテストにgotestsumを利用 ○ ○ ○ cimg/go(CircleCI製のDockerイメージ)にデフォルトで入っているツール JUnit XML形式でテスト結果を出力可能 CircleCIにこれを食わせると、テストタブで結果が見られる gotestsum --format standard-verbose --junitfile test-results.xml ./... 17
(根気)テストのログをよく見る ● CircleCIの画面の一部 18
学び Flaky Testを引き起こしたコードの紹介と、Flaky Testへの取り組み方を紹介 しました ● goroutineを使用するテストでは、終了処理に注意する ○ goroutineの中で片付けをする場合は、テストの終了を待つ ● 片付けの処理は t.Cleanupにまとめる ○ ○ 処理の見通しが良くなる 片付けの処理を忘れることがなくなる ● (可能なら )そもそも同じポートを使う楽をしない ● Go言語にある機能を適切に使うことが、Flakyさを低減していくことにつな がる
ありがとうございました