アプリが数十台の実機、異なるベンダー、Android バージョン、画面解像度の「動物園」で動作しなければならない場合、手動テストはすぐに悪夢と化します。以下は、接続されているすべてのデバイスを自動検出し、全デバイスに並行して APK をインストールし、インストルメント化テストを実行し、各実行のビデオを録画し、それらの録画を FFmpeg で単一のビデオレポートに結合する Python パイプラインです。
スタック全体は > Python + ADB + FFmpeg < という、標準的な QA ツールです。魔法はなく、単に面倒な作業を自動化するだけです。

パイプラインアーキテクチャ
1adb devices ──► シリアル一覧2 │3 ▼4APK インストール (全デバイスに並行、ThreadPoolExecutor)5 │6 ▼7各デバイス:8 screenrecord (バックグラウンド) → am instrument (テスト実行) → 停止 + ビデオ取得9 │10 ▼11FFmpeg: 各クリップにシリアルをオーバーレイ + 連結 ──► test_report.mp4
必要なもの
- ADB (Android Debug Bridge) - Android Platform Tools から。デバイス制御用。
- Python 3.10+ - オーケストレーション用 (
from __future__なしでlist[str],tuple[...]を使用)。 - FFmpeg - ビデオ処理とアセンブリ用。
- USB デバッグが有効になっていて、USB (または
adb tcpip経由の Wi-Fi) で接続されたデバイス。
すべてのコードに通じる原則: サブプロセスへの引数は リストとして 渡し、`shell=True` は使用しません。これにより安全性が高まり (ファイル名によるインジェクションがない)、パス内のスペースや特殊文字で問題が発生しません。
1. デバイス検出

