Allure Reports en Appium: reportes visuales con @Step y screenshots automáticos

@Step en 13 pages, screenshot automático en fallo, 3 errores reales durante la integración. De 36 a 37 tests con reporte profesional.

Allure Report overview mostrando 37 test cases con 97.29 por ciento de éxito y barra verde con 1 fallo rojo
37 tests, 97.29%. El fallo es intencional — para demostrar la captura automática de screenshots.

CONTEXTO

Tengo 37 tests funcionando. DataProviders en 3 test classes, 13 pages con Page Object Model, un framework que corre completo en ~11 minutos. Pero cuando un test falla, lo único que veo es un stack trace en la consola. No hay evidencia visual, no hay pasos detallados, no hay forma rápida de entender qué pasó.

En mi serie de Selenium integré Allure con @Step, screenshots on failure y Allure CLI para generar reportes HTML. Acá hago lo mismo para Appium. La mecánica es casi idéntica — mismo stack Java + TestNG — pero con un par de diferencias que me complicaron.


Qué es Allure Reports

Allure genera reportes HTML interactivos a partir de los resultados de tests. Muestra qué tests pasaron, cuáles fallaron, cuánto tardó cada uno, y — lo más útil — los pasos internos de cada test si usás @Step.

Sin Allure: "test falló, acá tenés un stack trace". Con Allure: "test falló en el paso 4 de 6, acá tenés un screenshot de la app en ese momento".

Para QA Automation, un reporte visual es la diferencia entre "corrí tests" y "acá está la evidencia".


Dependencias

Tres cosas en el pom.xml:

<properties>
    <allure.version>2.29.0</allure.version>
    <aspectj.version>1.9.22.1</aspectj.version>
</properties>
<!-- Allure TestNG (para integración con TestNG) -->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-testng</artifactId>
    <version>${allure.version}</version>
    <scope>test</scope>
</dependency>

<!-- AspectJ Weaver (para que @Step funcione) -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
    <scope>test</scope>
</dependency>

<!-- Allure Java Commons (para @Step en src/main/java) -->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-java-commons</artifactId>
    <version>${allure.version}</version>
</dependency>

Y el plugin de Surefire con el agente de AspectJ:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.5.2</version>
            <configuration>
                <argLine>
                    -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                </argLine>
            </configuration>
        </plugin>
    </plugins>
</build>

Error: @Step no resuelve en las pages

Primer error después de agregar las dependencias. allure-testng tiene <scope>test</scope>, así que solo está disponible en src/test/java. Mis pages están en src/main/java — no pueden ver esa dependencia.

IntelliJ mostrando error Cannot resolve symbol Step en LoginPage línea 22 con popup de error
@Step en rojo. allure-testng tiene scope test, las pages están en src/main/java. No se ven.

La solución: agregar allure-java-commons sin scope test. Es la dependencia que contiene @Step y queda disponible para todo el proyecto.

<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-java-commons</artifactId>
    <version>${allure.version}</version>
</dependency>

Reload de Maven. Rojos desaparecen.


@Step en las pages

@Step documenta cada acción como un paso visible en el reporte. Sin esto, Allure solo muestra "test pasó" o "test falló". Con @Step, muestra exactamente qué hizo el test paso a paso.

Los @Step van en las pages, no en los tests. La page ejecuta la acción de negocio ("ingresar credenciales", "agregar al carrito"), el test orquesta. Lo que querés ver en un reporte son acciones de negocio, no llamadas a métodos.

Ejemplo: LoginPage

@Step("Ingresar username: {username}")
public LoginPage ingresarUsername(String username) {
    driver.findElement(AppiumBy.id(USERNAME_FIELD)).sendKeys(username);
    return this;
}

@Step("Ingresar credenciales: {username} / {password}")
public LoginPage ingresarCredenciales(String username, String password) {
    ingresarUsername(username);
    ingresarPassword(password);
    return this;
}

Los parámetros entre {} se resuelven automáticamente. Allure muestra el valor real: "Ingresar username: [email protected]", no "Ingresar username: {username}".

ingresarCredenciales tiene @Step y llama a ingresarUsername e ingresarPassword que también tienen @Step. En el reporte se ven anidados — el paso principal con los sub-pasos adentro.

Las 13 pages con @Step

Agregué @Step a todos los métodos públicos de las 13 pages:

