ADB + FFmpeg を活用した Android デバイスファーム:10 ~ 50 台のデバイスでの自動 UI テスト

@ridark_eth
英語1 日前 · 2026年7月07日
209K
104
10
22
175

TL;DR

本ガイドでは、ADB と FFmpeg を活用し、数十台の Android デバイスで APK のインストール、UI テスト、動画レポート作成を自動化する Python パイプラインを解説します。費用対効果の詳細な分析も含まれています。

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

スタック全体は > Python + ADB + FFmpeg < という、標準的な QA ツールです。魔法はなく、単に面倒な作業を自動化するだけです。

Ridark - inline image

パイプラインアーキテクチャ

text
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. デバイス検出

Ridark - inline image

adb devices は、未承認 / オフライン状態のデバイスもリストします。device 状態のものだけを保持します。

python
1import subprocess
2
3def get_devices() -> list[str]:
4 """'device' 状態にある全デバイスのシリアルを返す。"""
5 out = subprocess.run(
6 ["adb", "devices"],
7 capture_output=True, text=True, check=True,
8 ).stdout
9
10 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 待ちになります)。

python
1from concurrent.futures import ThreadPoolExecutor, as_completed
2
3def 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.stdout
9 return serial, ok, (r.stdout + r.stderr).strip()
10
11def 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 に出力します。これが結果の判定基準です。

python
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 == 0
12 return serial, ok

package はテストパッケージ ID で、通常は [com.example.app](https://com.example.app/).test です。

4. テスト中の画面録画

screenrecord はデバイス上で直接ビデオを録画します。留意すべき制限: ファイルあたり約 3 分の上限と 音声なし。バックグラウンドで録画を開始し、テストを実行し、クリーンに停止してファイルをホストにプルします。

録画を停止する最も信頼性の高い方法は、ローカルの adb にシグナルを送るのではなく、デバイス自体で pkill を使用することです。これにより、screenrecord が MP4 コンテナを正しくファイナライズします。

python
1import time
2
3def start_recording(serial: str, remote: str = "/sdcard/run.mp4") -> subprocess.Popen:
4 return subprocess.Popen(
5 ["adb", "-s", serial, "shell", "screenrecord", remote]
6 )
7
8def 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 を使用してシリアルをオーバーレイします。その後、すべてのクリップが同一になり、最終的なアセンブリは再エンコードなしの高速な連結になります。

python
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 )
14
15def 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. すべてを統合

python
1def main() -> None:
2 apk = "app-debug.apk"
3 package = "com.example.app.test"
4 runner = "androidx.test.runner.AndroidJUnitRunner"
5
6 serials = get_devices()
7 if not serials:
8 print("No devices found. Check USB and the output of 'adb devices'.")
9 return
10
11 print(f"Devices found: {len(serials)}")
12 install_on_all(apk, serials)
13
14 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}")
20
21 out = f"{serial}_labeled.mp4"
22 label_clip(f"{serial}.mp4", out, serial)
23 labeled.append(out)
24
25 concat_report(labeled, "test_report.mp4")
26 print("Done: test_report.mp4")
27
28if __name__ == "__main__":
29 main()

ここでは "record + test" ループはデバイス間で順次実行されます。その方が読みやすいからです。実際のファームでは、このブロックも ThreadPoolExecutor でラップして、すべてのデバイスを一度にテストすることをお勧めします。ロジックはセクション 2 のインストールと同じです。

車輪の再発明はしないで: 既製ツール

Ridark - inline image
  • 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 つのコマンドで、アプリがデバイスフレート全体で実行され、各デバイスでの動作を示すビデオレポートが最後に生成されます。

お金を節約できる便利な記事を見逃さないために、ブックマークして購読してください 📝

ワンクリック保存

YouMindでバイラル記事をAI深読み

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
クリエイターのために

あなたの Markdown をきれいな 𝕏 記事に

自分の長文を投稿するとき、画像・表・コードブロックを 𝕏 向けに整形するのは手間がかかります。YouMind は Markdown 全体を、そのまま投稿できるきれいな 𝕏 記事に変換します。

Markdown → 𝕏 を試す

解読すべきパターンをもっと

最近のバイラル記事

バイラル記事をもっと見る