Wenn Ihre App auf einem ganzen Zoo von Dutzenden echten Smartphones, verschiedenen Herstellern, Android-Versionen und Bildschirmauflösungen funktionieren muss, wird manuelles Testen schnell zum Albtraum. Im Folgenden finden Sie eine Python-Pipeline, die selbstständig alle angeschlossenen Geräte erkennt, die APK parallel auf allen installiert, instrumentierte Tests ausführt, einen Video-Mitschnitt jedes Durchlaufs erstellt und diese Aufnahmen mit FFmpeg zu einem einzigen Video-Report zusammenfügt.
Der gesamte Stack > Python + ADB + FFmpeg < ist Standard-QA-Tooling. Keine Magie, nur die Automatisierung der lästigen Routinearbeit.

Pipeline-Architektur
1adb devices ──► Liste der Seriennummern2 │3 ▼4APK-Installation (parallel auf allen Geräten, ThreadPoolExecutor)5 │6 ▼7für jedes Gerät:8 screenrecord (im Hintergrund) → am instrument (Tests ausführen) → stoppen + Video holen9 │10 ▼11FFmpeg: Seriennummer auf jeden Clip einblenden + zusammenfügen ──► test_report.mp4
Was Sie brauchen
- ADB (Android Debug Bridge) aus den Android Platform Tools -> Gerätesteuerung.
- Python 3.10+ -> Orchestrierung (ich verwende list[str], tuple[...] ohne from __future__).
- FFmpeg -> Videoverarbeitung und -zusammenstellung.
- Geräte mit aktiviertem USB-Debugging, verbunden über USB (oder über Wi-Fi via adb tcpip).
Ein Prinzip, das sich durch den gesamten Code zieht: Ich übergebe Argumente an subprocess als Liste und ohne shell=True. Das ist sicherer (keine Injection über Dateinamen) und funktioniert auch mit Leerzeichen oder Sonderzeichen in Pfaden.
1. Geräteerkennung