Page Métodos con @Step
BasePage swipe, manejarPermiso
LoginPage ingresarUsername, ingresarPassword, limpiarUsername, ingresarCredenciales, tapLogin, tapLoginEsperandoError, obtenerErrorUsername, obtenerErrorPassword
ProductsPage obtenerTitulo, irAlLogin, scrollHastaProducto, seleccionarProducto, abrirMenu, irADrawing, irAQRScanner, irAQRScannerDenegando, irAGeoLocation, irAWebView
ProductDetailPage obtenerNombreProducto, obtenerPrecio, obtenerCantidad, aumentarCantidad, seleccionarColor, agregarAlCarrito, irAlCarrito, volverAProducts
CartPage obtenerTitulo, obtenerNombreProducto, obtenerPrecioProducto, obtenerCantidad, obtenerTotalItems, obtenerTotalPrecio, eliminarProducto, proceedToCheckout
CheckoutShippingPage obtenerTitulo, completarFormulario, irAPayment (+ campos individuales)
CheckoutPaymentPage obtenerSubtitulo, completarFormulario, irAReviewOrder (+ campos individuales)
ReviewOrderPage obtenerNombreProducto, obtenerPrecioProducto, obtenerNombreDireccion, realizarPedido, scrollHastaTotal (+ campos de envío y pago)
CheckoutCompletePage obtenerTitulo, obtenerThankYou, continuarComprando
DrawingPage dibujar, limpiarCanvas, tapSave, guardarYObtenerMensaje
GeoLocationPage obtenerTitulo, obtenerLatitud, obtenerLongitud, tapStartObserving, tapStopObserving
QRScannerPage obtenerTitulo
WebViewPage cargarUrl, cambiarAWebView, cambiarANativo, loginEnWeb, estaEnInventario

El patrón es siempre el mismo: @Step("Descripción legible") antes del método. Los que reciben parámetros usan {param} para mostrar el valor.


Screenshot automático en fallo

Quería que cuando un test falle, Allure adjunte un screenshot de la app automáticamente. Sin tener que agregar código en cada test.

Primer intento: listener con @Attachment

Creé un AllureListener que implementa ITestListener con un método onTestFailure que toma el screenshot:

@Attachment(value = "Screenshot on failure", type = "image/png")
private byte[] saveScreenshot(AndroidDriver driver) {
    return driver.getScreenshotAs(OutputType.BYTES);
}

No funcionó. El screenshot se tomaba (confirmé con prints en consola: 459040 bytes), pero no aparecía en el reporte. El problema: @Attachment en un método private dentro de un listener no se intercepta bien con AspectJ. El attachment se generaba pero no se asociaba al test.

Segundo intento: Allure.addAttachment

Cambié a la API directa:

Allure.addAttachment("Screenshot on failure", "image/png",
        new ByteArrayInputStream(screenshot), ".png");

Mismo resultado. El screenshot se generaba, la consola lo confirmaba, pero Allure no lo mostraba en el reporte. El contexto del test se pierde dentro del listener.

Lo que funcionó: screenshot en tearDown

Moví la lógica al @AfterMethod de BaseTest, que recibe ITestResult:

@AfterMethod
public void tearDown(ITestResult result) {
    if (result.getStatus() == ITestResult.FAILURE && driver != null) {
        Allure.getLifecycle().addAttachment(
                "Screenshot on failure", "image/png", ".png",
                driver.getScreenshotAs(OutputType.BYTES)
        );
    }
    if (driver != null) {
        driver.quit();
    }
}

La diferencia: @AfterMethod corre dentro del lifecycle de Allure del test. El screenshot se asocia correctamente al test case que falló. Y se toma antes de cerrar el driver — si fuera al revés, no habría browser para capturar.

El AllureListener quedó vacío. Lo dejé en el proyecto por si en el futuro necesito agregar algo en onTestStart o onTestSkipped, pero los screenshots se manejan en BaseTest.


Test de fallo intencional

Para demostrar que el screenshot funciona, agregué un test que falla a propósito:

@Test
public void verificarTituloIncorrecto_falloIntencional() {
    LoginPage loginPage = productsPage.irAlLogin();
    loginPage.ingresarCredenciales("[email protected]", "10203040");
    loginPage.tapLogin();

    // Assertion que falla a propósito
    Assert.assertEquals(productsPage.obtenerTitulo(), "Texto Incorrecto",
            "Fallo intencional para probar screenshot en Allure");
}

En Allure se ve así:

Allure Suites mostrando test verificarTituloIncorrecto falloIntencional con pasos Step verdes y screenshot de My Demo App en Tear down
Los pasos verdes, la assertion en amarillo, el screenshot en Tear down. Todo el contexto del fallo en una pantalla.

Los @Step muestran el recorrido: "Navegar al Login desde menú" → "Ingresar credenciales" (con sub-steps) → "Tap en Login" → "Obtener título de Products" → assertion fallida. En Tear down, el screenshot de la app mostrando la pantalla de Products.

El screenshot aparece en Tear down porque ahí es donde el @AfterMethod lo genera. Lo importante es que está asociado al test correcto y muestra el estado exacto de la app en el momento del fallo.


Test verde con @Step

