クラウドMacでxccovによるiOSコードカバレッジ回帰ゲートを作る

クラウドMacでxccovによるiOSコードカバレッジ回帰ゲートを作る

単体テストの一部をクラウドMacへ移行したチームが陥りやすいのは、「テストが通ったかどうか」だけを確認してしまうことです。あるコミットで重要な分岐のアサーションが削除されても、テストはすべて成功したまま、カバレッジだけが気付かないうちに低下する可能性があります。より確実なのは、ビルドごとに独立した xcresult を生成し、Xcodeに付属する xccov でデータを抽出して、プロジェクト全体のカバレッジと今回変更したファイルの両方を検査する方法です。

カバレッジの入力条件を固定する

カバレッジを比較する意味があるのは、テストの実行条件が安定している場合だけです。まず、Xcodeのバージョン、scheme、テストプラン、シミュレータの機種、OSバージョンを固定します。実行環境ごとにdestinationを自動選択させてはいけません。テストコマンドではカバレッジを明示的に有効化し、実行のたびに新しい結果ディレクトリを作成します。

set -euo pipefail

RESULT_DIR="$PWD/Artifacts/Coverage"
RESULT_BUNDLE="$RESULT_DIR/TestResults.xcresult"

rm -rf "$RESULT_BUNDLE"
mkdir -p "$RESULT_DIR"

xcodebuild test \
  -workspace ExampleApp.xcworkspace \
  -scheme ExampleApp-CI \
  -testPlan UnitTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -enableCodeCoverage YES \
  -resultBundlePath "$RESULT_BUNDLE"

resultBundlePath が指すパスは、実行前に存在していてはいけません。以前の結果が残っていると、新しい結果を書き込めない場合があります。CIスクリプトでは xcodebuild -version、現在のコミット識別子、完全なdestinationも記録し、ツールチェーンの変更をコードの回帰と誤認しないようにします。

カバレッジゲートが評価するのは、同じテスト条件における変化です。異なるシミュレータやテストプラン同士を横並びで評価するためのものではありません。

xcresultから監査可能なデータを抽出する

テスト完了後は、ターミナルに表示される整形済みテキストを解析するのではなく、xccov でJSONをエクスポートします。JSONならファイルパス、ターゲット名、行カバレッジを保持しやすく、表示幅の変化によって列がずれることもありません。

xcrun xccov view \
  --report \
  --json \
  "$RESULT_BUNDLE" > "$RESULT_DIR/coverage.json"

test -s "$RESULT_DIR/coverage.json"

パーサーでは、最初に targets が存在することを確認してから、ターゲットごとにファイルを集計します。レポートが空の場合、カバレッジを0%として比較を続けるのではなく、収集失敗として直ちに扱うべきです。よくある原因には、schemeでテストが有効になっていない、ターゲットがカバレッジ集計の対象になっていない、レポートがディスクへ保存される前にテストプロセスが異常終了した、といったものがあります。

元の証拠を保存する

次の内容を同じジョブの成果物として保存することを推奨します。

成果物 用途
TestResults.xcresult テスト、ログ、カバレッジの生成元を検証する
coverage.json スクリプトで安定して解析する
coverage-summary.json しきい値、実測値、失敗したファイルを保存する
command.txt 実行パラメータとツールチェーンを再現する

パーセンテージのスクリーンショットだけでは、「実際にどのテストが実行されたのか」を確認できません。トラブルシューティングの根拠になるのは、元の結果バンドルです。

2段階のゲートを同時に設定する

プロジェクト全体のカバレッジは大幅な低下の検出には適していますが、大規模なコードベースに未テストのコードが数十行追加されても、全体値はコンマ数ポイントしか変化しないことがあります。そのため、次の2段階のルールを推奨します。

  1. プロジェクト全体のカバレッジが固定の下限を下回らないこと。
  2. 今回変更された実行可能なソースファイルが、より高いしきい値を下回らないこと。

たとえば、全体カバレッジの下限を72%、変更ファイルのしきい値を85%に設定します。しきい値はチームの現在のベースラインから決めるべきであり、最初から運用不可能な理想値を設定するべきではありません。次のPythonコードは、対象のカバレッジを読み取る例です。実際のプロジェクトでは、さらに files を展開してファイルごとに判定できます。

import json
import sys

