突然発生した Flaky Testとの戦い

494 Views

December 12, 24

スライド概要

GoのプロダクトにおけるFlaky Testの事例紹介です。

DMM meetup #40 ~DMM.go × Think! FrontEnd~ の発表資料です。
https://dmm.connpass.com/event/335033/

profile-image

Go言語です

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

突然発生した Flaky Testとの戦い DMM.go #9 21新卒 登石 拓磨(Toishi Takuma) 2024-11-05 © DMM CONFIDENTIAL

2.

自己紹介 ● 2021年新卒 ● DMM.comプラットフォーム開発 本部 第三開発部 基盤開発 ● Gen2-GW の開発・運用を しています Takuma Toishi 登石 拓磨 2

3.

目次 1. 導入 ○ ○ Gen2-GWとは Flaky Testとは 2. Flaky Testのコード例 3. 調査方法で工夫した点 3

4.

Gen2-GWのテスト事情 ● Gen2-GWとは? ○ ○ 自社で開発しているAPI Gatewayです Go言語で実装しています ● API Gatewayを立てて設定の挙動を確認するテストがある ○ ○ ○ テストごとに、実際にHTTPサーバーが起動する なぜ必要か:API Gatewayの起動時に設定を読み込む ここにFlaky Testが発生した 4

5.

Flaky Testとは何か ● 実行ごとに結果が変わる不安定なテスト ○ コードに変更がなくても、失敗したり成功したりする ● Flaky Testを放置するとどうなるか ○ ○ 成功するまでテストを実行し直すことで、開発速度に悪影響 テストの失敗に対して鈍感になっていく 5

6.

2. Flaky Testのコード例

7.

問題となったテスト func TestHoge(t *testing.T) { // Gatewayを起動する // 設定を読み込む go runGateway(ctx, “:30000”) resp, err := http.Get("http://localhost:30000/ping") // 色々テスト } cancel() 7

8.
[beta]
問題となったテスト
// ctxで停止できるサーバ
// 実際にはサードパーティのライブラリにある関数
func runGateway(ctx context.Context, addr string) {
srv := &http.Server{Addr: addr}
// サーバ起動処理(略)

}

<-ctx.Done()
srv.Shutdown()

8

9.

問題となったテスト package pkg func TestHoge(t *testing.T){...} func TestFuga(t *testing.T){...} func TestPiyo(t *testing.T){...} ● ● ● サーバーを立てるテストが複数並んでいた 同一パッケージのテストは直列に実行されるので、 TestHoge→TestFuga→TestPiyoのように実行される 全てのテストで同じポートが使われていた 9

10.

どこに問題があるか(再掲) func TestHoge(t *testing.T) { go runGateway(ctx, “:30000”) // テストごとに設定(挙動)が異なる resp, err := http.Get("http://localhost:30000/ping") cancel() } // テストがすぐに終了する // サーバー終了処理が完了する前かもしれない 次のTestFugaが実行される → TestHogeで立てた Gatewayにリクエストが飛んでいく 10

11.

どうすればよかったのか ● テストごとに異なるポートを使う ○ ○ ○ サーバを立てる部分がライブラリでラップされているので、 ランダムに割り当てるのは難しい 手動で異なるポートを使うように実装することはできるが、 人間が重複しないように気をつけないといけない ただこれをやらないと、テストの並列実行ができない問題は残る ● サーバの終了を待つ ○ ○ 良さそう 今回のケースではこれを実装しました 11

12.
[beta]
修正後 (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

13.
[beta]
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

14.

3. 調査方法で工夫した点

15.

調査の取り組み方(工夫) 1. CIで大量にテストを実行して、再現を試みる 2. (気合い)テストのログをよく見る 15

16.

CIで大量にテストを実行して、再現を試みる ● 手元のPCでは再現しなかった ● 20回中2回のテストが失敗した CircleCIでJobを並列に実行 16

17.

(気合い)テストのログをよく見る ● go testのログは人間には読みづらい ● CI上でのテストにgotestsumを利用 ○ ○ ○ cimg/go(CircleCI製のDockerイメージ)にデフォルトで入っているツール JUnit XML形式でテスト結果を出力可能 CircleCIにこれを食わせると、テストタブで結果が見られる gotestsum --format standard-verbose --junitfile test-results.xml ./... 17

18.

(根気)テストのログをよく見る ● CircleCIの画面の一部 18

19.

学び Flaky Testを引き起こしたコードの紹介と、Flaky Testへの取り組み方を紹介 しました ● goroutineを使用するテストでは、終了処理に注意する ○ goroutineの中で片付けをする場合は、テストの終了を待つ ● 片付けの処理は t.Cleanupにまとめる ○ ○ 処理の見通しが良くなる 片付けの処理を忘れることがなくなる ● (可能なら )そもそも同じポートを使う楽をしない ● Go言語にある機能を適切に使うことが、Flakyさを低減していくことにつな がる

20.

ありがとうございました