Cuando tu aplicación tiene que funcionar en un zoológico de docenas de teléfonos reales, diferentes fabricantes, versiones de Android y resoluciones de pantalla, las pruebas manuales se convierten rápidamente en una pesadilla. A continuación, te presento un pipeline en Python que descubre todos los dispositivos conectados por sí solo, instala el APK en todos ellos en paralelo, ejecuta pruebas instrumentadas, graba un video de cada ejecución y une esas grabaciones en un único informe de video con FFmpeg.
Todo el stack > Python + ADB + FFmpeg < es el conjunto de herramientas estándar de control de calidad. Sin magia, solo automatizando el trabajo pesado.

Arquitectura del pipeline
1adb devices ──► lista de seriales2 │3 ▼4Instalación del APK (en paralelo en todos los dispositivos, ThreadPoolExecutor)5 │6 ▼7para cada dispositivo:8 screenrecord (en segundo plano) → am instrument (ejecutar pruebas) → detener + extraer video9 │10 ▼11FFmpeg: superponer serial en cada clip + concatenar ──► test_report.mp4
Lo que necesitarás
- ADB (Android Debug Bridge) de Android Platform Tools -> control de dispositivos.
- Python 3.10+ -> orquestación (uso list[str], tuple[...] sin from __future__).
- FFmpeg -> procesamiento y ensamblaje de video.
- Dispositivos con depuración USB habilitada, conectados por USB (o por Wi-Fi mediante adb tcpip).
Un principio que recorre todo el código: paso los argumentos a subprocess como una lista y sin shell=True. Es más seguro (sin inyección a través de nombres de archivo) y no se rompe con espacios o caracteres especiales en las rutas.
1. Descubrimiento de dispositivos

