Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

フレーキーテストをCIで検知・アラートさせる仕組みを作った話(JUnit, Vitest)

明けましておめでとうございます。
2024年は皆さんにとってどんな年だったでしょうか。
私たちSansanでは、本社移転により業務環境が大きく変わり、特に印象的な年になりました。
2025年も、より素晴らしい年になるように頑張りたいですね。

こんにちは。技術本部 Contract One Dev の川崎です。

現場の習慣を変える、契約データベース「Contract One」の開発業務を担当しています。

フレーキーテストをご存知でしょうか。

JetBrains社の記事にフレーキーなテストとは以下と定義すると記載があります。

*1Flaky なテストは、コードまたはテストそのものに変更がないにもかかわらず、合格と不合格の両方を返すテストであると定義されています。

つまりざっくり言うと、実行の度に成功したり失敗したりするテストのことを指してるわけですね。

Contract Oneにおいてもテストが度々フレーキーになることがあり、その扱いに困ることがありました。
そこでContract Oneなりのフレーキーテスト対策を導入しましたので、そのお話を本記事にまとめてみようと思います。

フレーキーテストが存在することによる問題点

テストの不安定さは心の不安定さ

フレーキーテストが存在することによる問題点は、多くの開発チームが直面する共通のものだと考えています。
具体的には以下の2点のような問題点です。

1. 生産性の低下:
フレーキーテストが多発すると、開発者はテスト結果の信頼性を疑い、問題のあるテストを再実行する必要が生じます。この過程での手間と時間のロスは、生産性を著しく低下させます。また、原因特定のためのデバッグ作業が増えることで、開発のスピードが落ちるだけでなく、チームの士気にも影響を与えます。
またフレーキーテストは修正しないとずっとソースコードに残りつづけます。
CI/CDパイプラインでテストがたまに失敗するという事態が残り続けるため、結果として今後ずっと機能リリースが少しずつ遅れていく原因に繋がります。
2. 信頼性の低下:
テストの結果が一貫しないと、コードの品質に対する信頼が揺らぎます。本来ならば、テストはコードの動作を保証するものであるはずですが、結果が安定しないために、その役割を果たせなくなります。

私たちが開発している「Contract One」でも、フレーキーテストは無視できない問題として存在していました。

CI上の自動テストでフレーキーテストを検知して警告させる

Contract Oneの開発チームでは、これらの課題に対する施策を考案、実装し取り入れることで、フレーキーテストを管理し最小限に抑えこみました。

具体的には以下の仕組みをつくりました。

  1. CI上で失敗したテストを一回だけ再実行させる
  2. テストの結果をレポート出力させる
  3. 出力させたレポートを解析してフレーキーテストを検知する
  4. フレーキーテストが検知された場合、PRのコメントを投稿して警告する
PRにフレーキーテストが警告されている様子

ライブラリ毎の検知の仕組み

JUnit

Contract OneのバックエンドはKotlinでビルドツールはGradle、テストライブラリはJUnitを利用しています。
JUnit単体では失敗したテストをリトライする機能がないため、*2gradle/test-retry-gradle-pluginというリトライプラグインを導入しました。

あとはテストレポートを解析して警告するのですが、JUnitのテストレポートは一回失敗したテストもテストレポートに載せてくれるので「1回失敗して、2回目は成功したテスト」をGithub Actionsのカスタムアクションで検知して警告させることで実現しました。

Vitest

フロントエンドの自動テストはVitestを採用しています。
Vitestについてはそれ自体にリトライ機能が備わっているのですが、出力したレポートに「リトライによって成功したテスト」の情報が載らないという問題があります。
なのでvitestのグローバル設定で*3aftereachの際に該当テストが「リトライしたかどうか」また「成功・失敗したかどうか」を取得、保存するようにコードを書いてます。
つまりフレーキーテストかどうかを判定してフレーキーテストだけJSONLに出力させています。
具体的なコードは次になります。

afteEach((context) => {
  reportFlakyTest(context)
})

const reportFlakyTest = (context: TaskContext) => {
  const isFlakyTest =
    context.task.result?.state === "pass" && context.task.result.retryCount && context.task.result.retryCount > 0
  if (!isFlakyTest) return

  const outputPath = "./logs/test-results/flaky-tests-report.jsonl"

  const flakyTest = {
    file: context.task.suite?.file?.name,
    name: context.task.name,
    result: context.task.result,
  }
  appendFileSync(outputPath, `${JSON.stringify(flakyTest)}\n`)
}

あとは出力されたJSONLをgithub actions上で検知し、PRにコメントさせれば完成です。

検知できることによって変わった開発文化

フレーキーテストが検知できるようになったことで、ContractOneでは以下のような文化が自然と生まれ前述した問題点がどんどん改善されていってます。

フレーキーテストを修正する活動

どのテストがフレーキーなのかが明確に検知してPRコメントという形で残るようになりました。
明確になったフレーキーテストの修正を開発メンバーがタスクとして管理できるようになったためプロジェクトの間など時間の余裕があるときに修正されていく文化ができています。

フレーキーテストが修正されているPRの様子

フレーキーテストが発生しづらいテストの書き方・仕組みを共有・実装する

テストがフレーキーになっていた原因を探る中で同じ原因で問題が再発しないためにテストの共通実装を修正したり、気をつける点を共有したりする文化ができました。
例えばですが、ContractOneでフレーキーテストの原因の1つがモックの差し込み・解除方法というのがわかった際は、安全にモックを使えるように共通実装を変えたりスタイルガイドを加筆したりしました。


こういった文化の形成もあいまってフレーキーテストはかなり減少し、ストレスレスで生産性の高い開発に取り組めるようになっています。

まとめ

今回はContract One開発チームのフレーキーテスト対策を紹介しました。
この対策があることでフレーキーテストが減り開発生産性が上がったので、もしフレーキーテストにお困りであれば、似たような仕組みを導入してみてはいかがでしょうか。

Contract OneではPMFを達成しグロース期に入ったため一段と開発スピードをあげていく必要があります。
そこで、一緒に働く仲間を絶賛募集中になります!
もしご興味があればカジュアル面談からで大丈夫ですのでご連絡ください。
詳しくは以下のリンクから採用情報をご確認ください。

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途・新卒採用向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話します。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

© Sansan, Inc.