CI/CD con GitHub Actions: 37 tests de Appium corriendo en un emulador Android
8 intentos, errores reales, KVM, pantalla de 320px. Pipeline completo con emulador Android, Allure Report en GitHub Pages. 89% en CI.
Contexto
Este post documenta el proceso completo de armar CI/CD para tests de Appium con GitHub Actions.
Tengo 37 tests corriendo en mi celular físico (Motorola G51). 97.29% de éxito, 11 minutos de ejecución, Allure Report con screenshots on failure. Todo funciona.
Pero corre solo en mi máquina. Si alguien clona el repo, necesita un celular conectado y Appium corriendo. Eso no es un pipeline — es un setup local.
En mi serie de Selenium ya armé CI/CD con GitHub Actions. Chrome viene preinstalado en el runner, Java + Maven + correr tests. Pipeline simple. Para Appium el problema es otro: necesitás un emulador Android corriendo adentro del runner, con Appium Server levantado, el driver UiAutomator2 instalado, y el APK disponible. Es otro nivel de setup.
Por qué Appium CI es diferente
En Selenium CI:
Checkout → Java → Maven → Tests (Chrome ya está en el runner)
En Appium CI:
Checkout → Java → Node.js → Appium Server → Driver UiAutomator2
→ Android SDK → Crear emulador → Bootear emulador → Instalar APK → Tests
No es solo "correr tests". Es levantar un ecosistema Android entero dentro del runner.
Hacer el BaseTest configurable
Antes de tocar el pipeline, el BaseTest tenía todo hardcodeado para mi celular:
options.setUdid("ZY32FJFXNF"); // mi Motorola
options.setCapability("appium:chromedriverExecutable",
new File("../chromedriver-win64/chromedriver.exe").getAbsolutePath()); // Windows
Eso no funciona en CI. El emulador tiene otro UDID (emulator-5554), el runner es Linux, no hay chromedriver de Windows. Necesitaba que esos valores fueran configurables sin romper mi flujo local.
La solución: System.getProperty con valores por defecto.
// Configurable: local usa tu celular, CI usa emulador
String udid = System.getProperty("udid", "ZY32FJFXNF");
String appPath = System.getProperty("appPath", "apk/mda-2.2.0-25.apk");
options.setUdid(udid);
options.setApp(new File(appPath).getAbsolutePath());
// ChromeDriver solo si se especifica (local Windows)
String chromedriverPath = System.getProperty("chromedriverPath");
if (chromedriverPath != null) {
options.setCapability("appium:chromedriverExecutable",
new File(chromedriverPath).getAbsolutePath());
}
// Timeouts más altos para CI (emulador lento)
int serverInstallTimeout = Integer.parseInt(
System.getProperty("serverInstallTimeout", "60000"));
int serverLaunchTimeout = Integer.parseInt(
System.getProperty("serverLaunchTimeout", "60000"));
options.setCapability("appium:uiautomator2ServerInstallTimeout", serverInstallTimeout);
options.setCapability("appium:uiautomator2ServerLaunchTimeout", serverLaunchTimeout);
// Wait configurable
int waitTimeout = Integer.parseInt(System.getProperty("waitTimeout", "10"));
wait = new WebDriverWait(driver, Duration.ofSeconds(waitTimeout));
En local, sin cambiar nada, corre como siempre — usa los defaults de mi celular:
mvn clean test "-DchromedriverPath=../chromedriver-win64/chromedriver.exe"
En CI, Maven pasa los valores del emulador:
mvn clean test "-Dudid=emulator-5554" "-DwaitTimeout=60" \
"-DserverInstallTimeout=120000" "-DserverLaunchTimeout=120000"
El workflow
La herramienta clave es reactivecircus/android-emulator-runner. Es un GitHub Action que crea un AVD (Android Virtual Device), lo bootea, y ejecuta comandos dentro de ese contexto.
Este es el workflow final (después de 8 intentos):
name: Appium Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Appium
run: |
npm install -g appium
appium driver install uiautomator2
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run tests on emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
arch: x86_64
profile: pixel_6
emulator-options: -no-window -no-audio -no-snapshot
emulator-boot-timeout: 600
script: |
adb devices
adb wait-for-device
appium --relaxed-security &
sleep 10
cd appium-java-framework && mvn clean test "-Dudid=emulator-5554" "-DwaitTimeout=60" "-DserverInstallTimeout=120000" "-DserverLaunchTimeout=120000"
- name: Upload allure-results
if: always()
uses: actions/upload-artifact@v4
with:
name: allure-results
path: appium-java-framework/allure-results
- name: Install Allure CLI
if: always()
run: |
curl -o allure-2.29.0.tgz -OLs https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/2.29.0/allure-commandline-2.29.0.tgz
tar -zxvf allure-2.29.0.tgz
echo "$PWD/allure-2.29.0/bin" >> $GITHUB_PATH
- name: Generate Allure Report
if: always()
run: allure generate appium-java-framework/allure-results --clean -o allure-report
- name: Deploy to GitHub Pages
if: always()
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: allure-report
No llegué a este YAML de una. Fueron 8 iteraciones.
Los 8 intentos
Esto es lo que no se ve en los tutoriales. El camino real de armar un pipeline desde cero.
Intento 1: Action inexistente
Usé simple-elo/allure-report-action para generar el Allure Report. No existe. El workflow falló en "Set up job" antes de ejecutar un solo step.
La solución: instalar Allure CLI directo en el runner con curl y generar el reporte manualmente. Sin depender de actions de terceros que pueden desaparecer.

