MetricKit 診断データを固定サンプルで回帰テストする

MetricKit 診断データを固定サンプルで回帰テストする

MetricKit の診断ペイロードは、コードの変更に合わせて届くわけではありません。今日パーサーを 1 行変更しても、フィールドの欠落が発覚するのは、数日後に実際のクラッシュレポートやハングレポートが届いたときかもしれません。より確実なのは、取得済みのペイロードから機密情報を除去して正規化し、固定サンプルとして保存する方法です。クラウド Mac 上でコミットのたびに同じ入力を再生すれば、次の偶発的なイベントを待つのではなく、診断パイプラインそのものを検証できます。

まずテスト範囲を明確にする

固定サンプルは、原始 JSON を読み取れるか、システムフィールドを内部モデルへマッピングできるか、機密情報が除去されているか、不正な入力に対して適切にフォールバックできるか、という 4 層のロジックを検証するのに適しています。一方で、システムが必ずペイロードを生成することは証明できず、デバイス側のコールバック検証を置き換えることもできません。

受信処理とアプリケーション側の処理は分離することを推奨します。受信層は jsonRepresentation() の取得、保護されたディレクトリへの書き込み、アップロードキューへの追加だけを担当します。解析層は Data を受け取り、MetricKit の型に依存しない内部構造を出力します。単体テストでは後者だけを呼び出し、テストターゲットでシステムオブジェクトを偽装する必要がない設計にします。

サンプルの目的は、解析に一度成功した状況を再現することではありません。入力契約を固定し、パーサーを変更するたびに「どのフィールドが変わり、どの情報が失われたのか」を確認できるようにすることです。

リポジトリに保存できる正規化サンプルを作成する

生のペイロードには、バンドル識別子、デバイス情報、時刻、コールスタックのシンボル、ローカルパスが含まれる可能性があります。そのままコミットしてはいけません。原本はアクセス制限された環境にのみ保存し、リポジトリに追加できる正規化済みのコピーを生成します。時刻は固定値に、識別子はテスト用の値に置き換え、パスは $APP$HOME に置換します。コールスタックのアドレスは形式だけを維持し、実際のアドレスは残しません。

内部形式に schema を追加することはできますが、MetricKit が持つ元のバージョンを書き換えてはいけません。ディレクトリは診断タイプ別に整理します。

Tests/Fixtures/MetricKit/
├── crash/basic.json
├── crash/missing-stack.json
├── hang/main-thread.json
├── disk-write/threshold.json
└── malformed/truncated.json

各サンプルが表す条件は 1 つだけにします。1 つのファイルにクラッシュ、ハング、ディスク異常がすべて含まれていると、失敗時の原因特定が困難になります。サンプル名には期待結果ではなく入力内容を記述します。期待値はテストコードに置くことで、出力に合わせてアサーションまで安易に変更されていないか、レビュー時に発見しやすくなります。

テスト実行前にスキーマゲートを設ける

テストをコンパイルする前に jq で低コストのチェックを行えば、無効な JSON、バージョンの欠落、匿名化されていないパスをすばやく検出できます。以下の diagnostics はチーム独自の正規化済み配列であり、システムの生ペイロードも同じ構造であると仮定するものではありません。

set -euo pipefail

root="Tests/Fixtures/MetricKit"

find "$root" -name '*.json' -print0 |
while IFS= read -r -d '' file; do
  jq -e '
    type == "object" and
    .schema == 1 and
    (.diagnostics | type == "array") and
    all(.diagnostics[];
      (.kind | type == "string") and
      (.timestamp | type == "string") and
      (.stackID | type == "string")
    )
  ' "$file" >/dev/null

  if grep -E '/Users/|/private/var/|[A-F0-9]{16,}' "$file"; then
    echo "fixture contains unnormalized data: $file" >&2
    exit 1
  fi
done

スキーマゲートですべてのシステムフィールドを必須にしてはいけません。新しいオプションフィールドが追加されるたびに、意味のない失敗が発生するためです。内部処理が実際に依存するキーだけを確認し、未知のフィールドはデコーダーで無視するようにします。

正常系と異常系のサンプルで解析契約を検証する

少なくとも正常入力を 1 組、失敗入力を 3 組用意します。重要なのは「エラーが発生しなかった」ことではなく、出力を集計、アラート、原因調査に引き続き利用できるかどうかです。

サンプル 期待する動作 発生してはならないこと
完全なクラッシュ タイプ、時刻、スタック識別子を出力する 元のローカルパスを保存する
空の診断配列 空の結果を返す デコード失敗として扱う
コールスタックの欠落 不完全としてマークする 空のスタックを生成して正常データとして扱う
途中で切れた JSON 分類可能なエラーを返す プロセスを直接終了する
未知のタイプ 未知の列挙値として記録する ペイロード全体を破棄する

JSON 全体ではなく内部モデルをアサートする

JSON 全体のスナップショットは、フィールドの順序や無関係なメタデータの影響を受けやすくなります。診断件数、タイプ、安定した識別子、機密情報の除去結果を優先してアサートします。整形済み JSON のスナップショットを追加するのは、正規化した出力をシステム間で交換する場合に限ります。エラーも invalidJSONmissingRequiredFieldunsupportedDiagnostic のような比較可能な列挙値にし、変化しやすい自然言語のメッセージだけを比較してはいけません。

クラウド Mac の CI に組み込む

VMRunner のクラウド Mac では、単体テストの前に fixture チェックを実行し、非対話型ジョブで固定の作業ディレクトリを使用します。実行順序の一例は、コードのチェックアウト、スキーマゲートの実行、パーサーの単体テスト、テスト結果の生成、ワークスペースに未コミットのサンプル変更がないかの確認です。

サンプルの変更は個別にレビューする必要があります。システムフィールドが追加された場合は、まずパーサーでそのフィールドを利用する必要があるか確認します。必要であれば内部の schema を更新し、移行テストも同時にコミットします。不要であれば、デコード処理は未知のフィールドを許容するままにします。CI のスクリプトで基準ファイルを自動的に上書きしてはいけません。実際にフィールドが欠落していても、新しく生成された誤った結果によって「承認」されてしまうためです。

マージ前のチェック項目

これらの制約を整備すれば、MetricKit の診断処理は「データが届いてから試す」ものではなく、通常の再現可能なエンジニアリングテストになります。システムペイロードについては引き続きデバイス側での検証が必要ですが、解析、機密情報の除去、互換性の確認が、偶然届くレポートに依存することはなくなります。

よくある質問

MetricKit の固定サンプルで実機テストを置き換えられますか?

置き換えられません。固定サンプルはデコード、正規化、機密情報除去を検証しますが、診断ペイロードが実際に生成、配信されることは実機テストと運用観測で確認する必要があります。

元の MetricKit JSON をそのままリポジトリへ保存してよいですか?

通常は避けます。匿名化した元データはアクセス制限された場所に保管し、利用者識別子、ローカルパス、機密情報を除いた正規化済みサンプルだけを管理対象にします。

未知のフィールドが追加されたら CI を失敗させるべきですか?

追加の任意フィールドだけなら許容します。ただし内部契約で必須とした診断種別、時刻、コールスタック識別子が欠落した場合はテストを失敗させます。

専用物理ノード

次回のビルドを専用クラウドMacで実行

モデル、ノード、課金期間を選択できます。構成と米ドル価格は注文前にすべて表示され、利用可能状況はコンソールからリアルタイムで確認できます。

プランを選んで注文