ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

CircleCIを利用した不安定なテスト(Flaky Test)の検疫

技術部開発基盤グループの根岸(@negipo)です。ポプマスがサ終してしまいましたね……。

さて、ロコガイドにはさいきん開発基盤グループができました(わいわい)。開発者生産性を向上することで、ロコガイドという組織のもつパフォーマンスを技術的に高めてゆくのがミッションです。直したいところがいっぱいでやる気もいっぱいです。やっていきます!

本稿では最近実行された様々な改善のうち、不安定なテスト(Flaky Test)に対する対応について記述しています。

不安定なテストに対する対応

不安定なテストは名前の通りその結果が非決定論的なテストです。CI/CDパイプライン全体の安定性を損なう不安定なテストに対応していくことで、デプロイ回数が増加し、開発者生産性が向上します。LeanとDevOpsの科学などを参照するまでもなく、CI/CDパイプラインのパフォーマンスを改善することが組織の生産性に直結することは事実と言っていいでしょう。

不安定なテストはどんなシステムにも入り込みます。

このように不安定なテストの対応に向けて様々な努力がなされる中、CI/CDパイプライン安定性のため、不安定なテスト改善に向けた機構的対応にロコガイドも手をつけはじめました。

今回、CI/CDパイプラインの安定性の向上を目的として、ロコガイドのRuby on Railsアプリケーション向けに下記のシステムを構築しました。

  • CircleCIが2021年に追加したテストインサイトを有効化
  • テストインサイトの機能を基盤として、不安定なテストを自動的に分類
  • 一般に『検疫(クアランタイン/Quarantine)』と呼ばれるシステムを構築し、分離された不安定なテストにパイプライン全体が依存しないようワークフローを調整

詳細を解説していきます。

検疫とは

検疫とは、不安定なテストのCI上のクリティカルパスからの除外を指す言葉です。典型的にはGoogleが語彙として導入しているのを見つけることができます。今回は採用しませんでしたが、検疫の実行を目的としたgemなども公開されているようです。

ロコガイドのテスト

ロコガイドにおけるテストはやや複雑で、CircleCIとCodeBuildで実装されています。概説すると以下の通りとなります。

  • パイプラインの管理にCircleCIを利用しているが、コスト最適化のためにRSpecの実行ジョブそのものはCodeBuildに委譲している
  • RSpecを動作させるときには4つのCodeBuildジョブを立ち上げてS3でソースコードを転送し、CodeBuild内でそれぞれ4つ、計16個のtest-queueワーカを立ち上げている
  • CodeBuildはテスト実行結果をアーティファクトとしてS3に出力し、CircleCIはその内容を取得・マージして表示する

ロコガイドのテスト概略

検疫システム導入前の状況

ロコガイドのバックエンドを構成しているRailsのテストは逆テストピラミッドを構成しており、システムテストが比率として多いです。そのためもあり不安定なテストが非常に多く、今年の5月ごろにはCI/CD全体の成功率が週間で20%を切るような状況がありました。その上当時はテスト実行1回につき30分かかるという状況もあったため、開発体験としてはかなり厳しい状態でした。

一方、開発基盤チームの発足後すぐ、チーム内外でタイムアウトの調整やデータベースクリーンアップのタイミング最適化などで成功率50%を超えるような状況まで改善を達成していました。また同時期にキャッシュの導入などでテスト実行時間を15分まで短縮したことで、不安定なテストの検疫システムのような若干複雑な対応を構築するための最低限の素地が揃っていたと言えます。

CircleCIテストインサイトを有効化

CircleCIテストインサイトはCircleCIが2021年に公開したテストダッシュボードです。下記のような多岐に渡る機能があり、APIで一定の情報が取得可能です。

  • 1回の実行における平均テスト回数
  • 検出された不安定なテスト数
  • 失敗した数
  • テストスイートにおける実行数
  • 直近の実行
    • テストの回数
    • スキップされたテスト数
    • テストの成功率
  • 問題のあるテストの自動検出
    • 不安定なテスト
    • 失敗の多いテスト
    • 実行速度の遅いテスト

開発基盤チームでは発足後すぐCI/CDの状況をモニタリングするダッシュボードを構築していましたが、不安定なテストの自動検出のためにCircleCIテストインサイトを有効化し、利用することにしました。

