Web/admin/iOSにE2Eテストを整備した話 ― 認証を迂回したら、次はテストコード自身の罠が待っていた
個人開発しているMinecraftサーバー監視アプリ「MineWatch」(公式サイト、アプリの全体像はこちら)は、Web・admin・iOSという3つの異なる技術スタックを持っています。前回の記事ではJenkinsのPRレビューでテスト・スキャンを回す仕組みを紹介しましたが、そこではE2Eは検証項目の1つとして名前だけ触れていました。この記事は、それぞれにE2Eテスト(Web/adminはPlaywright、iOSはXCUITest)を整備した話です。認証をどう迂回するかという最初の壁を越えたら、次はテストコード自身の書き方の罠が、そしてさらにJenkinsのまっさらな実行環境特有の罠が待っていました。
TL;DR
- APIはPostman/NewmanでエンドポイントのE2Eが既にできていたので、同じ考え方をWeb/admin/iOSのUIにも広げた。
- Web/adminはPlaywright、iOSはXCUITestで、まず「Firebase実ログインを経由せずに認証済み状態を作る」仕組みを整えた。Web/adminはカスタムトークン注入とApp Checkのdebug token、iOSは起動引数によるDevBypass。
- iOS側は、テスト作成中に実際のプロダクトバグ(編集後に一覧の表示名が更新されない)を発見・修正できた。テストを書く過程そのものが不具合を見つけるのに役立った例。
- XCUITest特有の罠が2つあった。テストターゲットから
String(localized:)を呼ぶと、アプリ本体ではなくテストバンドル自身の文字列を探しに行く。モーダルとその背後の一覧、両方のnavigation barがアクセシビリティツリーに残るため、無指定のクエリだとどちらのボタンか曖昧になる。 - JenkinsのまっさらなworkspaceでCIに統合したところ、開発機では発生しない実行環境特有のバグを2件発見・修正した。
1. 背景: APIは既にE2Eがあった、UIは手つかずだった
MineWatchのAPIは、Postman/NewmanでエンドポイントのE2Eテストが既にできていました。この考え方をWeb・admin・iOSのUIにも広げ、プッシュ前・PR前にローカルで一通り自動確認できる状態にしたい、というのが出発点でした。
最初に決めたのは、実際のFirebase認証・実際のApp Checkそのものはテストのスコープからあえて外すことでした。これはPostman/Newmanと同じ割り切りです。認証周りを迂回できないと、UIの主要フロー(登録・編集・削除・設定変更)を自動で検証すること自体に進めません。
2. Web/admin: Playwrightで認証をどう迂回するか
Web/adminは、Firebase Authのカスタムトークンで認証済み状態を作ります。
/**
* E2Eテスト専用の固定Firebaseユーザー。実在のユーザーと衝突しないよう
* 明確にテスト用と分かるUIDにしている。
*/
export const E2E_TEST_UID = "e2e-playwright-test-user";
/**
* E2Eテスト用ユーザーのFirebaseカスタムトークンを発行する。
*/
export async function issueE2eCustomToken(): Promise<string> {
ensureAdminApp();
return getAuth().createCustomToken(E2E_TEST_UID);
}
Firebase Admin SDKでサーバー側からカスタムトークンを発行し、ブラウザ側でsignInWithCustomTokenに渡します。これで実際のFirebase Authのフローは通しつつ、メールアドレス・パスワードの入力やソーシャルログインの実行そのものは迂回できます。
もう1つの壁がApp Checkでした。Firebase App CheckのWeb SDKは、debug tokenをグローバル変数に設定しておくことで、reCAPTCHAを介さずトークンを取得できます。このappCheckDebugTokenを、Playwrightのfixtureからブラウザへ注入する形にしました。
Playwright導入の最初のPR(#258)で、web側は「サーバー登録→一覧表示→削除」、admin側は「ダッシュボード表示のスモークテスト」という最小限の代表フローを1本ずつ実装し、パターンを確立しました。続くPR(#261)で、web側の編集・通知設定変更、admin側の端末一覧・お問い合わせ対応といった残りの主要フローを追加しています。
3. iOS: 想定より進んでいた認証迂回と、テストが見つけた実バグ
iOS側のXCUITestに着手したとき、「Firebase実ログインを経由せずにテストを回すための起動引数」をこれから作る想定でいました。ところが実際に着手してみると、UITEST_STUB_AUTHという起動引数、専用のXCUITestターゲット、MockServerRepository/MockAccountRepositoryというモックリポジトリは、既にスクリーンショット撮影用のテスト(ScreenshotTests.swift)のために用意済みでした。issueに書いていた「やりたいこと」は実質終わっていて、今回の主な作業は実際のテストケースを追加することでした。
追加したのは、サーバーの登録・削除・編集、クラッシュレポート同意トグルの4テストです。ここで、テストを書く過程で実際のプロダクトバグを見つけました。詳細画面でサーバー名を編集して一覧へ戻っても、表示名が更新されないという不具合です。原因は、一覧を表示するViewControllerがviewDidLoadのタイミングでしか一覧を読み込んでおらず、登録時は明示的にリロードを呼んでいたのに、編集からの戻りでは同じ処理が無かった、という非対称な実装でした。viewWillAppearでのリロードを追加して修正しています。テストを書く作業そのものが、実装のバグを見つけるきっかけになりました。
4. XCUITest特有の罠2つ
実装中に、実機で確認して初めて分かった罠が2つありました。
1つは、テストターゲットからのローカライズ文字列の扱いです。画面タイトルの検証にString(localized:)を使うと、アプリ本体の文字列リソースではなく、テストバンドル自身の文字列リソースを探しに行きます。テストバンドルには当然その文字列が無いので、キー文字列がそのまま返ってきてしまいます。対策は素朴で、画面タイトルは日本語のリテラル文字列を直接書くことにしました。
もう1つは、モーダル画面とその背後の画面、両方のnavigation barがアクセシビリティツリーに同時に残ることです。サーバー登録フォームはモーダルで一覧の上に重なりますが、一覧側のnavigation barも消えずにツリーに残ります。app.navigationBars.buttonsのような、どのnavigation barかを指定しないクエリを書くと、モーダル側・背後側どちらのボタンを指しているか曖昧になり、実際に誤タップが起きました。対策は、app.navigationBars["サーバー"]のようにタイトルで対象のnavigation barを明示することです。
5. Jenkins CIへの統合と、まっさらな環境特有のバグ
当初、CIへの組み込みは「まずローカル実行を優先し、CIに入れるかは別途判断する」というスコープでした。ただ実際に運用してみると、プッシュ前のローカル実行だけでは、CI環境固有の問題を検出できません。最終的にJenkinsのPRレビューパイプラインへ統合し、web/adminへの変更を検知したときだけE2Eステージが走るようにしました。
この統合が終わったあと、別件(Trivyのイメージ脆弱性スキャン導入)のPRを検証している最中に、Jenkinsのまっさらなworkspace特有のバグが2件見つかりました。
1つ目は、APIコンテナがAPNsの鍵ファイル不足でlifespan起動に失敗する問題でした。開発機には既にファイルが置かれているため気づかず、まっさらなworkspaceで初めて表面化しました。
2つ目は、firebase-config.jsonの欠如でした。このファイルは.gitignore対象(appCheckDebugTokenのみが秘密)で、開発機には実体がありますが、Jenkinsのworkspaceには.sampleしかありません。nginxがこのファイルをまるごとbind mountしているため、実体が無いと/firebase-config.jsonが404のHTMLを返し、Playwright側のJSON.parseが失敗してログイン手順自体が動かなくなっていました。加えて、ログインは通っても、web側からのサーバー登録がApp Check強制で弾かれる問題も残っていました(adminはサーバー側のAPIキー経由のため影響を受けない。webはブラウザから直接POSTするため影響を受ける)。対策は、実ファイルが無ければ.sample(apiKey等の非秘密値は本物、appCheckDebugTokenだけプレースホルダ)で補うフォールバックをmake e2e-ciに追加し、既存のdocker-compose.postman.ymlと同じ理由でAPP_CHECK_ENFORCED=falseを加えることでした。
Jenkins側のステージは、web/admin変更時だけ条件付きで実行され、make e2e-ciの起動から後片付け(make e2e-ci-stop)までを自己完結させています。upやmigration自体が失敗した場合の後片付け漏れを防ぐため、実行を始めたことを示すフラグを見て、成功・失敗どちらでも後片付けを試みる安全網も入れています。
まとめ
- APIで確立していた「実認証・実App Checkはスコープ外にする」という割り切りを、Web/admin/iOSのUIテストにも一貫して適用した。
- Web/adminはFirebaseカスタムトークン+App Check debug token、iOSは起動引数によるDevBypassで、それぞれ実認証を迂回した。
- テストを書く作業そのものが、既存の実装の非対称なバグ(編集後に一覧が更新されない)を見つけるきっかけになった。
- XCUITestでは、テストバンドル自身のローカライズリソースを探しに行く挙動や、モーダルと背後の画面のnavigation barが両方ツリーに残る挙動など、実機で確認しないと分からない罠があった。
- ローカルで動くテストが、CIのまっさらな環境でそのまま動くとは限らない。開発機には既にある前提のファイル(APNs鍵、Firebase設定)が無いことで、初めて表面化する問題があった。
個人開発の環境での構成なので、そのまま組織に持ち込む場合は、テスト用Firebaseプロジェクトの分離やCredentialの管理ポリシーを先に確認してください。
JQITのエンジニアの95%以上は未経験からの採用です。
よければコーポレートサイトにも遊びに来てください。
未経験から学べます!一緒に挑戦していきましょう![]()
noteやXもやってます↓