Intento 2: Maven no encuentra el pom.xml
El cd appium-java-framework y el mvn clean test estaban en líneas separadas del script. El emulator-runner ejecuta cada línea en un shell independiente — el cd moría con su shell y el mvn corría en la raíz del repo.
/usr/bin/sh -c cd appium-java-framework
/usr/bin/sh -c mvn clean test "-Dudid=emulator-5554"
La solución: unirlos con &&:
script: cd appium-java-framework && mvn clean test "-Dudid=emulator-5554"

Intento 3: Deploy sin permisos
El workflow no tenía permiso de escritura en el repo. GitHub Pages no podía pushear a la branch gh-pages.
remote: Permission to cesarbeassuarez/appium-java-framework.git denied to github-actions[bot].
La solución: agregar permissions: contents: write al job.

permissions: contents: write, el workflow no puede pushear a gh-pages.Intento 4: Todos los tests fallan — timeout en Products
Los tests corrieron (progreso), pero todos fallaron en setUp esperando la pantalla de Products. 10 segundos de wait no alcanzan en un emulador.
La solución: hacer el wait configurable con System.getProperty("waitTimeout", "10") y pasar 30 segundos desde CI.

Intento 5: socket hang up
Appium Server arrancaba ANTES de que el emulador existiera. Cuando los tests creaban sesiones, Appium perdía la conexión con el device.
Could not proxy command to the remote server. Original error: socket hang up
La solución: mover el start de Appium DENTRO del script del emulator-runner. Así el emulador ya está booteado cuando Appium arranca.

Intento 6: Sin KVM — emulador inutilizable
El emulador corría sin aceleración por hardware (KVM). Sin KVM, el emulador es tan lento que la instalación de UiAutomator2 Server hace timeout antes de siquiera arrancar.
You're running a Linux VM where hardware acceleration is not available.
The instrumentation process cannot be initialized within 30000ms timeout.
La solución: habilitar KVM en el runner con un step dedicado + subir los timeouts de instalación/lanzamiento del server.
Intento 7: 51% — pantalla de 320px
Con KVM habilitado, 19 de 37 tests pasaron. Los otros 17 fallaban buscando elementos que no encontraban. El problema no era velocidad — era el tamaño de pantalla del emulador: 320x640 px con densidad 160. Los botones quedaban debajo del fold y nunca eran visibles.
Mi Motorola G51: 1080x2400 px. El emulador default: 320x640 px.
La solución: agregar profile: pixel_6 al emulador. Eso le da una pantalla de 1080x2400, densidad 411 — similar a mi celular.

Intento 8: 89% — resultado estable
Con pantalla realista: 33 de 37 tests verdes. Los 3 que fallan son de GeoLocation — la app crashea ("My Demo App keeps stopping") porque el emulador usa la imagen default de Android que no incluye Google Play Services. La pantalla de GeoLocation necesita los servicios de ubicación de Google. Sin ellos, la app se cae.
El fallo intencional de LoginTest sigue ahí — correcto.