adb devices también lista dispositivos en estado no autorizado / fuera de línea. Solo conservamos aquellos que están realmente en estado de dispositivo.
1import subprocess23def get_devices() -> list[str]:4 """Devuelve los seriales de todos los dispositivos en estado '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:]: # la primera línea es el encabezado "List of devices"12 line = line.strip()13 if line.endswith("\tdevice"): # descartar no autorizados / fuera de línea14 serials.append(line.split("\t")[0])15 return serials
2. Instalación paralela del APK
Instalar en 50 dispositivos uno por uno es lento. Distribuimos el trabajo en un grupo de hilos: cada adb install es un proceso separado, por lo que los hilos funcionan muy bien aquí (estamos esperando E/S, no quemando CPU).
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}")
Banderas: -r -> reinstalar conservando datos, -g -> otorgar todos los permisos de ejecución de inmediato (útil para que las pruebas no tropiecen con diálogos de permisos).
3. Ejecución de pruebas instrumentadas
am instrument ejecuta pruebas Espresso/JUnit en el dispositivo. Imprime OK en caso de éxito y FAILURES!!! en caso de fallo en stdout -> así determinamos el resultado.
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 es el ID del paquete de prueba, normalmente com.example.app.test.
4. Grabación de la pantalla durante una prueba
screenrecord graba video directamente en el dispositivo. Limitaciones a tener en cuenta: un límite de ~3 minutos por archivo y sin audio. Iniciamos la grabación en segundo plano, ejecutamos la prueba, luego la detenemos limpiamente y extraemos el archivo al host.
La forma más confiable de detener la grabación no es señalizando el adb local, sino con pkill en el propio dispositivo -> así screenrecord finaliza el contenedor MP4 correctamente.
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 en el dispositivo hace que screenrecord cierre el archivo correctamente15 subprocess.run(["adb", "-s", serial, "shell", "pkill", "-SIGINT", "screenrecord"])16 proc.wait(timeout=10)17 time.sleep(1) # darle un momento al dispositivo para finalizar el contenedor18 subprocess.run(["adb", "-s", serial, "pull", remote, local], check=True)
5. Ensamblaje del informe de video con FFmpeg
Diferentes dispositivos tienen diferentes resoluciones de pantalla, por lo que no se pueden concatenar simplemente con -c copy. Normalizamos cada clip a un formato común (1080×1920) y superponemos el serial mediante drawtext durante el proceso. Después de eso, todos los clips son idénticos y el ensamblaje final es una concatenación rápida sin necesidad de recodificación.
1def label_clip(src: str, dst: str, label: str) -> None:2 """Escalar a 1080x1920 y superponer una etiqueta (el serial del dispositivo)."""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. Poniéndolo todo junto
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 se encontraron dispositivos. Verifica el USB y la salida de 'adb devices'.")9 return1011 print(f"Dispositivos encontrados: {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'}] pruebas en {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("Listo: test_report.mp4")2728if __name__ == "__main__":29 main()
Aquí el bucle "grabar + probar" se ejecuta secuencialmente en todos los dispositivos -> es más legible así. Para un parque real, querrías envolver este bloque en un ThreadPoolExecutor también, para que todos los dispositivos se prueben a la vez; la lógica es la misma que la instalación en la sección 2.
No reinventes la rueda: herramientas listas para usar

- scrcpy -> duplicación y control en tiempo real de un dispositivo desde tu PC. Indispensable al depurar pruebas fallidas.
- Appium / Espresso / UI Automator -> marcos de pruebas de interfaz de usuario completos; el am instrument anterior es su motor.
- Gradle Managed Devices -> ejecuta pruebas en emuladores directamente desde la compilación, sin necesidad de manejar ADB manualmente.
- Firebase Test Lab / AWS Device Farm -> una flota en la nube de dispositivos reales si prefieres no mantener tu propio hardware.
- GNU parallel -> si prefieres orquestar desde bash en lugar de Python.
La economía: qué cuesta y qué ahorra
La automatización de pruebas no se trata de "hacer dinero de la nada" -> se trata de reducir los dos elementos más costosos: horas-persona y minutos en la nube. A continuación, una estimación en tres escalas típicas. Las cifras son ilustrativas y dependen de la región, el fabricante del dispositivo y los precios del proveedor; verifica las tarifas actuales antes de comprar.
Tu propio parque -> inversión única
Elemento
Para 10 dispositivos
Para 30 dispositivos
Para 50 dispositivos
Teléfonos Android usados (~$60 cada uno)
~$600
~$1,800
~$3,000
Hubs USB con alimentación
~$100
~$250
~$400
Mini-PC / host
~$400
~$400
~$500
Cables, rack, varios
~$80
~$150
~$250
Total único
~$1,200
~$2,600
~$4,150
Electricidad por mes
centavos
~$10–20
~$20–40
Esto es gasto de capital: pagas una vez, y el parque funciona durante años casi sin costo.
Nube -> pagas por minuto
Firebase Test Lab, AWS Device Farm, BrowserStack y similares cobran por minuto-dispositivo -> aproximadamente $0.05–0.20 por minuto-dispositivo. Una sola ejecución de regresión en 30 dispositivos a 5 minutos cada uno son 150 minutos-dispositivo, es decir, ~$7.5–30 por ejecución.
Ahora multiplica por la intensidad de CI:
Frecuencia de ejecución
Ejecuciones por mes
Costo (a ~$15/ejecución)
2× al día
~44
~$660/mes
10× al día
~220
~$3,300/mes
En cada push (equipo activo)
500+
$7,500+/mes
Punto de equilibrio: un parque de 30 dispositivos (~$2,600) se amortiza frente a la nube en aproximadamente 4 meses con una modesta frecuencia de 2 ejecuciones al día; con CI activo, en menos de un mes. Después de eso, la factura de la nube sigue goteando cada mes, y el parque no.
Trabajo manual -> lo que se libera
Una ejecución manual de un solo escenario de regresión en 30 dispositivos equivale aproximadamente a un día laboral de un ingeniero de QA. Ejecutar la regresión dos veces por semana suma ~8 días-persona al mes. Con un costo de QA de alrededor de $1,600–2,700/mes, eso es una parte significativa de un salario que el pipeline libera para trabajo significativo en lugar del tedioso "conectar–instalar–probar–grabar" ×30.
Cómo se convierte esto en dinero
No hay ingresos directos "del script" aquí -> hay tres mecanismos indirectos pero muy reales:
- Lanzamientos más rápidos. Regresión en minutos en lugar de un día → lanzas funciones con más frecuencia → respondes al mercado más rápido. Para un producto de suscripción, esto se vincula directamente con la retención y los ingresos.
- Menos errores en producción. Detectar un bloqueo en un Samsung específico antes del lanzamiento cuesta centavos; el mismo bloqueo que llega a los usuarios significa calificaciones bajas en la tienda, pérdida de clientes y reembolsos. Cada error detectado temprano son un puñado de reseñas negativas que nunca se escriben.
- Se vende como servicio. Configurar un parque de dispositivos y pruebas de CI es un rol demandado en freelancing y subcontratación. El pipeline anterior es un núcleo listo para tal oferta.
La diferencia clave con los "esquemas de generación de engagement": allí, el dinero proviene de engañar a los algoritmos y termina en una prohibición. Aquí, proviene de horas ahorradas y pérdidas evitadas. El primero se desmorona; el segundo es un caso de negocio sostenible del que no te avergüenzas mostrar a un cliente.
Resumen
El resultado es un pipeline reproducible: un comando, y la aplicación se prueba en toda la flota de dispositivos, con un informe de video al final que muestra el comportamiento en cada dispositivo específico.





![Procedimiento completo para ahorrar 5 millones de yenes en 6 meses usando Fable 5 y note [Incluye prompts para copiar y pegar]](/cdn-cgi/image/width=1920,quality=90,format=auto,metadata=none/https%3A%2F%2Fcms-assets.youmind.com%2Fmedia%2F1783533977044_mlxvvu_HMoBqNQaMAA7EsU.jpg)