Así se ve un test que pasa, con los pasos detallados:

Allure mostrando test eliminarProductoDelCarrito con pasos Step verdes incluyendo Seleccionar producto y Agregar al carrito
Cada acción visible. Sin @Step esto sería solo "test pasó" sin ningún detalle.

Set up → pasos de negocio en Test body → Tear down. Cada paso con su duración. Si algo falla, sabés exactamente en qué paso.


Error: allure-results acumula entre corridas

mvn clean borra target/ pero no borra allure-results/. La carpeta está en la raíz del proyecto, no dentro de target. Si corrés los tests varias veces sin borrarla, el reporte muestra tests duplicados de corridas anteriores.

La solución: borrar antes de cada corrida limpia.

Remove-Item -Recurse -Force allure-results
mvn clean test
allure serve allure-results

O agregarle al flujo de trabajo: siempre Remove-Item antes de mvn clean test.


Error: VM crash por RAM

Con 8GB de RAM, el AspectJ agent a veces causa que la VM no pueda arrancar:

OpenJDK 64-Bit Server VM warning: INFO: os::commit_memory failed;
error='El archivo de paginación es demasiado pequeño para completar la operación'

Maven forkea una JVM nueva para correr los tests. El agente de AspectJ consume memoria adicional. Con Appium Server, el celular conectado, IntelliJ abierto y un browser — la máquina no da abasto.

La solución: cerrar todo lo que no sea esencial antes de correr mvn clean test. O correr los tests desde IntelliJ (click derecho → Run), que no forkea una VM nueva.

No es un error de Allure ni del framework. Es una limitación de hardware que vale la pena documentar para quienes trabajen con setups similares.


Cómo generar el reporte

Dos comandos:

mvn clean test
allure serve allure-results

mvn clean test corre los tests y genera archivos JSON en allure-results/. allure serve toma esos archivos, genera el HTML y lo abre en el browser automáticamente.

Si no tenés Allure CLI instalado: Allure CLI installation.


Nota sobre mi serie de Selenium

En mi serie de Selenium hice la misma integración: Allure con @Step, screenshots on failure, Allure CLI. Acá lo pueden ver. La mecánica es casi idéntica porque el stack es el mismo (Java + TestNG). La diferencia principal fue que en Selenium usé un listener con @Attachment para los screenshots y funcionó. En Appium tuve que mover la lógica al tearDown — probablemente porque el driver de Appium maneja el contexto de forma distinta.


Estructura actual

appium-java-framework/
├── chromedriver-win64/
│   └── chromedriver.exe
├── appium-java-framework/
│   ├── src/
│   │   ├── main/java/
│   │   │   └── pages/
│   │   │       ├── BasePage.java              ← @Step (swipe, manejarPermiso)
│   │   │       ├── CartPage.java              ← @Step
│   │   │       ├── CheckoutCompletePage.java   ← @Step
│   │   │       ├── CheckoutPaymentPage.java    ← @Step
│   │   │       ├── CheckoutShippingPage.java   ← @Step
│   │   │       ├── DrawingPage.java           ← @Step
│   │   │       ├── GeoLocationPage.java       ← @Step
│   │   │       ├── LoginPage.java             ← @Step
│   │   │       ├── ProductDetailPage.java     ← @Step
│   │   │       ├── ProductsPage.java          ← @Step
│   │   │       ├── QRScannerPage.java         ← @Step
│   │   │       ├── ReviewOrderPage.java       ← @Step
│   │   │       └── WebViewPage.java           ← @Step
│   │   └── test/java/
│   │       └── tests/
│   │           ├── AllureListener.java
│   │           ├── BaseTest.java              ← screenshot on failure en tearDown
│   │           ├── CartTest.java
│   │           ├── CheckoutTest.java
│   │           ├── GestosTest.java
│   │           ├── LifecycleTest.java
│   │           ├── LoginTest.java             ← + verificarTituloIncorrecto_falloIntencional
│   │           ├── PermisosTest.java
│   │           ├── PrimerTest.java
│   │           ├── ProductDetailTest.java
│   │           └── WebViewTest.java
│   ├── apk/
│   │   └── mda-2.2.0-25.apk
│   └── pom.xml                                ← + allure-testng, aspectjweaver, allure-java-commons, surefire plugin

13 pages con @Step, 10 test classes, 37 tests.


Estado actual

  • 13 pages con @Step en todos los métodos públicos
  • Screenshot automático en fallo (en BaseTest tearDown)
  • Allure Report generándose con allure serve allure-results
  • 37 tests: 36 verdes + 1 fallo intencional con screenshot
  • 3 errores reales resueltos: scope de dependencia, screenshot en listener vs tearDown, acumulación de allure-results

Repo: github.com/cesarbeassuarez/appium-java-framework