profile: pixel_6 y KVM habilitado: 33 de 37 tests verdes en CI.
Corrí una segunda vez para confirmar estabilidad. Mismo resultado: 89.18%, 33 verdes, 1 failed, 3 broken. Consistente.
Resultado final
Allure Report en vivo: cesarbeassuarez.github.io/appium-java-framework/

Resultados:
| Métrica | Valor |
|---|---|
| Tests totales | 37 |
| Pasados | 33 |
| Failed | 1 (intencional) |
| Broken | 3 (GeoLocation — sin Google Play Services) |
| Porcentaje | 89.18% |
| Tiempo de tests | ~10 minutos |
| Tiempo total pipeline | ~20 minutos |
| Tiempo local (celular físico) | ~22 minutos |
Por qué 3 tests fallan en CI:
Los 3 tests de GeoLocation (navegarAGeoLocation, verificarCoordenadasGeoLocation, startStopObserving) usan los servicios de ubicación de Google. El emulador corre con target: default, que no incluye Google Play Services. La app crashea al intentar acceder a la API de ubicación. El test de QR Scanner sí pasa porque solo necesita la cámara simulada del emulador.
La solución sería cambiar target: default a target: google_apis, pero decidí dejarlo así — es un resultado honesto y documentable.
Screenshot on failure funcionando en CI:
El screenshot automático de Allure captura la pantalla del emulador cuando un test falla. Mismo mecanismo que en local, sin cambios de código.

Cómo se hace en la industria
Un emulador gratis en GitHub Actions tiene limitaciones: es más lento que un celular físico (x2 en tiempo), algunas features no funcionan (Google Play Services con target default), y un solo device a la vez.
En producción, las empresas usan device farms en la nube: BrowserStack, Sauce Labs, AWS Device Farm, LambdaTest. El pipeline envía el APK y los tests al servicio, ellos los ejecutan en dispositivos reales o emuladores optimizados con hardware dedicado, y devuelven los resultados. Los tiempos son similares a correr en local porque el hardware es potente y dedicado. Además, la paralelización es clave: una suite de 37 tests que tarda 11 minutos en serie podría correr en 2-3 minutos distribuida en 5 dispositivos.
La arquitectura del pipeline es la misma — solo cambia dónde se ejecutan los tests. Lo que armé acá demuestra el flujo completo: pipeline → device → tests → reporte → deploy. Conectarlo a una device farm es cambiar el script que apunta al emulador local por uno que apunta al servicio en la nube.
Qué aprendí
Cosas que no sabía antes de armar este pipeline:
- El emulator-runner ejecuta cada línea del script en un shell separado. El
cdmuere con su shell. - Sin KVM habilitado, el emulador de Android es prácticamente inutilizable para tests.
- El perfil por defecto del emulador tiene pantalla de 320x640 px — los elementos quedan fuera de pantalla.
workflow_dispatchte deja correr el pipeline manualmente desde GitHub sin hacer push.- Appium Server debe arrancar DESPUÉS de que el emulador esté booteado, no antes.
- Las capabilities
uiautomator2ServerInstallTimeoutyuiautomator2ServerLaunchTimeoutson necesarias en emuladores lentos.
Estado actual del proyecto
| Qué | Estado |
|---|---|
| Tests totales | 37 |
| CI/CD | GitHub Actions con emulador Android |
| Allure Report en CI | Publicado en GitHub Pages |
| Allure Report local | Funciona con allure serve |
| Tests en celular físico | 97.29% (36 pass + 1 fail intencional) |
| Tests en CI (emulador) | 89.18% (33 pass + 1 fail + 3 broken) |
| Repo | github.com/cesarbeassuarez/appium-java-framework |
| Allure Report live | cesarbeassuarez.github.io/appium-java-framework/ |
Cierre de la serie
Este es el último post de la serie de Appium + Java.
Empecé eligiendo herramientas, configurando el entorno, conectando un celular físico. Pasé por locators, waits, Page Object Model, DataProviders, gestos, WebViews, permisos, Allure Reports, y ahora CI/CD.
El framework tiene 37 tests, 13 pages con POM, DataProviders en 3 test classes, Allure con @Step y screenshots, y un pipeline que corre en GitHub Actions con reporte publicado automáticamente.
Lo que queda pendiente: la comparación Appium vs Maestro. Pero eso viene después de construir una serie completa de Maestro. Comparar sin experiencia real en ambas herramientas no tiene sentido.