Timee Product Team Blog

タイミー開発者ブログ

さよなら Flaky Test!Devinと共に実現する、CI安定化への道

  • タイミーでは、Flaky Test がデプロイの妨げになることで開発効率が悪化していました
  • この問題を解決するため、AI エージェント「Devin」を活用し、Flaky Test の検出から修正プルリクエストの作成までを完全に自動化しました
  • 結果、CIは安定し、開発者は本来の業務に集中できるようになったことで、開発体験が向上しました

こんにちは!タイミーでバックエンドエンジニアとして働いている 福井 (bary822) です。

皆さんは Flaky Test に悩まされた経験はないでしょうか?

タイミーでも、Flaky Test によって CI の信頼性が低下し、開発者の貴重な時間を奪ってしまうという課題を抱えていました。

ある期間においては master ブランチにおけるテスト実行の 4.5% が Flaky Test によって失敗しており、20+回/日 の頻度でデプロイされていることを考えると1日に1名は CI を re-run せざるを得ない状態に陥っていました。

この記事では、根深い Flaky Test 問題を解決するために、Devin を活用して修正プルリクエストの作成を自動化し、CI の安定化と開発体験の向上を実現した取り組みについてご紹介します。

私たちが抱えていた課題

Flaky Test を放置すると開発チーム全体に様々な悪影響を及ぼします。私たちが直面していた主な課題は以下の通りです。

  • 開発効率の悪化: 問題ないはずの Pull Request の CI が Flaky Test によって失敗すると、開発者は本来不要な原因調査や CI の再実行に時間を費やすことになります。多くの人にとって一定の緊張が発生するデプロイ作業の一環として実行される CI が失敗するというのは精神的にも大きな負担となります。
  • デプロイの遅延: master ブランチの CI が失敗すると、デプロイ担当者はそれが Flaky Test によるものかどうかを切り分ける調査を強いられていました。 Flaky Test だった場合は再実行によってその場を凌ぐことが常態化しており、迅速な価値提供の妨げとなっていました。これは、ビジネスの機会損失にも繋がりかねません。
  • CI における品質保証の機能不全: テストの成否が不安定なため、master ブランチで既存機能が正しく動作することが保証されているかどうかを正確に判断できなくなります。私たちの場合はその多くが「テストは正しく書かれているが何らかの不安定な要因によって失敗することがある」ケースだったため、実質的な品質の低下はほとんど発生していませんでした。しかし、その反対(本来失敗すべきなのにたまたまパスしてしまう)が発生していたとしても「また Flaky か…」と見逃されてしまいコードの修正が遅れる可能性がありました。

これらの課題を解決し、開発者全員が安心して高速に開発を進められる環境を作るため、私たちは Flaky Test の早期発見と修正を自動化する仕組みの構築に乗り出しました。

解決策:Devin による Flaky Test 修正の完全自動化

私たちは、以下のステップで Flaky Test の特定から修正までを自動化する仕組みを考案しました。

  1. Flaky Test の自動検出: CI 環境で実行されるテストの実行データを Datadog (Test Optimization)に送信し、Flaky Test を自動で検出する
  2. Devin による修正PRの作成: 新しい Flaky Test が検出されると、それをトリガーに GitHub Actions のワークフローを起動する。ワークフローは、テストファイル名やエラーメッセージなどの情報をプロンプトに含めて API 経由で Devin に渡し、修正プルリクエストの作成を依頼する。
  3. レビューとマージ: Devin によって作成されたプルリクエストは、 CODEOWNERS の設定に基づいて適切なチームに自動でレビューが割り当てられます。チームは、Devin が提案した修正内容を確認し、必要に応じて変更を加え、マージします。

この仕組みの全体像は以下のようになっています。

Flaky Test の発見から修正の「叩き台」作成までを自動化する

このフローにより、Flaky Test の発生から修正提案までが完全に自動化され、開発者は Devin が作成した Pull Request をレビューするだけでよくなりました。

もちろん Flaky Test の発見自体が誤検知である可能性もあるため、その場合はチームの判断でクローズします。

また、Devin が誤った原因仮説に基づいた望ましくない修正を提案する場合もあるため、必要なら追加の変更を行ったり、新しい Pull Request を作成したりして柔軟に対応します。

いずれにせよ Flaky な状態が疑われるテストが発見されると、ほとんど同じタイミングで担当するチームがそれを認識し、修正の「叩き台」がすでに作成されている状態を実現することができました。

Devin はどのようにテストを修正するのか?