with open("Artifacts/Coverage/coverage.json", encoding="utf-8") as f:
    report = json.load(f)

targets = report.get("targets", [])
if not targets:
    raise SystemExit("Coverage report has no targets")

tested = [t for t in targets if t.get("name") == "ExampleApp.app"]
if len(tested) != 1:
    raise SystemExit("Expected application target was not found")

coverage = float(tested[0]["lineCoverage"]) * 100
minimum = 72.0

print(f"application_line_coverage={coverage:.2f}")
sys.exit(0 if coverage >= minimum else 2)

スクリプトでは、「カバレッジが基準未満」と「レポートを解析できない」を異なる終了コードで区別します。前者ではテストの追加または変更理由の説明が必要です。後者では収集経路を修正する必要があり、同じ種類の失敗として扱ってはいけません。

本当にテストが必要なファイルだけを検査する

変更ファイルは git diff --name-only で取得し、xccov レポート内のパスとの積集合を取れます。すべての .swift ファイルをそのままゲートの対象にしてはいけません。通常、次のものは明示的に除外する必要があります。

除外ルールはリポジトリに格納し、コードレビューの対象にします。失敗してから一時的にワイルドカードを追加してはいけません。また、パスを比較する前にリポジトリのルートを統一し、シンボリックリンクを解決して、一時ビルドディレクトリのプレフィックスを取り除く必要があります。そうしないと、同じファイルでも絶対パスの違いによって一致しない可能性があります。

名前を変更したファイルについては、単純なファイル一覧よりも git diff --name-status -M の方が確実です。ファイルが古いパスから新しいパスへ移動した場合は、新しいパスでレポート項目を探します。「レポートに存在しない」と誤判定してはいけません。

並列テストと変動に対処する

カバレッジが時折変動する原因は、通常、xccov 自体のランダム性ではなく、テスト実行の不確定性です。非同期テストでは明確な状態になるまで待機し、固定秒数のスリープに依存してはいけません。共有データベース、シングルトン、一時ディレクトリは、各テストの前にリセットします。並列テストが同じリソースを競合して使用する場合は、まず関連するテストターゲットの並列実行を無効にし、具体的な共有箇所を特定します。

シミュレータのライフサイクルも追跡できるようにする必要があります。常駐型の実行環境ではジョブの前にアプリデータを消去できますが、毎回すべてのランタイムを削除する必要はありません。本当に比較すべきなのは、同じテストプランを同じツールチェーンで連続実行したときに、一貫した結果が得られるかどうかです。まず同じコミットを3回繰り返し実行し、ファイルごとの差分を記録します。継続的に変動するファイルは、先にテストの分離を修正してから、厳格なゲートの対象にします。

最後に、失敗サマリーには、そのまま対応へ移れる情報を記載します。具体的には、実際の全体カバレッジ、要求値、しきい値を下回った変更ファイル、そのファイルのカバー済み行数と実行可能行数、元の xcresult のアーカイブ場所です。これにより、開発者はジョブ全体を再実行しなくても、テストを追加すべきか、スクリプトを修正すべきか、テスト環境の不確定性に対処すべきかを判断できます。

よくある質問

プロジェクト全体のカバレッジだけをゲートにすれば十分ですか?

十分ではありません。全体値は大きな後退の検知には向きますが、小さな変更への感度が低いためです。変更ファイルの基準も併用し、生成コードやテスト補助コードは明示的に除外します。

同じコミットでもカバレッジが変動するのはなぜですか?

テスト対象の差、未完了の非同期処理、並列テストの共有状態、シミュレータの残存データ、Xcodeの差が主な原因です。scheme、destination、テストプラン、ツールチェーンを先に固定します。

ゲート失敗時に保存すべき成果物は何ですか?

元のxcresult、解析後のcoverage.json、判定サマリー、実行コマンド、コミット識別子を保存します。これにより未実行、解析エラー、実際のカバレッジ低下を切り分けられます。

専用物理ノード

プロジェクト単位で起動・停止できるクラウドMacでワークフローを再現

3種類のApple Silicon構成から必要なリソースを選び、日単位、週単位、月単位、または四半期単位でレンタルできます。専用物理マシンを使用しており、仮想マシンではありません。実際の利用可能状況は、コンソールからリアルタイムで確認できます。

構成を選んで申し込む