adb devices listet auch Geräte im Status unauthorized / offline auf. Wir behalten nur die, die tatsächlich im Status device sind.
1import subprocess23def get_devices() -> list[str]:4 """Gibt die Seriennummern aller Geräte im Status 'device' zurück."""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:]: # erste Zeile ist der "List of devices"-Header12 line = line.strip()13 if line.endswith("\tdevice"): # unauthorized / offline aussortieren14 serials.append(line.split("\t")[0])15 return serials
2. Parallele APK-Installation
Die Installation auf 50 Geräten nacheinander ist langsam. Wir verteilen die Arbeit auf einen Thread-Pool: Jede adb install ist ein eigener Prozess, daher funktionieren Threads hier hervorragend (wir warten auf I/O, nicht auf CPU-Leistung).
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 'FEHLER'}] {serial}")17 if not ok:18 print(f" {log}")
Flags: -r -> Neuinstallation unter Beibehaltung der Daten, -g -> alle Laufzeitberechtigungen sofort erteilen (praktisch, damit Tests nicht an Berechtigungsdialogen scheitern).
3. Ausführen instrumentierter Tests
am instrument führt Espresso/JUnit-Tests auf dem Gerät aus. Bei Erfolg gibt es OK aus, bei Fehlschlag FAILURES!!! auf stdout -> daran erkennen wir das Ergebnis.
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 ist die Test-Package-ID, normalerweise com.example.app.test.
4. Bildschirmaufnahme während eines Tests
screenrecord zeichnet Videos direkt auf dem Gerät auf. Zu beachtende Einschränkungen: ein ~3-Minuten-Limit pro Datei und kein Ton. Wir starten die Aufnahme im Hintergrund, führen den Test aus, stoppen sie dann sauber und holen die Datei auf den Host.
Die zuverlässigste Methode, die Aufnahme zu stoppen, ist nicht das Signalisieren des lokalen adb, sondern pkill auf dem Gerät selbst -> so finalisiert screenrecord den MP4-Container korrekt.
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 auf dem Gerät bewirkt, dass screenrecord die Datei ordentlich schließt15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])16 proc.wait(timeout=10)17 time.sleep(1) # dem Gerät einen Moment geben, den Container zu finalisieren18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)
5. Zusammenstellen des Video-Reports mit FFmpeg
Verschiedene Geräte haben unterschiedliche Bildschirmauflösungen, daher kann man sie nicht einfach mit -c copy aneinanderhängen. Wir normalisieren jeden Clip auf ein einheitliches Format (1080×1920) und blenden die Seriennummer per drawtext ein. Danach sind alle Clips identisch und die finale Zusammenstellung ist eine schnelle Concat ohne erneute Kodierung.
1def label_clip(src: str, dst: str, label: str) -> None:2 """Skaliert auf 1080x1920 und blendet ein Label (die Geräteseriennummer) ein."""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. Alles zusammenfügen
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("Keine Geräte gefunden. Überprüfen Sie USB und die Ausgabe von 'adb devices'.")9 return1011 print(f"Gefundene Geräte: {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"[{'BESTANDEN' if ok else 'FEHLGESCHLAGEN'}] Tests auf {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("Fertig: test_report.mp4")2728if __name__ == "__main__":29 main()
Hier läuft die "Aufnahme + Test"-Schleife sequenziell über die Geräte -> das ist lesbarer. Für eine echte Farm sollten Sie diesen Block ebenfalls in einen ThreadPoolExecutor packen, damit alle Geräte gleichzeitig getestet werden; die Logik ist dieselbe wie bei der Installation in Abschnitt 2.
Das Rad nicht neu erfinden: Fertige Werkzeuge

- scrcpy -> Echtzeit-Spiegelung und Steuerung eines Geräts vom PC. Unverzichtbar beim Debuggen fehlschlagender Tests.
- Appium / Espresso / UI Automator -> vollwertige UI-Test-Frameworks; das am instrument oben ist ihre Engine.
- Gradle Managed Devices -> Tests auf Emulatoren direkt aus dem Build heraus, ohne manuelles ADB-Gefummel.
- Firebase Test Lab / AWS Device Farm -> eine Cloud-Flotte echter Geräte, falls Sie keine eigene Hardware vorhalten möchten.
- GNU parallel -> falls Sie die Orchestrierung lieber in Bash als in Python vornehmen.
Die Wirtschaftlichkeit: Was es kostet und was es spart
Testautomatisierung geht nicht darum, "Geld aus dem Nichts zu zaubern" -> es geht darum, die beiden teuersten Posten zu reduzieren: Personenstunden und Cloud-Minuten. Nachfolgend eine Schätzung für drei typische Größenordnungen. Die Zahlen sind illustrativ und hängen von Region, Gerätehersteller und Anbieterpreisen ab; prüfen Sie vor dem Kauf die aktuellen Tarife.
Eigene Farm -> einmalige Investition
Posten
Für 10 Geräte
Für 30 Geräte
Für 50 Geräte
Gebrauchte Android-Smartphones (~60 € pro Stück)
~600 €
~1.800 €
~3.000 €
Aktive USB-Hubs
~100 €
~250 €
~400 €
Mini-PC / Host
~400 €
~400 €
~500 €
Kabel, Rack, Kleinteile
~80 €
~150 €
~250 €
Einmalige Gesamtkosten
~1.200 €
~2.600 €
~4.150 €
Strom pro Monat
Centbeträge
~10–20 €
~20–40 €
Dies sind Investitionsausgaben: einmal zahlen, und die Farm läuft dann jahrelang nahezu kostenlos.
Cloud -> Sie zahlen pro Minute
Firebase Test Lab, AWS Device Farm, BrowserStack und ähnliche Anbieter berechnen pro Geräteminute – etwa 0,05–0,20 € pro Geräteminute. Ein einzelner Regressionstestlauf auf 30 Geräten mit je 5 Minuten ergibt 150 Geräteminuten, also etwa 7,50–30 € pro Lauf.
Multiplizieren Sie das mit der CI-Intensität:
Lauffrequenz
Läufe pro Monat
Kosten (bei ~15 €/Lauf)
2× pro Tag
~44
~660 €/Monat
10× pro Tag
~220
~3.300 €/Monat
Bei jedem Push (aktives Team)
500+
7.500+ €/Monat
Amortisation: Eine 30-Geräte-Farm (~2.600 €) amortisiert sich gegenüber der Cloud in etwa 4 Monaten bei bescheidenen 2 Läufen pro Tag; bei aktiver CI in unter einem Monat. Danach läuft die Cloud-Rechnung jeden Monat weiter, die Farm nicht.
Manuelle Arbeit -> was frei wird
Ein manueller Durchlauf eines einzelnen Regressionstestszenarios auf 30 Geräten entspricht etwa einem Arbeitstag eines QA-Ingenieurs. Regression zweimal pro Woche summiert sich auf etwa 8 Personentage pro Monat. Bei QA-Kosten von irgendwo um die 1.600–2.700 €/Monat ist das ein beträchtlicher Teil eines Gehalts, den die Pipeline für sinnvolle Arbeit freigibt – anstelle der "Verbinden–Installieren–Klicken–Aufnehmen"-Routine ×30.
Wie sich das in Geld umwandelt
Es gibt hier keine direkten Einnahmen "aus dem Skript" – aber drei indirekte, sehr reale Mechanismen:
- Schnellere Releases. Regression in Minuten statt an einem Tag → Sie liefern Funktionen häufiger aus → Sie reagieren schneller auf den Markt. Bei einem Abonnementprodukt wirkt sich das direkt auf Kundenbindung und Umsatz aus.
- Weniger Fehler in der Produktion. Einen Absturz auf einem bestimmten Samsung vor der Veröffentlichung zu finden, kostet Centbeträge; derselbe Absturz, der die Nutzer erreicht, bedeutet schlechtere Store-Bewertungen, Abwanderung und Rückerstattungen. Jeder früh gefundene Fehler ist eine Handvoll negativer Rezensionen, die nie geschrieben werden.
- Es lässt sich als Dienstleistung verkaufen. Der Aufbau einer Gerätefarm und von CI-Tests ist eine gefragte Rolle in der Freiberuflichkeit und im Outsourcing. Die obige Pipeline ist ein fertiger Kern für ein solches Angebot.
Der entscheidende Unterschied zu "Engagement-Farming-Systemen": Dort kommt das Geld aus der Täuschung von Algorithmen und endet in einer Sperre. Hier kommt es aus eingesparten Stunden und verhinderten Verlusten. Das erste bricht zusammen; das zweite ist ein nachhaltiges Geschäftsmodell, das Sie einem Kunden nicht peinlich präsentieren müssen.
Zusammenfassung
Das Ergebnis ist eine reproduzierbare Pipeline: Ein Befehl, und die App wird auf der gesamten Geräteflotte getestet, mit einem Video-Report am Ende, der das Verhalten auf jedem einzelnen Gerät zeigt.





