iOS アプリでレポートの「書き出し」をタップすると、共有シートにはファイルが表示された。しかし、受け取った側で開くと 1 ページ足りない。この種の不具合では、書き出し関数が成功を返しただけでは検証になりません。テストジョブで最終的な PDF ファイルを取得し、機械的に判定できる内容を確認したうえで、残りを目視で確認する必要があります。以下の手順はクラウド Mac 上でのシミュレータテストを想定していますが、ローカルでも同様に実行できます。
検証対象となるファイルを先に定義する
明細 2 件と明確なレポートタイトルなど、固定の入力データを用意します。テストケースには、期待するファイル名、最低ページ数、本文中の変わらない短い文字列を定めておきます。生成日時、ランダムな識別子、金額の合計をそのまま文字列でアサートするのは避けてください。これらはテストデータによって変わる可能性があります。入力データを固定し、動的な項目には別途、明確な書式ルールを設けるほうが確実です。
テスト用ビルドでは、アプリが PDF を自身の Documents/export.pdf に書き出すようにします。これは検証を容易にするためであり、製品版に同じ保存先を求めるものではありません。実行前に古いファイルを削除し、書き出し完了後にファイルの存在とサイズが 0 でないことを確認します。そうしないと、前回の生成物によって今回のテストが誤って成功するおそれがあります。
ファイルが存在すること、解析できること、内容が正しいことは、それぞれ別のチェックです。前のチェックに通っても、次のチェックに通ったことにはなりません。
シミュレータから今回の生成物を取得する
まず、テストジョブで指定したシミュレータ識別子を使い、アプリの実行と書き出し操作を行います。ジョブの終了後、同じ識別子のデバイスでアプリのコンテナを照会します。対象を booted で推測しないでください。複数のシミュレータが同時に起動している場合は、特に取り違えやすくなります。次の例で使う識別子とアプリのバンドル ID は、テスト設定から渡します。
UDID="替换为测试使用的模拟器标识"
BUNDLE_ID="替换为测试应用的包名"
CONTAINER="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)" || exit 1
mkdir -p artifacts
test -s "$CONTAINER/Documents/export.pdf" || exit 1
cp "$CONTAINER/Documents/export.pdf" artifacts/export.pdf
file artifacts/export.pdf
get_app_container を使うには、指定したシミュレータにアプリがインストールされている必要があります。照会に失敗した場合は、ディスク全体を検索するのではなく、テストで実際に使ったデバイス識別子、バンドル ID、インストール結果を先に確認します。コピー後はジョブのディレクトリ内にあるファイルを検査し、以降の解析とアーカイブにもそのコピーを使います。これにより、アプリが再び書き込んでも検査対象が変わりません。file の出力は診断の手がかりになりますが、PDF の解析に代わるものではありません。
PDFKit で構造と文字を確認する
次の Swift コードを scripts/check-pdf.swift として保存します。ファイルパスと期待する語句を受け取り、各ページの境界を検査したうえで、その語句を含むページが少なくとも 1 ページあることを確認します。終了コードが 0 以外なら、ビルドジョブは今回の自動チェックを不合格と判定できます。
import Foundation
import PDFKit
guard CommandLine.arguments.count == 3,
let pdf = PDFDocument(url: URL(fileURLWithPath: CommandLine.arguments[1])),
pdf.pageCount > 0 else {
fputs("PDF is missing or cannot be parsed\n", stderr)
exit(2)
}
let expected = CommandLine.arguments[2]
var extracted = ""
for index in 0..<pdf.pageCount {
guard let page = pdf.page(at: index) else {
fputs("PDF page is missing\n", stderr)
exit(2)
}
let media = page.bounds(for: .mediaBox)
let crop = page.bounds(for: .cropBox)
guard media.width > 0, media.height > 0,
crop.width > 0, crop.height > 0,
media.contains(crop) else {
fputs("PDF page bounds are invalid\n", stderr)
exit(2)
}
extracted += page.string ?? ""
}
guard extracted.contains(expected) else {
fputs("Expected text was not extracted\n", stderr)
exit(2)
}
print("Checked \(pdf.pageCount) PDF page(s)")
swift scripts/check-pdf.swift artifacts/export.pdf "Quarterly Report" を実行します。例の語句は、固定したテストデータに実際に含まれる文字列に置き換えてください。要件がちょうど 2 ページなら、pdf.pageCount == 2 もアサーションに追加します。0 より大きいことだけを確認しても、ページ分割の変化は検出できません。ページサイズも製品要件に応じて個別に確認できますが、取り決めがないまま、正当な用紙サイズまでエラーにしないでください。
文字抽出は見た目の検証ではない
page.string が確認するのは抽出可能な文字であり、文字が裁ち落とされていないことや、グラフ・画像・色が正しいことまでは保証しません。逆に、スキャン画像や画像として描画された文字は、見た目に問題がなくても抽出できない場合があります。文字列のアサーションが失敗したら、まず元の PDF を保存し、内容が書き込まれていないのか、そのページに文字抽出が適さないのかを見極めます。ジョブを通すためだけにアサーションを削除してはいけません。
失敗した工程を特定する
実行ごとに、書き出し操作の結果、ファイルのコピー結果、PDFKit の終了コードを分けて記録することを勧めます。失敗時には、アプリが書き出しを実行したか、コンテナに今回のファイルが生成されたか、コピーを解析できるか、ページ数・境界・文字が期待どおりかを順番に判断できます。「PDF テスト失敗」とだけ出力すると、原因を調べるために全工程を再実行することになります。
| 症状 | 最初に確認すること |
|---|---|
| コンテナが見つからない | シミュレータ識別子、バンドル ID、インストール結果 |
| ファイルが空 | 古いファイルを削除したか、書き込みが完了したか |
| 解析できない | コピー処理、最終ファイルの形式 |
| 文字が一致しない | 固定入力、ページの内容、文字抽出の限界 |
失敗時には artifacts/export.pdf とテストログを保存しますが、先にレポートに実際のユーザーデータが含まれていないことを確認してください。機密情報が含まれる可能性がある場合は、合成サンプルを使い、生成物へのアクセスを制限し、チームの取り決めに従って削除します。本番のレポートを公開 CI の成果物にアップロードしてはいけません。
再現・検証できる実行で締めくくる
検証手順を提出する前に、同じ固定入力で 2 回続けて実行します。どちらも改めて書き出して対象ファイルを上書きし、ページ数と文字のチェックで一致する結果が得られる必要があります。次に、期待する語句を意図的に削除してスクリプトが失敗することを確認します。さらに、テスト用アプリがファイルを生成しないようにして、コピー処理が失敗することを確認します。この 2 つの逆方向のチェックで、「アサーションがそもそも実行されていない」可能性を排除できます。
自動チェックに通った後も、代表的なサンプルの PDF を開き、改ページ位置、長い文字列の折り返し、グラフの端を確認してください。安定して判定できる構造とテキストの要件は機械が、実際の見た目は人が確認します。それぞれの結果を分けて記録しておけば、次に書き出しの異常が起きたとき、比較の基準が明確になります。
よくある質問
PDFKit で文字を抽出できなければ書き出し失敗ですか?
必ずしも失敗ではありません。画像化された文字や特殊なフォントでは抽出できない場合があるため、ファイルを保存して表示結果も確認します。
シミュレータ内の PDF を検証ジョブに渡すには?
アプリの Documents に書き出し、simctl get_app_container でデータ領域を特定してから、ジョブの成果物ディレクトリへコピーします。
次のビルドは専有クラウドMacにお任せください。
ViewMiniの機種、リージョン、利用期間を確認し、ワークフローに合った専有物理ノードを設定しましょう。