ボタン名の変更、アイコンの差し替え、制約の調整といった一般的な修正でも、iOS画面から読み上げ可能なラベルが失われたり、タップ領域が狭くなったり、大きな文字サイズでテキストが切れたりすることがあります。すべてのコミットを人手で確認するのは現実的ではありません。より確実なのは、クラウドMac上でシミュレータとアプリの状態を固定し、XCTestでアクセシビリティ監査を実行したうえで、失敗を再現可能なマージゲートとして運用する方法です。
まずゲートの対象範囲を定義する
最初からプロダクト全体を走査する必要はありません。ログイン後のホーム画面、主要な編集画面、送信確認画面など、利用頻度の高いフローを選び、各テストケースでは安定した状態を1つだけ検証します。アニメーション、ランダムなレコメンド、現在時刻、ネットワークのレスポンスは要素ツリーを変化させるため、テストモードでは無効化するか、固定データを注入します。
最初の段階では、次の4種類の問題を確認することを推奨します。
| チェック項目 | よくある不具合 | ゲートでの扱い |
|---|---|---|
| 要素の説明 | アイコンボタンに読み上げ可能な名前がない | 即時失敗 |
| タップ領域 | コントロールは見えるがタップしにくい | 即時失敗 |
| コントラスト | 前景と背景を区別しにくい | デザイン確認後に修正 |
| テキストレイアウト | 大きな文字サイズで切れる、または重なる | 即時失敗 |
自動監査が担うのは、機械的に安定して判定できる問題の検出です。操作全体が使いやすいことを証明するものではありません。読み上げ順序、案内の分かりやすさ、複雑なジェスチャーについては、引き続き人手で確認する必要があります。
テスト画面を確定的な状態にする
UIテストで最も避けたいのは、「同じ入口なのに異なる画面が表示される」状態です。アプリ側でテスト専用の起動引数を認識し、起動時に一時状態を消去して固定データを読み込み、不要なアニメーションを無効化します。これらの引数はテスト環境だけを変更するものとし、本番のビジネスロジックに入り込ませてはいけません。
let app = XCUIApplication()
app.launchArguments = [
"-ui-testing",
"-reset-demo-state",
"-disable-animations"
]
app.launchEnvironment["TEST_LOCALE"] = "zh-Hans"
app.launch()
テストデータはアプリのプロセス内で用意し、UIテストがリアルタイムAPIに依存しないようにします。読み込み中、空データ、エラー、正常結果を網羅する場合は、状態ごとに独立した引数を用意します。そうすれば、失敗後はコマンドをコピーするだけで再現でき、特定のリモート条件が再び発生するのを待つ必要がありません。
要素の特定方法を固定する
重要なコントロールをボタンのタイトルや座標で特定してはいけません。タイトルはローカライズによって変わり、座標はウィンドウサイズや文字サイズによって変化します。操作可能な要素には、意味的に安定した accessibilityIdentifier を設定します。
checkoutButton.accessibilityIdentifier = "checkout.submit"
cartSummary.accessibilityIdentifier = "checkout.summary"
識別子には見た目上の位置ではなく、要素の役割を記述します。footer.orangeButton のような名前はデザイン変更後に意味を失いますが、checkout.submit ならレイアウトが変わっても継続して使用できます。
対象を絞ってXCTest監査を実行する
使用中のシステムバージョンが該当APIをサポートしている場合は、安定した状態まで遷移した画面に対して監査を実行できます。まず主要な要素が表示されるまで待ち、その後でチェックを開始します。これにより、読み込み中の一時的なレイアウトを不具合として扱うことを防げます。
func testCheckoutAccessibility() throws {
let app = XCUIApplication()
app.launchArguments = ["-ui-testing", "-reset-demo-state"]
app.launch()
let submit = app.buttons["checkout.submit"]
XCTAssertTrue(submit.waitForExistence(timeout: 10))
if #available(iOS 17.0, *) {
try app.performAccessibilityAudit(for: [
.sufficientElementDescription,
.hitRegion,
.contrast,
.textClipped
])
}
}
安易にグローバルな除外リストを作成してはいけません。一時的な除外が必要な場合は、対象の画面、要素識別子、問題の種類を明確に限定し、コードレビューで除外を削除する条件を明記します。そうしなければ、除外項目が徐々に恒久的な死角になってしまいます。
クラウドMacの実行条件を固定する
まず、現在利用できるシミュレータデバイスとランタイムを確認し、パイプラインから確定したdestinationを渡します。すべてのノードに同名のデバイスが存在するとは限りません。
xcrun simctl list devices available
xcrun simctl list runtimes
export SIM_DESTINATION='platform=iOS Simulator,name=iPhone 15,OS=17.5'
set -o pipefail
xcodebuild test \
-project ExampleApp.xcodeproj \
-scheme ExampleAppUITests \
-destination "$SIM_DESTINATION" \
-only-testing:ExampleAppUITests/AccessibilityTests \
-resultBundlePath "$PWD/TestResults/Accessibility.xcresult"
実行時にはデバイス名とシステムバージョンをパイプライン変数として扱い、ノードにインストール済みのランタイムと一致させます。実行前に新しいシミュレータを作成するか、デバイスを再利用する場合は、先にアプリを終了してテスト状態を消去します。並列ジョブで同じシミュレータを共有してはいけません。言語設定、権限ダイアログ、前のテストケースの残存状態が相互に影響するためです。
クラウドMacのモデルやノードの選択によって、テスト設計の原則が変わることはありません。実行環境を追加する場合は、コンソールで現在選択可能な構成を確認し、Xcodeのバージョン、ランタイムのバージョン、言語、destinationを記録します。これにより、失敗時に同じ条件で再実行できます。
実行を階層化し、よくある誤検知に対処する
各マージリクエストでは、主要画面、デフォルト言語、標準の文字サイズだけを対象にし、フィードバック時間をチームが許容できる範囲に抑えます。メインブランチでは、多言語、縦横両方向、ダークモード、Dynamic Typeへ対象を広げます。すべての組み合わせを1つのテストメソッドに詰め込んではいけません。画面と状態ごとに分割すれば、失敗したテスト名だけで対象範囲を把握できます。
不安定な失敗が発生した場合は、次の順序で確認します。
- 主要な要素が要素ツリーに存在するだけでなく、読み込みを完了しているか確認します。
- 言語、地域、文字サイズ、画面方向、外観の設定を比較します。
- テストデータが前回の実行によって変更されていないか確認します。
- 失敗したメソッドだけを再実行し、問題が安定して再現するか確認します。
- テスト結果バンドル、実行コマンド、環境情報を保存したうえで、プロダクトの不具合か、テストの分離不足かを判断します。
ゲートのルールにも段階を設けます。説明の欠落、タップ不能、テキストの重なりはマージをブロックするのに適しています。デザイン確認が完了していないコントラストの問題は、まず記録にとどめることもできますが、担当者と対応期限を必ず設定します。最終目標は、常にすべてがグリーンになるレポートを得ることではありません。失敗するたびに、画面、状態、要素、環境条件まで特定できるようにすることです。
よくある質問
自動監査だけでアクセシビリティ品質を保証できますか?
できません。ラベル不足や小さなタップ領域などは検出できますが、読み上げ順序や操作の分かりやすさは実機を使った手動確認が必要です。
ローカルでは成功するのにクラウドMacで失敗する原因は何ですか?
シミュレータのランタイム、言語、地域、文字サイズ、画面方向、投入データを比較してください。これらを起動条件として固定すると再現性が上がります。
完全な監査はどのタイミングで実行すべきですか?
主要画面の小さな監査は変更ごとに実行し、多言語や文字サイズを含む完全な組み合わせはメインブランチや定期ジョブで実行するのが現実的です。
次回のビルドをクラウドMacで実行しましょう。
3種類のApple Silicon構成を比較し、現在のワークフローに合うリージョンを5つの海外ノードから選べます。