ドキュメントにあるように、下記のようにJUnit形式の結果ファイルを出力し、有効化することができました。

  • CodeBuildで実行されたRSpecのJUnit形式の結果ファイルをCodeBuildのアーティファクトとしてS3に配置する
  • CircleCI側でCodeBuildのアーティファクトを読み出し、さらにCircleCIのアーティファクトとして書き出して配置する
    • 結果として計16個のJUnit形式結果ファイルが存在することになるのですが、複数ファイルに関する記載はドキュメントにはないものの、CircleCIの特定ディレクトリに全ファイルを配置してstore_test_resultsでディレクトリ指定することで問題なくテストインサイトを動作させることができました

以上で不安定なテストの自動検出を行うリソースが整いました。

手動検疫システムの構築

CodeBuildへのソースコード配布時、CircleCI側のプロセスからは各CodeBuildジョブにそれぞれどのテストを実行すべきかを入力しています。

そこで、特定のYAMLファイルに不安定なテストを記録し、CircleCIのジョブパラメータで「すべてのテスト」「不安定なテストのみ」「不安定なテスト以外」をそれぞれ実行対象として選択可能にしました。feature branchなどでのテスト実行では「すべてのテスト」を、デプロイパイプラインでは「不安定なテストのみ」と「不安定なテスト以外」をそれぞれ個別に実行し、パイプライン全体は「不安定なテスト以外」のみに依存するよう構成することで、全体としてのCI/CDの成功率を向上させることを図っています。

検疫システムのワークフロー

テストインサイトの機能で自動的に分類された不安定なテストを検疫システムに組み込む

前述の通りテストインサイトではAPI経由で不安定なテストを取得可能なので、簡単なシェルスクリプトで先のYAMLファイルの手動管理を代替できます。

#!/bin/bash

set -eux

curl -H "Circle-Token: $CIRCLE_TOKEN" https://circleci.com/api/v2/insights/gh/{user}/{repos}/flaky-tests | \
  jq -r '.flaky_tests[] | "- " + .file' \
  sort -u > tmp/flaky_test_paths.yml

cat tmp/flaky_test_paths.yml

このスクリプトの実行をCircleCIのステップとして登録し、手動による不安定なテストの分類・配置を代替することができました。尚、些細なことですが、この際プロジェクトAPIトークンはCircle CI API v2を利用できないため、ワークフローの環境変数としてはパーソナルAPIトークンを取得して利用する必要があります。

ハイブリッドな検疫システム

さて、この運用を開始したところ、CircleCIのテストインサイトでは直近100件のテスト結果から不安定なテストを動的に生成しているためか、実際には不安定なテストが安定なものとみなされてしまうケースが散見されました。そのため、一定の不安定なテストについてはgit管理を行い、CI実行時にあらたに発見された不安定なテストについては実行時に追加するというハイブリッドな運用で進めることにしました。

#!/usr/bin/env ruby

require 'open-uri'
require 'json'
require 'yaml'

FILE_PATH = "tmp/flaky_test_paths.yml"

def main
  open("https://circleci.com/api/v2/insights/gh/tokubai/bargain/flaky-tests",
    "Circle-Token" => ENV.fetch("CIRCLE_TOKEN")) do |f|
    body = f.read
    dynamic_flaky_tests = JSON.parse(body)["flaky_tests"].map{|test| "./#{test["file"]}"}.uniq
    static_flaky_tests = YAML.load_file(FILE_PATH)
    puts [
      "Adding followings into #{FILE_PATH}: ",
      dynamic_flaky_tests - static_flaky_tests
    ]
    YAML.dump(dynamic_flaky_tests | static_flaky_tests, File.open(FILE_PATH, 'w'))
  end
end

main

こうした工夫で安定的に検疫システムを稼働させることができるようになりました。

まとめ

不安定なテストの分類をCircleCIテストインサイトに任せることで、不安定なテストの検疫をおこなうシステムを構築できました。検出済みの不安定なテストのパイプラインに対する影響がゼロになったことで、以降成功率は90%を超える状態で推移するなど、CI/CDの成功率の著しい向上を達成できました。

CI/CDの日次成功率

また、副次的に不安定なテストの存在を可視化できたことで、不安定なテストの対応を比較的少ない社内調整で進めていくことが可能な健全化タスクとして開発者に認識してもらうことができました。不安定なテストに気づくべきタイミング、修正の方法などについてのアナウンスをおこない、開発基盤グループと各チームの開発者が協働して少しずつ不安定なテストの対応を進めています。

開発基盤グループでは今後もこのように開発者生産性の向上を図っていきます。ロコガイドの技術発信を引き続きよろしくお願いします。