adb devices は、未承認 / オフライン状態のデバイスもリストします。device 状態のものだけを保持します。
1import subprocess23def get_devices() -> list[str]:4 """'device' 状態にある全デバイスのシリアルを返す。"""5 out = subprocess.run(6 ["adb", "devices"],7 capture_output=True, text=True, check=True,8 ).stdout910 serials: list[str] = []11 for line in out.splitlines()[1:]: # 最初の行は "List of devices" ヘッダー12 line = line.strip()13 if line.endswith("\tdevice"): # unauthorized / offline は除外14 serials.append(line.split("\t")[0])15 return serials
2. 並行 APK インストール
50 台のデバイスに 1 台ずつインストールするのは時間がかかります。スレッドプールで処理を分散します。各 adb install は個別のプロセスであるため、スレッドはここで非常に効果的です (CPU を消費するのではなく、I/O 待ちになります)。
1from concurrent.futures import ThreadPoolExecutor, as_completed23def install_apk(serial: str, apk_path: str) -> tuple[str, bool, str]:4 r = subprocess.run(5 ["adb", "-s", serial, "install", "-r", "-g", apk_path],6 capture_output=True, text=True,7 )8 ok = r.returncode == 0 and "Success" in r.stdout9 return serial, ok, (r.stdout + r.stderr).strip()1011def install_on_all(apk_path: str, serials: list[str]) -> None:12 with ThreadPoolExecutor(max_workers=len(serials) or 1) as pool:13 futures = [pool.submit(install_apk, s, apk_path) for s in serials]14 for f in as_completed(futures):15 serial, ok, log = f.result()16 print(f"[{'OK' if ok else 'FAIL'}] {serial}")17 if not ok:18 print(f" {log}")
フラグ: -r -> データを保持したまま再インストール、-g -> すべての実行時権限を即座に付与 (テストが権限ダイアログで止まらないようにするのに便利)。
3. インストルメント化テストの実行
am instrument はデバイス上で Espresso/JUnit テストを実行します。成功時は OK、失敗時は FAILURES!!! を stdout に出力します。これが結果の判定基準です。
1def run_instrumented_tests(2 serial: str,3 package: str,4 runner: str = "androidx.test.runner.AndroidJUnitRunner",5) -> tuple[str, bool]:6 r = subprocess.run(7 ["adb", "-s", serial, "shell", "am", "instrument", "-w",8 f"{package}/{runner}"],9 capture_output=True, text=True,10 )11 ok = "FAILURES!!!" not in r.stdout and r.returncode == 012 return serial, ok
package はテストパッケージ ID で、通常は [com.example.app](https://com.example.app/).test です。
4. テスト中の画面録画
screenrecord はデバイス上で直接ビデオを録画します。留意すべき制限: ファイルあたり約 3 分の上限と 音声なし。バックグラウンドで録画を開始し、テストを実行し、クリーンに停止してファイルをホストにプルします。
録画を停止する最も信頼性の高い方法は、ローカルの adb にシグナルを送るのではなく、デバイス自体で pkill を使用することです。これにより、screenrecord が MP4 コンテナを正しくファイナライズします。
1import time23def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:4 return subprocess.Popen(5 ["adb", "-s", serial, "shell", "screenrecord", remote]6 )78def stop_recording(9 serial: str,10 proc: subprocess.Popen,11 remote: str = "/sdcard/run.mp4",12 local: str = "run.mp4",13) -> None:14 # デバイス上の SIGINT により screenrecord がファイルを適切に閉じる15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])16 proc.wait(timeout=10)17 time.sleep(1) # デバイスがコンテナをファイナライズする時間を与える18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)
5. FFmpeg を使用したビデオレポートのアセンブリ
デバイスによって画面解像度が異なるため、-c copy で単純に連結することはできません。各クリップを共通フォーマット (1080×1920) に正規化し、その過程で drawtext を使用してシリアルをオーバーレイします。その後、すべてのクリップが同一になり、最終的なアセンブリは再エンコードなしの高速な連結になります。
1def label_clip(src: str, dst: str, label: str) -> None:2 """1080x1920 にスケーリングし、ラベル (デバイスシリアル) をオーバーレイする。"""3 vf = (4 "scale=1080:1920:force_original_aspect_ratio=decrease,"5 "pad=1080:1920:(ow-iw)/2:(oh-ih)/2,"6 f"drawtext=text='{label}':x=20:y=20:fontsize=42:"7 "fontcolor=white:box=1:[email protected]"8 )9 subprocess.run(10 ["ffmpeg", "-y", "-i", src, "-vf", vf,11 "-an", "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", dst],12 check=True,13 )1415def concat_report(clips: list[str], out: str = "test_report.mp4") -> None:16 with open("concat_list.txt", "w") as f:17 for c in clips:18 f.write(f"file '{c}'\n")19 subprocess.run(20 ["ffmpeg", "-y", "-f", "concat", "-safe", "0",21 "-i", "concat_list.txt", "-c", "copy", out],22 check=True,23 )
6. すべてを統合
1def main() -> None:2 apk = "app-debug.apk"3 package = "com.example.app.test"4 runner = "androidx.test.runner.AndroidJUnitRunner"56 serials = get_devices()7 if not serials:8 print("No devices found. Check USB and the output of 'adb devices'.")9 return1011 print(f"Devices found: {len(serials)}")12 install_on_all(apk, serials)1314 labeled: list[str] = []15 for serial in serials:16 proc = start_recording(serial)17 _, ok = run_instrumented_tests(serial, package, runner)18 stop_recording(serial, proc, local=f"{serial}.mp4")19 print(f"[{'PASS' if ok else 'FAIL'}] tests on {serial}")2021 out = f"{serial}_labeled.mp4"22 label_clip(f"{serial}.mp4", out, serial)23 labeled.append(out)2425 concat_report(labeled, "test_report.mp4")26 print("Done: test_report.mp4")2728if __name__ == "__main__":29 main()
ここでは "record + test" ループはデバイス間で順次実行されます。その方が読みやすいからです。実際のファームでは、このブロックも ThreadPoolExecutor でラップして、すべてのデバイスを一度にテストすることをお勧めします。ロジックはセクション 2 のインストールと同じです。
車輪の再発明はしないで: 既製ツール

- scrcpy -> PC からデバイスをリアルタイムにミラーリングおよび制御。テスト失敗のデバッグに不可欠。
- Appium / Espresso / UI Automator -> 本格的な UI テストフレームワーク。上記の
am instrumentはそのエンジンです。 - Gradle Managed Devices -> ビルドから直接エミュレータでテストを実行。手動の ADB 操作は不要。
- Firebase Test Lab / AWS Device Farm -> 独自のハードウェアを維持したくない場合の、クラウド上の実機ファーム。
- GNU parallel -> Python ではなく bash からオーケストレーションしたい場合。
経済性: コストと節約効果
テスト自動化は「無から金を生み出す」ことではありません。最もコストのかかる 2 つの項目、人件費 と クラウド利用料 を削減することです。以下は、3 つの典型的な規模での見積もりです。数値は参考値であり、地域、デバイスベンダー、プロバイダーの価格設定によって異なります。購入前に最新のレートを確認してください。
自前ファーム -> 初期投資
項目
10 台の場合
30 台の場合
50 台の場合
中古 Android スマートフォン (約 $60/台)
約 $600
約 $1,800
約 $3,000
電源供給 USB ハブ
約 $100
約 $250
約 $400
ミニ PC / ホスト
約 $400
約 $400
約 $500
ケーブル、ラック、その他
約 $80
約 $150
約 $250
初期費用合計
約 $1,200
約 $2,600
約 $4,150
月額電気代
わずか
約 $10~20
約 $20~40
これは設備投資です。一度支払えば、ファームはほとんど追加コストなしで何年も稼働します。
クラウド -> 分単位の支払い
Firebase Test Lab、AWS Device Farm、BrowserStack などは、デバイス分単位で課金されます。おおよそ 1 デバイス分あたり $0.05~0.20 です。30 台のデバイスで 1 回のリグレッション実行 (各 5 分) は 150 デバイス分、つまり約 $7.5~30 になります。
これを CI の頻度で掛け合わせます:
実行頻度
月間実行回数
コスト (約 $15/回の場合)
1 日 2 回
約 44 回
約 $660/月
1 日 10 回
約 220 回
約 $3,300/月
プッシュ毎 (アクティブなチーム)
500 回以上
$7,500+/月
損益分岐点: 30 台のファーム (約 $2,600) は、1 日 2 回の控えめな実行でも約 4 ヶ月でクラウド費用を回収できます。アクティブな CI の場合は 1 ヶ月未満です。その後もクラウドの請求は毎月発生し続けますが、ファームにはかかりません。
手動作業 -> 解放されるリソース
30 台のデバイスでの単一リグレッションシナリオの手動実行は、QA エンジニアの丸 1 日分 の作業に相当します。週 2 回のリグレッション実行は、月に約 8 人日になります。QA コストが月額約 $1,600~2,700 であることを考えると、これは給与のかなりの部分であり、パイプラインによって「接続→インストール→操作→記録」という作業 ×30 から解放され、有意義な作業に充てることができます。
これがどのように収益に変わるか
ここで「スクリプトから直接収入が得られる」わけではありません。しかし、間接的でありながら非常に現実的な 3 つのメカニズムがあります。
- リリースの高速化。 リグレッションが 1 日から数分に短縮 → 機能をより頻繁に出荷 → 市場により迅速に対応。サブスクリプション製品の場合、これは顧客維持と収益に直接結びつきます。
- 本番環境のバグ減少。 特定の Samsung 端末でのクラッシュをリリース前に捕捉するコストはわずかです。同じクラッシュがユーザーに届くと、ストアの評価低下、解約、返金につながります。早期に捕捉されたバグは、書かれることのない否定的なレビューに相当します。
- サービスとして販売可能。 デバイスファームのセットアップと CI テストは、フリーランスやアウトソーシングで需要の高い役割です。上記のパイプラインは、そのようなサービスの既製のコアとなります。
「エンゲージメントファーミングスキーム」との重要な違い: そこでは、収益はアルゴリズムを欺くことから生まれ、最終的にはアカウント停止に終わります。ここでは、節約された時間と防止された損失から生まれます。前者は崩壊しますが、後者はクライアントに誇れる持続可能なビジネスケースです。
まとめ
結果は再現可能なパイプラインです。1 つのコマンドで、アプリがデバイスフレート全体で実行され、各デバイスでの動作を示すビデオレポートが最後に生成されます。




![Fable 5 と note を活用して 6 ヶ月で 500 万円を貯める全手順 [コピペ可能なプロンプト付き]](/cdn-cgi/image/width=1920,quality=90,format=auto,metadata=none/https%3A%2F%2Fcms-assets.youmind.com%2Fmedia%2F1783533977044_mlxvvu_HMoBqNQaMAA7EsU.jpg)