「Devin が本当に Flaky Test を修正できるのか?」と疑問に思う方もいるかもしれません。

私たちも最初は半信半疑でしたが、Devin は当初の期待を上回るほど的確な修正を提案してくれています。

これは、Devin に渡しているプロンプトの一部です。再現手順の特定や、 Pull Request の体裁、修正の勘所などを細かく指示しています。

レビュアーやラベルなど、Pull Request 上で設定する静的な項目に関しては期待値を宣言的に定義し、CLIコマンドの返り値によってその検証を行うように指示しているのが重要なポイントです。

こうすることで、Devin 自身がセルフチェックを行いやすくして求める出力を安定的に得られるようにしています。

以下の Flaky Test の修正を行い、${レポジトリ名} に Pull Request を作成してください。

# テスト情報
## テストファイル
${Flaky Test が発生したテストファイル名}

## テストケース
${Flaky Test が発生したテストケース名}

## エラーメッセージ(先頭20行)
${テスト失敗時のエラーメッセージとスタックトレース}

## 実行コマンド
${テストの実行コマンド} (CI環境ではテストファイルを分割して並列実行しているため、どのテストファイルをどの順番、Seed値で実行しているかの情報が必要)

## 問題発生時のコミット
${直前の Commit の SHA}

# 作業内容
1. テストコードの修正
2. 修正後、当該テストをローカル環境で5回実行し、全て成功することを確認
  - seed値を変えながらテストを実行する時は、1つ以上の他のテストファイルも同時に実行
  - 複数のテストファイルに渡ってランダムな順番でテストが実行されるようにしたい
3. 以下の要件を満たす Pull Request をDraftで作成
  - タイトル: flaky: (原因と修正内容を簡潔に記載)
  - コードオーナーの設定に従ってレビューリクエストを送信。コードオーナー不在の場合は ${コードオーナー不在の場合に対応する GitHub Team 名} に送信
4. CIが成功したら、Pull Request を Open に変更

# Pull Request の description に含める情報
  - 発生していた問題の概要
  - 修正内容とそれによって Flaky が解消されると判断した理由
  - Datadog 上のテスト結果ページのURL

# 重要な注意事項
## 制約
  - 修正内容は最小限に抑え、テストの意図を変更しないようにしてください。
  - 他の spec ファイルで使用されている既存のパターンに合わせた実装を行ってください
  - 可能な限り修正前に失敗するまで条件を変えながらテストを実行し、失敗したときの実行コマンドなどの再現手順を明記してください。
  - テストの失敗を再現するときには並列実行環境下で自然に発生し得る条件を設定してください。特定のデータを手動で作成するなどすることは禁止します。
  - テストの修正によって Flaky が解消されると判断した理由を明記してください。

## 検証コマンド
以下のコマンドで各項目が正しく設定されていることを確認してください:

