Une ferme d'appareils Android avec ADB + FFmpeg : tests d'interface automatisés sur 10 à 50 appareils

@ridark_eth
ANGLAISil y a 1 jour · 07 juil. 2026
209K
104
10
22
175

TL;DR

Ce guide présente un pipeline Python permettant d'automatiser l'installation d'APK, les tests d'interface et la génération de rapports vidéo sur des dizaines d'appareils Android via ADB et FFmpeg, incluant une analyse détaillée coûts-avantages.

Quand votre application doit fonctionner sur une ménagerie de dizaines de téléphones réels, de différents fabricants, versions Android et résolutions d'écran, les tests manuels deviennent rapidement un cauchemar. Voici un pipeline Python qui découvre automatiquement tous les appareils connectés, installe l'APK sur tous en parallèle, exécute des tests instrumentés, enregistre une vidéo de chaque exécution, et assemble ces enregistrements en un seul rapport vidéo avec FFmpeg.

L'ensemble de la stack > Python + ADB + FFmpeg < est un outillage QA standard. Pas de magie, juste l'automatisation de la corvée.

Ridark - inline image

Architecture du pipeline

text
1adb devices ──► liste des numéros de série
2
3
4Installation APK (en parallèle sur tous les appareils, ThreadPoolExecutor)
5
6
7pour chaque appareil :
8 screenrecord (en arrière-plan) → am instrument (exécution des tests) → arrêt + récupération vidéo
9
10
11FFmpeg : superposition du numéro de série sur chaque clip + concaténation ──► test_report.mp4

Ce dont vous aurez besoin

  • ADB (Android Debug Bridge) depuis Android Platform Tools -> contrôle des appareils.
  • Python 3.10+ -> orchestration (j'utilise list[str], tuple[...] sans from __future__).
  • FFmpeg -> traitement et assemblage vidéo.
  • Appareils avec le débogage USB activé, connectés en USB (ou via Wi-Fi avec adb tcpip).

Un principe qui traverse tout le code : je passe les arguments à subprocess sous forme de liste et sans shell=True. C'est plus sûr (pas d'injection via les noms de fichiers) et ça ne casse pas avec les espaces ou les caractères spéciaux dans les chemins.

1. Découverte des appareils

Ridark - inline image

adb devices liste également les appareils en état non autorisé / hors ligne. Nous ne conservons que ceux qui sont réellement dans l'état "device".

