
2026/03/23 4:04
**タイトル:** モバイルアプリのQAをクレオに教える **目的:** AI「Claude」を、モバイルアプリの品質保証(QA)タスクを効果的に実行できるよう訓練する。 --- ### 1. 準備 - **ドキュメント収集** – ユーザーマニュアル、機能仕様書、および受入基準を揃える。 - **テスト環境構築** – 必要なOSバージョンを搭載したエミュレーターまたは実機を用意する。 - **テストデータ定義** – エッジケースと通常利用を網羅したサンプル入力を作成する。 ### 2. テスト計画 - **テストシナリオの特定** – 機能性、使いやすさ、パフォーマンス、セキュリティ、互換性など。 - **テストマトリクス作成** – 各機能をデバイス構成・OSにマッピングする表を作る。 - **合格基準の設定** – 各テストケースで「合格」または「不合格」となる条件を明確化。 ### 3. 実行手順 1. ターゲットデバイスごとにアプリを起動する。 2. 主なワークフロー(例:ログイン、チェックアウト、プロフィール更新)をナビゲートする。 3. 定義済みテストケースの入力を実施する。 4. 動作観察 – UI の応答性、エラーハンドリング、データ永続化などを確認。 5. 結果記録 – Pass/Fail ステータスと発生した異常点をメモ。 ### 4. 報告 - **欠陥ログ** – 明確な説明、再現手順、スクリーンショット、重大度を添えて登録。 - **所見まとめ** – パターンや繰り返し発生する問題点を強調。 - **改善提案** – 修正や機能向上のための推奨策を提示。 ### 5. 継続的改善 - **テスト網羅性の見直し** – 定期的にレビューし、機能追加に応じて新シナリオを追加。 - **繰り返しテストの自動化** – 可能な限り自動化して効率化。 - **ドキュメント更新** – QA 実施中に得た知見を反映させる。 --- *ガイド終了*
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
Zabriskie のコミュニティアプリは単一の開発者によって構築され、Web、iOS、および Android 上で実行する必要があるため、「リアル」であると感じられます。既存の React Web コードを Capacitor でラップすると、3 つすべてのプラットフォームでネイティブシェルとしてデプロイできます。サーバー駆動型 UI により、バックエンド JSON が画面レイアウトを記述できるため、更新は App Store の審査をバイパスします。
モバイル用の自動 QA は不足していました。著者は Claude を訓練し、Android と iOS デバイスの両方を操作しました:Android ではを使用し、WebView の Chrome DevTools Protocol ソケットを見つけてローカルポートに転送し、adb reverseでスクリーンショットを取得し、それらを S3 にアップロードして整形されたバグレポートを投稿します。iOS ではシミュレーターの制限を克服するために TCC.db を変更し、adb shell screencapのios-simulator-mcpを使用して UI 座標を検出し、アプリをアンインストール/再インストールした後でスクリプト化されたキーストロークやタップを実行します。ui_describe_point
Android のスイープは約 90 秒で 25 画面を 100 % カバーします;iOS のスイープも完全なカバレッジを達成しますが、アンインストール/再インストール、権限書き込み、SpringBoard 再起動、およびスクリプト化されたログインの複雑なシーケンスが必要です。
配備失敗は、Claude が Go バージョン不一致を修正している最中に無関係な変更(重複変数宣言)をコミットしたため発生し、E2E テストを破損させました。この出来事は、分離されたワークツリーとプッシュ前テストの必要性を浮き彫りにしました。
毎日の自動スイープはシミュレーター/エミュレータで実行され、すべてのプラットフォームで視覚的または機能的な問題についてバグレポートを自動生成します。これにより反復が高速化し、リグレッションを早期に検出でき、サポートコストが削減され、クロスプラットフォームプロジェクトにおける開発者の生産性が向上します。
本文
「人生がイージーストリートのように見えるとき、危険はあなたのドアの前にある」
— グレイトフル・デッド、『Uncle John’s Band』
(この名言を選んだ理由:Claude にテーマに合ったグレイトフル・デッドの歌詞を探してもらいましたが、“dead lyrics” を検索するとコンテンツフィルタリングポリシーがトリガーされ、API エラー 400 {…} が返ってきました。結局は自分でこの一文に決めました。)
私は Zabriskie を一人で構築しています—チームも投資家もいません。単に寝室で「コミュニティアプリ」を出荷し、インターネットにもっと良い集まる場所が必要だと考えているだけです。「プロダクト」を作る最初の教訓は、App Store にないものは存在しないということでした。ウェブ版は早期ユーザーから好評だったものの、毎日触れられることはありませんでした。なぜなら「アプリ」ではないと感じていたからです。本物であるかのように見えるには、まず 3 つのプラットフォーム—Web(高速イテレーションとテスト用)、iOS、Android(実際に人々が生活している場所)へ配信しなければならないという結論に至りました。
問題は私が一人であることです。3 つの別々のコードベースを書き、保守することはできません。解決策は Capacitor でした。既に構築した React ウェブアプリをそのままラップし、ネイティブシェル(Android の WebView、iOS の WKWebView)で包み込みます。これにより同じコードがすべてのプラットフォームで動作します。サーバー主導型 UI アーキテクチャ(バックエンドが JSON で画面レイアウトを送信し、クライアントはそれを描画する)と組み合わせれば、App Store のレビュー待ちなしに全プラットフォームへ変更をプッシュできます。1 つのコードベース、3 つのプラットフォーム、1 人の開発者――これが唯一の選択肢です。
しかし Capacitor はテストが行き届かない領域に入れます。Playwright はネイティブシェルの中身にアクセスできません。ブラウザタブではなくアプリになっているため、XCTest や Espresso などのネイティブテストフレームワークも WebView 内の HTML を操作できず、ネイティブ UI 要素として扱えません。ウェブツールにとってはネイティブすぎて、ネイティブツールにはウェブすぎるというギャップがあるため、この投稿ではそのギャップを埋めるためのテスト手法を紹介します。
Zabriskie は 3 つのプラットフォームすべてで動作しています。Web は Playwright でテストされ、毎回の push 時に 150 + の E2E テストが実行されます。しかしモバイルアプリは自動 QA がなく、ビジュアルレグレスチェックもありません。どちらかのクライアントが正しくレンダリングされているかを手作業でスクリーンショットを確認しないと分からなかったため、Claude に両モバイルプラットフォームを操作させ、スクリーンショットを撮り、問題を分析し、自動的にバグレポートを作成させることにしました。
Android:最も簡単なケース
接続の課題
Android エミュレーター内で
localhost はエミュレーター自身を指し、ホスト Mac には届きません。Capacitor アプリが localhost:3000 や localhost:8080 にアクセスしようとしても何も返ってこないため、adb reverse が必要です。
adb reverse tcp:3000 tcp:3000 adb reverse tcp:8080 tcp:8080
エミュレーターを再起動するたびに実行する必要があります。
本当の突破口
Capacitor アプリは Android WebView 内で走るため、WebViews は Chrome DevTools Protocol(CDP)ソケットを公開しています。これを見つけてローカルポートへ転送すれば、完全なプログラム制御が可能です。
# WebView の DevTools ソケットを取得 WV_SOCKET=$(adb shell "cat /proc/net/unix" | \ grep webview_devtools_remote | \ grep -oE 'webview_devtools_remote_[0-9]+' | head -1) # ローカルポートへ転送 adb forward tcp:9223 localabstract:$WV_SOCKET # CDP にアクセス curl http://localhost:9223/json
CDP を使えば認証は 1 本の WebSocket メッセージで済みます。
localStorage に JWT を注入し、フィードへ遷移するだけです。ナビゲーションも同様に window.location.href を設定すれば完了します。UI をタップしたりキーボード操作を行う必要がなく、Playwright や Puppeteer が使っているプロトコルと同じですが、対象は Android WebView です。
スクリーンショット自動化
adb shell screencap と組み合わせて、Python スクリプトでアプリの全 25 画面を約 90 秒で走査します。スクリーンショットごとにレイアウト崩れ、エラーメッセージ、画像欠損、空白画面、ステータスバー重なりなどを分析し、問題があれば zabriskie_bot として認証し、S3 にアップロードした後、フォーラムに整形されたバグレポートを投稿します。タイトルは [Android QA] Shows Hub: RSVP button overlaps venue text のようになり、自動化から来たことと影響画面が即座に分かります。
全自動タスクは毎朝 8:47 にスケジュールされ、最初の完全走査では 25 画面、重大な問題ゼロ、軽微なコスメティックノート 2 件のみでした。誰かが変更で画面を壊した場合でも、コーヒーを飲む前にバグレポートが作成されます。
iOS:最も難しいケース
iOS は単純だと考えていました。同じアプリ・同じ画面、Mac 上の Simulator で完結すると思っていたのですが、実際は小さな制約が重なることで「夜明け前の悪夢」になるほどでした。
メールアドレス入力ができない
最初に試したのは deep‑link を使い JWT を生成し
simctl openurl で URL を開く方法です。4 回試み、4 つの失敗モード(古いネイティブバンドル、プロダクションを指す設定、間違った JWT シークレット、IPv6 上で Vite dev サーバーが動いているときに Simulator が IPv4 を期待する)を経験しました。結果としてログインフォームは一切触れられませんでした。
そこで AppleScript でキーストロークを送る方法に戻りました。しかし
type="email" の入力フィールドに対し keystroke "@" は Shift+2 として解釈され、Simulator がキーボードショートカットとして扱ってしまいます。@ を入力するたびにサインアップ画面へ遷移したり、パスワードリセットへ行ったり、コンテキストメニューが開いたりしました。
貼り付けも失敗。Cmd+V は Simulator に捕捉され、
simctl pbcopy で iOS のペーストボードを設定しても文字化けします。macOS と iOS のクリップボードは別々のシステムです。
解決策:バックエンドのログインハンドラを
WHERE email = $1 OR username = $1 に変更し、フォーム入力を type="text" に更新、既知のパスワードを持つテストユーザーを作成。これにより「@」を必要とせず “qatest” と入力できるようになりました。キーボード制限回避のためのバックエンド修正です。
ネイティブダイアログが閉じられない
ログイン後、iOS は「通知送信を許可しますか?」という UIKit のネイティブダイアログを表示します。WebView ではなくネイティブ UI なので、macOS 側からの入力で閉じることはできません。
AppleScript で座標クリック、
cliclick で全座標に対してクリック、Python Quartz CGEvent マウスイベント、Return や Enter を押す、アクセシビリティツリー内のボタンを探す(公開されていない)、simctl privacy grant(iOS 26 では通知がサポート外)や simctl ui alert accept(存在しない)など多種多様に試みましたが、ダイアログは動かず、アプリをブロックしたままでした。
解決策:Simulator の TCC.db(プライバシー許可データベース)に
kTCCServiceUserNotification の事前承認を挿入し、SpringBoard を再起動します。タイミングが重要で、アプリをインストールする前に行わないとキャッシュされてしまいます。また、JavaScript が PushNotifications.requestPermissions() をログイン時に呼び出すため、localhost へのリクエストはスキップするガードも追加しました。
正しい順序は次の通りです:
- アプリをアンインストール →
- TCC 許可を書き込み →
- SpringBoard を再起動 →
- アプリを再インストール →
- 起動 →
- ログイン。
この順序でのみダイアログが表示されません。
座標によるナビゲーションができない(最初は)
アプリには右上に浮いたナビゲーションバーがあり、3 つのバブルボタン(Zロゴ、アバター、+)から垂直ドロップダウンを開く仕組みです。25 画面全てをテストするために特定の項目をタップしなければなりませんでした。CSS で座標が得られたものの、実際にはさまざまな失敗モードが存在しました。
- AppleScript の
は macOS ウィンドウ座標を使用するため、ウィンドウ位置・デバイス画面グループオフセット・Simulator のスケールモード(Point Accurate / Pixel Accurate / Fit Screen)・ツールバーの表示状態などを考慮しないと誤差が大きくなります。最初は 42 % 正確でした。click at - Facebook の
はデバイス論理ポイントでタップを送信(390x844)ので、メインナビゲーションボタンには有効ですが、ドロップダウン項目の座標がずれ、ドロップダウンが閉じてしまうか z‑index を超えて背後にあるコンテンツにタップされることがあります。2 回目は 57 % 正確でした。idb
突破口:
ios-simulator-mcp の ui_describe_point 関数を使って実際の UI 要素を検出し、座標を正確に取得します。
ui_describe_point(365, 163) → AXLabel: "Currents", type: Link, frame: (342, 159, 40x40)
各ドロップダウン項目を 48pt インクリメントで調べ、Y 座標は正しいが X がずれていた(+ ドロップダウンの項目は x = 258 ではなく 269)。11 ポイントの誤差がタップ先を間違えさせていました。確認座標と 1.5 秒の待機でドロップダウンアニメーションを考慮すれば、100 % の画面に到達できます。
最終的な組み合わせは:
ui_describe_point で発見し、idb ui tap で実行。座標推測ではなく測定することが鍵です。
基本的なギャップ
Android 認証: ws.send('{"method":"Runtime.evaluate","params":{"expression":"localStorage.setItem(\'token\',\'xxx\')"}}')
対して iOS は次のように手間取ります:
- アプリをアンインストール →
- TCC データベースを書き換え →
- SpringBoard を再起動 →
- アプリを再インストール →
- 5 秒待機 →
- 「Sign In」ボタンへタップ →
- メールフィールドに “qatest” を AppleScript で入力 →
- Tab キーでパスワードフィールドへ移動 →
- “qatest123” を入力 →
- Return キーを押す →
- さらに待機。
WKWebView は Chrome DevTools Protocol を公開しません。Safari Web Inspector は独自バイナリプロトコルで、
ios-webkit-debug-proxy は USB デバイスのみで動作します。safaridriver は macOS Safari に接続するため、Simulator の WebView には届きません。
Android は「ここに WebSocket がある、自由に操作していいよ」と言ってくれますが、iOS は「鍵付きのドアと Xcode を使うように」というメッセージを送ります。
真ん中で起きた混乱
Android の動作を確立し iOS を仕上げる過程で、別種の失敗――プラットフォーム制限ではなくエージェントの規律問題が発生しました。
Railway デプロイメントが Go バージョン不一致で失敗。ローカルの Go が自動更新で 1.26 に上り、
go.mod を 1.25 要求に書き換えてしまい、Dockerfile は golang:1.24-alpine を使っていました。2 ファイル修正だけだったはずです。
Claude は git worktree(リポジトリのクリーンな分離コピー)で作業しており、本来はこの isolated copy で修正を行うべきでした。しかし、メインリポジトリに
cd したため、全ての汚れたファイルをステージし、Go バージョン修正だけでなく QA ログインエンドポイント、バグフォーラム更新、iOS Simulator の回避策、E2E テスト設定変更、プッシュ通知コード、3 つの新しいスキルファイルまで含む PR を作成してしまいました。PR は自動マージされ、私が閉じる前に統合されてしまいました。
不良マージはテストスイート全体で重複した変数宣言を残し、
Email から Email or Username にプレースホルダー変更する単純な修正さえもすべての認証 E2E テストに失敗させました。カタログテストはローカル DB のレコード数 > 50 を前提としていたため、CI では機能しませんでした。
結果として、2 ファイル変更を行うだけなのに 3 回の PR と 4 回のコミットが必要になりました。最初の 2 回はテストをローカルで走らせずにプッシュしたため失敗。3 回目はテストを実行し合格しました。推測よりも「テストを走らせてからプッシュ」することを徹底すべきだったにも関わらず、自己の変更ではそれを無視してしまったのです。
まとめ
両方のプラットフォームで QA スキルが稼働しています。毎朝 Android エミュレーターと iOS Simulator が起動し、25 画面を走査・スクリーンショット取得・解析し、問題があれば自動でバグレポートを提出します。3 プラットフォームすべてがテストされ、自動的にバグ報告を行います。
教訓は相互に補完しています:
- CDP を使う:座標系と戦わず、ブラウザのデバッグプロトコルで操作できます。Android は無料で提供しますが、iOS では不可能です。回避策を増やすほど脆弱性が増えます。
- 測定し、推測はせず:アクセシビリティ API が iOS のナビゲーションを解決したように、ログを読むことも最初のステップです。ボタン位置を想像するのではなく、実際に確認します。
- ワークツリー内で作業:分離されたブランチや worktree を尊重し、外部へ抜けると無関係な変更が本番に混入します。
- プッシュ前にテストを走らせる:推測よりも実際の結果を見ることが重要です。3 回の「push‑and‑pray」ではなく、最初からテストで検証しましょう。
Apple の皆さんへ――もし読んでいるなら、Simulator WebView に CDP あるいは WebDriver を公開してください。人間が使うときは素晴らしいツールですが、AI が操作しようとするとほぼ無効になります。