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.
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.

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í:

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:

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
@Stepen 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