python
1import subprocess
2
3def get_devices() -> list[str]:
4 """Retourne les numéros de série de tous les appareils dans l'état '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:]: # la première ligne est l'en-tête "List of devices"
12 line = line.strip()
13 if line.endswith("\tdevice"): # ignorer unauthorized / offline
14 serials.append(line.split("\t")[0])
15 return serials

2. Installation parallèle de l'APK

Installer sur 50 appareils un par un est lent. Nous répartissons le travail sur un pool de threads : chaque adb install est un processus séparé, donc les threads fonctionnent très bien ici (nous attendons des E/S, pas de calcul CPU).

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}")

Flags : -r -> réinstallation en conservant les données, -g -> accorder toutes les autorisations d'exécution immédiatement (pratique pour que les tests ne butent pas sur les dialogues d'autorisation).

3. Exécution des tests instrumentés

am instrument exécute les tests Espresso/JUnit sur l'appareil. Il affiche OK en cas de succès et FAILURES!!! en cas d'échec sur stdout -> c'est ainsi que nous déterminons le résultat.

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 est l'ID du package de test, généralement com.example.app.test.

4. Enregistrement de l'écran pendant un test

screenrecord enregistre la vidéo directement sur l'appareil. Limitations à garder à l'esprit : une limite d'environ 3 minutes par fichier et pas d'audio. Nous démarrons l'enregistrement en arrière-plan, exécutons le test, puis l'arrêtons proprement et récupérons le fichier sur l'hôte.

La façon la plus fiable d'arrêter l'enregistrement n'est pas de signaler l'adb local, mais d'utiliser pkill sur l'appareil lui-même -> ainsi screenrecord finalise correctement le conteneur 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 sur l'appareil permet à screenrecord de fermer le fichier correctement
15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])
16 proc.wait(timeout=10)
17 time.sleep(1) # laisser un moment à l'appareil pour finaliser le conteneur
18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)

5. Assemblage du rapport vidéo avec FFmpeg

Différents appareils ont différentes résolutions d'écran, donc vous ne pouvez pas simplement les concaténer avec -c copy. Nous normalisons chaque clip vers un format commun (1080×1920) et superposons le numéro de série via drawtext en cours de route. Après cela, tous les clips sont identiques et l'assemblage final est une concaténation rapide sans ré-encodage.

python
1def label_clip(src: str, dst: str, label: str) -> None:
2 """Mettre à l'échelle en 1080x1920 et superposer un libellé (le numéro de série de l'appareil)."""
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. Tout assembler

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("Aucun appareil trouvé. Vérifiez l'USB et la sortie de 'adb devices'.")
9 return
10
11 print(f"Appareils trouvés : {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 sur {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("Terminé : test_report.mp4")
27
28if __name__ == "__main__":
29 main()

Ici, la boucle "enregistrement + test" s'exécute séquentiellement sur tous les appareils -> c'est plus lisible ainsi. Pour une vraie ferme, vous voudriez également encapsuler ce bloc dans un ThreadPoolExecutor, afin que tous les appareils soient testés en même temps ; la logique est la même que pour l'installation dans la section 2.

Ne réinventez pas la roue : outils prêts à l'emploi

Ridark - inline image
  • scrcpy -> miroir et contrôle en temps réel d'un appareil depuis votre PC. Indispensable pour déboguer des tests qui échouent.
  • Appium / Espresso / UI Automator -> frameworks de test d'interface utilisateur complets ; le am instrument ci-dessus est leur moteur.
  • Gradle Managed Devices -> exécutez des tests sur des émulateurs directement depuis la build, sans manipulation manuelle d'ADB.
  • Firebase Test Lab / AWS Device Farm -> une flotte cloud d'appareils réels si vous préférez ne pas gérer votre propre matériel.
  • GNU parallel -> si vous préférez orchestrer depuis bash plutôt que Python.

L'économie : ce que ça coûte et ce que ça économise

L'automatisation des tests ne consiste pas à "gagner de l'argent à partir de rien" -> il s'agit de réduire les deux postes de dépenses les plus importants : les heures-personnes et les minutes cloud. Voici une estimation pour trois échelles typiques. Les chiffres sont indicatifs et dépendent de la région, du fabricant d'appareils et des tarifs des fournisseurs ; vérifiez les tarifs en vigueur avant d'acheter.

Votre propre ferme -> investissement unique

Article

Pour 10 appareils

Pour 30 appareils

Pour 50 appareils

Téléphones Android d'occasion (~60 $ chacun)

~600 $

~1 800 $

~3 000 $

Hubs USB alimentés

~100 $

~250 $

~400 $

Mini-PC / hôte

~400 $

~400 $

~500 $

Câbles, rack, divers

~80 $

~150 $

~250 $

Total unique

~1 200 $

~2 600 $

~4 150 $

Électricité par mois

quelques centimes

~10–20 $

~20–40 $

C'est une dépense d'investissement : payez une fois, et la ferme fonctionne ensuite pendant des années à un coût quasi nul.

Cloud -> vous payez à la minute

Firebase Test Lab, AWS Device Farm, BrowserStack et autres facturent par minute-appareil -> environ 0,05 à 0,20 $ par minute-appareil. Une seule exécution de régression sur 30 appareils à 5 minutes chacun représente 150 minutes-appareil, soit environ 7,5 à 30 $ par exécution.

Multipliez maintenant par l'intensité de l'IC :

Fréquence d'exécution

Exécutions par mois

Coût (à ~15 $/exécution)

2× par jour

~44

~660 $/mois

10× par jour

~220

~3 300 $/mois

À chaque push (équipe active)

500+

7 500 $+/mois

Seuil de rentabilité : une ferme de 30 appareils (~2 600 $) s'amortit face au cloud en environ 4 mois à un rythme modeste de 2 exécutions par jour ; avec une IC active, en moins d'un mois. Après cela, la facture cloud continue de couler chaque mois, et la ferme, non.

Travail manuel -> ce qui est libéré

Une exécution manuelle d'un seul scénario de régression sur 30 appareils représente environ une journée de travail d'un ingénieur QA. Exécuter la régression deux fois par semaine représente environ 8 jours-personnes par mois. Pour un coût QA d'environ 1 600 à 2 700 $/mois, c'est une part significative d'un salaire que le pipeline libère pour un travail significatif au lieu de la corvée "connecter–installer–toucher–enregistrer" ×30.

Comment cela se traduit en argent

Il n'y a pas de revenu direct "du script" ici -> il y a trois mécanismes indirects mais très réels :

  • Des versions plus rapides. La régression en minutes au lieu d'une journée → vous publiez des fonctionnalités plus souvent → vous répondez plus vite au marché. Pour un produit par abonnement, cela se traduit directement par la rétention et les revenus.
  • Moins de bugs en production. Attraper un crash sur un Samsung spécifique avant la publication coûte quelques centimes ; le même crash atteignant les utilisateurs signifie des notes de magasin en baisse, du churn et des remboursements. Chaque bug attrapé tôt, ce sont quelques avis négatifs qui ne seront jamais écrits.
  • Ça se vend comme un service. La mise en place d'une ferme d'appareils et de tests IC est un rôle très demandé en freelance et en sous-traitance. Le pipeline ci-dessus est un noyau prêt à l'emploi pour une telle offre.

La différence clé avec les "schémas de farming d'engagement" : là, l'argent vient de la tromperie des algorithmes et se termine par un bannissement. Ici, il vient des heures économisées et des pertes évitées. Le premier s'effondre ; le second est un cas d'affaires durable dont vous n'avez pas honte de faire la démonstration à un client.

Conclusion

Le résultat est un pipeline reproductible : une seule commande, et l'application est testée sur l'ensemble du parc d'appareils, avec un rapport vidéo à la fin qui montre le comportement sur chaque appareil spécifique.

Ajoutez cette page à vos favoris et abonnez-vous pour ne pas manquer les articles utiles qui vous feront économiser de l'argent 📝

Enregistrer en un clic

Lire les articles viraux en profondeur avec l’IA de YouMind

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

Explore YouMind
Pour les créateurs

Transformez votre Markdown en un article 𝕏 impeccable

Quand vous publiez vos propres textes longs, la mise en forme 𝕏 des images, tableaux et blocs de code est pénible. YouMind transforme un brouillon Markdown complet en un article 𝕏 impeccable, prêt à publier.

Essayer Markdown vers 𝕏

D'autres patterns à décoder

Articles viraux récents

Explorer plus d'articles viraux