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.

Allure Report en GitHub Pages mostrando 89.18 por ciento con 37 tests de Appium corriendo en emulador Android via GitHub Actions
37 tests, 89.18% en CI. 33 verdes, 1 fallo intencional, 3 broken por falta de Google Play Services en el emulador.

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.

Error en GitHub Actions Set up job mostrando Unable to resolve action simple-elo allure-report-action repository not found
Primer intento. El workflow ni arrancó — la action para Allure Report no existe.

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"
Log de GitHub Actions mostrando cd y mvn en shells separados con error there is no POM in this directory
El emulator-runner ejecuta cada línea en un shell independiente. El cd muere con su shell.

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.

Log de GitHub Actions mostrando Permission denied to github-actions bot al intentar push a gh-pages con error 403
Sin 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.

Allure Report mostrando 0 por ciento con 45 test cases y barra amarilla de 8 broken y 37 skipped
Todos los tests fallan en setUp. 10 segundos de wait no alcanzan en un emulador.

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.

Allure Report Suites mostrando Broken setUp con error Could not proxy command to the remote server socket hang up
Appium Server arrancaba antes de que el emulador existiera. Resultado: socket hang up en cada sesión.

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.

Allure Report mostrando CartTest broken con deviceScreenSize 320x640 resaltado en las capabilities del emulador
El emulador default tiene pantalla de 320x640. Los botones quedan fuera de pantalla y nunca son visibles.

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.

Allure Report overview mostrando 89.18 por ciento con 37 tests y barra de 1 rojo 3 amarillo 33 verde
Con profile: pixel_6 y KVM habilitado: 33 de 37 tests verdes en CI.
Allure Report Suites mostrando LoginTest con verificarTituloIncorrecto falloIntencional en rojo y screenshot on failure de la app Products
El fallo intencional funciona igual en CI. Screenshot automático capturado en el emulador.

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/

Allure Report overview 89.18 por ciento con 37 tests en 10 minutos 50 segundos mostrando 1 Product defects y 3 Test defects
Segunda corrida. Mismo resultado: 89.18%. Pipeline estable.

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.

Allure Report Suites mostrando PermisosTest con navegarAGeoLocation broken y screenshot My Demo App keeps stopping
La app crashea al acceder a GeoLocation. El emulador con target default no tiene Google Play Services.

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 cd muere 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_dispatch te 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 uiautomator2ServerInstallTimeout y uiautomator2ServerLaunchTimeout son 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.