### PRステータスが Open であることの確認
\`\`\`bash
gh api repos/${レポジトリ名}/pulls/{pr_number} --jq '.state, .draft'
# 期待値: "open" と false が表示される
\`\`\`

### レビュアーが追加されていることの確認
\`\`\`bash
# 変更したファイルにコードオーナーが設定されている場合
gh api repos/${レポジトリ名}/pulls/{pr_number}/requested_reviewers
# 期待値: teams配列に コードオーナーのTeam名 が含まれる

# 変更したファイルにコードオーナーが設定されていない場合
gh api repos/${レポジトリ名}/pulls/{pr_number}/requested_reviewers
# 期待値: teams配列に "${コードオーナー不在の場合に対応する GitHub Team 名}" が含まれる
\`\`\`

### devin-fix-flaky-test ラベルが設定されていることの確認
\`\`\`bash
gh api repos/${レポジトリ名}/issues/{pr_number}/labels
# 期待値: name が "devin-fix-flaky-test" のオブジェクトが含まれる
\`\`\`

# 参考情報
- テストはファイルごとに一定のまとまりで分割されて並列実行されており、ランダムに設定されたSeed値を使って実行順序を制御しています
- それぞれの実行環境では独立したデータベースが用意されており、実行環境間でデータが共有されないようにしています

このプロンプトに基づき、Devin は様々なパターンの Flaky Test を修正してくれました。

ここでは、その中から代表的な2つの例をご紹介します。

修正例1: バリデーションエラーによる不安定性の解消

開始、終了時間を定義する2つの DateTime 型のカラムによって計算される値に依存するバリデーションのテストにおいて、処理時間とタイミングによっては失敗するケースがありました。

Before:

context '...' do
  before do
    create(:offering, :just_working)
  end
end

offering はいわゆる「求人」を表現するモデルです。

just_working trait では稼働時間がちょうど6時間になるように設定されています。

trait :just_working do
  start_at { Time.current.ago(5.hours) }
  end_at { Time.current.since(1.hour) }
end

労働基準法では6時間を超えると45分以上の休憩が義務付けられます。この場合はギリギリ6時間なので休憩時間が0分でも offering レコードの保存に成功します。

しかし、開始・終了時間(start_atend_at )はそれが設定されるときの現在時刻に依存しているため、 start_at が設定された後のタイミングでちょうど1秒をまたいでしまうと、 end_at = start_at + 6時間 + 1秒 となってしまうため、休憩時間が設定されていないとバリデーションエラーが発生してしまいます。

ActiveRecord::RecordInvalid: 労働時間が6時間を超えています。法定休憩時間を満たすように休憩時間を設定してください。

After (Devinによる修正):

context '...' do
  before do
    create(:offering, :just_working, :with_rests) # :with_rests trait を追加
  end
end

Devinは、 with_rests trait を追加することで、6時間超の労働時間に対して法定休憩時間(45分以上)を自動的に設定するように修正しました。

これにより、バリデーションエラーを回避し、テストが安定して成功するようになりました。

修正例2:FactoryBot のランダムデータ生成による不安定性

FactoryBot で生成されるランダムなデータが、テスト対象のメソッドの条件分岐に影響を与えて Flaky になっていたケースがありました。

Before:

describe '...' do
  let(:client) { create(:client) }
  
  context '...' do
    let(:serializer) { BrandUserSerializer.new(brand_user, client_id: client.id) }
    let(:brand_user) { build_stubbed(:brand_user) } # brand_user に client が紐づく

    it { expect(serializer.kind).to eq :other }
  end
end

このときBrandUserSerializer#kind メソッドは次のように実装されていました。

def kind
  if @brand_user.client_id == @client_id
    :manual 
  else
    :other
  end
end

BrandUserSerializer のインスタンス生成時の第一引数として渡された brand_user に紐づく cliend_id と、第二引数として直接渡された client_id の値を比較し、同一であれば :manual を、そうでなければ :other を返します。

このテストでは異なる値が設定される :other を期待値としていました。しかし、build_stubbed では ID がランダムに設定されるため、ごく僅かな確率で第二引数として渡された client.id の値と一致することがあります。

この場合 .kind:manual を返すためテストが失敗してしまいます。

After (Devinによる修正):

describe '...' do
  let(:client) { create(:client) }
  
  context '...' do
    let(:serializer) { BrandUserSerializer.new(brand_user, client_id: client.id) }
    
    # 明示的に別の client を作成して確実に異なる client_id が返るようにする
    let(:other_client) { create(:client) }
    let(:brand_user) { build_stubbed(:brand_user, client: other_client) }

    it { expect(serializer.kind).to eq :other }
  end
end

Devinは、明示的に other_client を作成し、 brand_user に設定することで、確実に異なる client_id を持つようにしました。

この変更により、serializerの条件分岐で正しく :other が返されるようになり、テストが安定して成功するようになりました。

導入後の成果と今後の展望

これを書いている時点で 3件 の Flaky Test を修正する Pull Request が Devin によって作成されましたが、そのうち 2件 は開発者によって変更が加えられることがなくマージされました。

結果として、CI の成功率は安定し、開発者は Flaky Test による手戻りや不要な調査から解放され、より価値のある機能開発に集中できるようになりました。

Devin が作成した Pull Request を「叩き台」として、チーム内で修正方針を議論したり、追加の変更を加えたりといった、より建設的な活動も生まれていることも重要なポイントです。

今後は、さらにプロンプトを洗練させ、より複雑な Flaky Test にも対応できるように改善を続けていく予定です。また、今回の成功を足がかりに、Flaky Test 修正以外の開発プロセスにも積極的に AI を活用していくことを検討しています。

おわりに

今回は、 Devin を活用して Flaky Test の修正を自動化し、CI の安定化と開発体験の向上を実現した事例をご紹介しました。AI を開発ワークフローに組み込むことで、これまで人間が時間をかけて対応していた退屈な作業をなくし、より創造的な仕事に集中できる環境を作ることができます。

この記事が、同じように Flaky Test に悩む開発者の皆さんにとって、少しでも参考になれば幸いです。

最後までお読みいただき、ありがとうございました!