DataProvider en Appium: parametrización de tests con múltiples datos

@DataProvider en Appium: 3 tests parametrizados, 5 errores reales, hallazgos de la app. De 30 a 36 tests.

DataProviders credencialesValidas y credencialesInvalidas en LoginTest con username, password, campoError y mensaje esperado
Los dos DataProviders de LoginTest. credencialesInvalidas tiene 4 columnas: username, password, campo donde buscar el error y mensaje esperado.

CONTEXTO

Tengo 30 tests funcionando. Todos con datos hardcodeados: un solo usuario para login, un solo producto para verificar detalle, un solo set de credenciales en WebView. Funciona, pero es limitado.

El problema no es que los tests fallen — es que prueban poco. loginExitoso verifica un solo par de credenciales. verificarDetalleProducto abre un solo producto. loginExitosoEnWebView usa un solo usuario de saucedemo. Si mañana cambio esos datos, no sé si el test sigue pasando.

DataProvider de TestNG resuelve esto: ejecuta el mismo test con múltiples sets de datos. Un test, N ejecuciones. Sin duplicar código.

Elegí tres test classes para parametrizar:

  • LoginTest — el caso más claro. Validaciones de campos vacíos con errores distintos según qué campo falta.
  • ProductDetailTest — verificar que varios productos del catálogo muestran nombre y precio correcto.
  • WebViewTest — login en saucedemo.com con distintos usuarios: exitosos y fallidos.

Los otros tests (CartTest, CheckoutTest, GestosTest, PermisosTest, LifecycleTest) tienen lógica de flujo que no se beneficia de parametrización. No los toqué.


Qué es DataProvider

DataProvider es una anotación de TestNG que alimenta un test con datos. Un método anotado con @DataProvider devuelve un Object[][] — cada fila es una ejecución del test con esos parámetros.

@DataProvider(name = "ejemplo")
public Object[][] ejemplo() {
    return new Object[][] {
        {"dato1", "dato2"},
        {"dato3", "dato4"}
    };
}

@Test(dataProvider = "ejemplo")
public void miTest(String param1, String param2) {
    // Se ejecuta 2 veces: una con dato1/dato2, otra con dato3/dato4
}

TestNG los muestra por separado en los resultados: miTest[dato1, dato2] y miTest[dato3, dato4]. Si uno falla, los otros siguen corriendo. Cada fila es independiente.


LoginTest: primer refactor

Antes

Tenía 4 tests separados: loginExitoso, loginConCamposVacios, loginConClearYReingreso, scrollHastaProducto. Los dos últimos no son candidatos a parametrizar — tienen lógica específica. Los dos primeros sí.

loginConCamposVacios solo probaba un escenario: ambos campos vacíos. Faltaban: username vacío con password lleno, password vacío con username lleno, credenciales incorrectas.

DataProviders

@DataProvider(name = "credencialesValidas")
public Object[][] credencialesValidas() {
    return new Object[][] {
        {"[email protected]", "10203040"}
    };
}

@DataProvider(name = "credencialesInvalidas")
public Object[][] credencialesInvalidas() {
    return new Object[][] {
        {"", "",                "username", "Username is required"},
        {"[email protected]", "", "password", "Enter Password"},
        {"", "10203040",        "username", "Username is required"}
    };
}

Ese tercer parámetro "username" / "password" lo tuve que agregar después del primer error. No estaba en la versión inicial.

Hallazgo: la app acepta cualquier credencial

Tenía un cuarto caso en el DataProvider: {"[email protected]", "wrongpass", "username", "Provided credentials do not match any user in this service."}. Credenciales que no existen.

Lo probé manualmente. La app hizo login exitoso. My Demo App no valida credenciales contra ningún backend — acepta cualquier cosa con tal de que ambos campos tengan algo.

Eliminé ese caso del DataProvider. No es un error de mi test — es un comportamiento de la app. En un contexto profesional sería un bug o al menos algo a documentar en el test plan.

Resultado: 6 de 6 verdes

IntelliJ con LoginTest 6 de 6 tests pasados mostrando DataProviders parametrizados con credenciales y mensajes de error
6 de 6. Los parámetros aparecen en el nombre del test — cada escenario es identificable en el reporte.
LoginTest                          1 min 34 sec
  loginConClearYReingreso                         8 sec 100 ms
  loginConCredencialesInvalidas[, , username, Username is required]              2 sec 757 ms
  loginConCredencialesInvalidas[[email protected], , password, Enter Password] (1) 4 sec 336 ms
  loginConCredencialesInvalidas[, 10203040, username, Username is required] (2)  3 sec 197 ms
  loginExitoso[[email protected], 10203040]                                       5 sec 680 ms
  scrollHastaProducto                                                          13 sec 391 ms

Los parámetros aparecen en el nombre del test. Cada escenario es identificable en el reporte.


DataProvider

@DataProvider(name = "productos")
public Object[][] productos() {
    return new Object[][] {
        {"Sauce Labs Backpack",          "$ 29.99"},
        {"Sauce Labs Backpack (orange)", "$ 29.99"},
        {"Sauce Labs Backpack (yellow)", "$ 29.99"}
    };
}

@Test(dataProvider = "productos")
public void verificarDetalleProducto(String nombreProducto, String precioEsperado) {
    ProductDetailPage detailPage = productsPage.seleccionarProducto(nombreProducto);

    Assert.assertEquals(detailPage.obtenerNombreProducto(), nombreProducto);
    Assert.assertEquals(detailPage.obtenerPrecio(), precioEsperado);
    Assert.assertEquals(detailPage.obtenerCantidad(), "1");
}

Error: Bike Light y Onesie crashean la app

Primer intento usé tres productos variados: "Sauce Labs Backpack" ($29.99), "Sauce Labs Bike Light" ($9.99), "Sauce Labs Onesie" ($7.99). Distintos nombres, distintos precios — mejor cobertura.

Ambos fallaron con TimeoutException. Bike Light tardó 15 segundos, Onesie 24. No era un problema de scroll — seleccionarProducto ya tiene scrollTextIntoView. No era un problema de nombres — los verifiqué en Inspector, coinciden exacto.

Probé manualmente. Al tocar Bike Light o Onesie en la app, My Demo App crashea: "My Demo App continúa fallando".

Diálogo de error en Android mostrando My Demo App continúa fallando con opciones Información de apps y Cerrar app
Bug de la app. Bike Light y Onesie crashean al abrir detalle — manualmente también, no solo desde automation.

Bug de la app, no de mi código. Los productos existen en el catálogo pero la pantalla de detalle crashea al abrirlos. Lo mismo pasa manualmente, no solo desde automation.

Solución: usé las tres variantes de Backpack (normal, orange, yellow) que sí funcionan. Menos variedad de precios, pero demuestra el concepto de DataProvider sin depender de productos rotos.

Resultado: 6 de 6 verdes

IntelliJ con ProductDetailTest 6 de 6 tests pasados mostrando tres variantes de Backpack parametrizadas con DataProvider
6 de 6. Las tres variantes de Backpack con DataProvider. Los productos que crashean quedaron afuera.
ProductDetailTest                  1 min 19 sec
  agregarAlCarrito                                2 sec 742 ms
  modificarCantidad                               4 sec 217 ms
  seleccionarColor                                2 sec  76 ms
  verificarDetalleProducto[Sauce Labs Backpack, $ 29.99]                  1 sec 542 ms
  verificarDetalleProducto[Sauce Labs Backpack (orange), $ 29.99] (1)     1 sec 524 ms
  verificarDetalleProducto[Sauce Labs Backpack (yellow), $ 29.99] (2)     5 sec 981 ms

WebViewTest: parametrizar logins de saucedemo

DataProviders

Saucedemo.com lista sus usuarios en la pantalla de login: standard_user, locked_out_user, problem_user, performance_glitch_user, error_user, visual_user. Password para todos: secret_sauce.

Probé tres manualmente dentro del WebView de la app:

Usuario Password Resultado
locked_out_user secret_sauce Error: "Epic sadface: Sorry, this user has been locked out."
visual_user secret_sauce Login exitoso, entra al inventario
standard_user wrong_password Error: "Epic sadface: Username and password do not match any user in this service"

Con eso armé dos DataProviders:

@DataProvider(name = "loginWebExitoso")
public Object[][] loginWebExitoso() {
    return new Object[][] {
        {"standard_user", "secret_sauce"},
        {"visual_user",   "secret_sauce"}
    };
}

@DataProvider(name = "loginWebFallido")
public Object[][] loginWebFallido() {
    return new Object[][] {
        {"locked_out_user", "secret_sauce",  "Sorry, this user has been locked out"},
        {"standard_user",   "wrong_password", "Username and password do not match"}
    };
}

Los tests usan Assert.assertTrue(error.contains(...)) con fragmentos del mensaje — sin el "Epic sadface:" ni el punto final. Más robusto ante cambios menores de formato.

@Test(dataProvider = "loginWebExitoso")
public void loginExitosoEnWebView(String username, String password) {
    WebViewPage webViewPage = productsPage.irAWebView();
    webViewPage.cargarUrl(SAUCEDEMO_URL);
    webViewPage.cambiarAWebView();

    webViewPage.loginEnWeb(username, password);
    Assert.assertTrue(webViewPage.estaEnInventario());

    webViewPage.cambiarANativo();
}

@Test(dataProvider = "loginWebFallido")
public void loginFallidoEnWebView(String username, String password, String errorEsperado) {
    WebViewPage webViewPage = productsPage.irAWebView();
    webViewPage.cargarUrl(SAUCEDEMO_URL);
    webViewPage.cambiarAWebView();

    webViewPage.loginEnWeb(username, password);
    String error = webViewPage.obtenerErrorLoginWeb();
    Assert.assertTrue(error.contains(errorEsperado),
            "Esperaba: " + errorEsperado + " | Obtuvo: " + error);

    webViewPage.cambiarANativo();
}

Error: flaky test en cargarUrlYVerificarContextos

Con los DataProviders funcionando, un test que ya existía empezó a fallar: cargarUrlYVerificarContextos. AssertionError — Assert.assertTrue(contextos.contains("WEBVIEW_...")) dio false.

El test cargaba la URL y chequeaba los contextos inmediatamente. Antes pasaba porque la red estaba rápida ese día. Ahora no. Flaky test clásico por falta de wait.

La solución: esperar a que haya más de un contexto antes de verificar.

@Test
public void cargarUrlYVerificarContextos() {
    WebViewPage webViewPage = productsPage.irAWebView();
    webViewPage.cargarUrl(SAUCEDEMO_URL);

    // Esperar a que el contexto WEBVIEW esté disponible
    wait.until(d -> webViewPage.obtenerContextos().size() > 1);

    Set<String> contextos = webViewPage.obtenerContextos();
    Assert.assertTrue(contextos.contains("NATIVE_APP"));
    Assert.assertTrue(contextos.contains("WEBVIEW_com.saucelabs.mydemoapp.android"));
}

Mismo patrón que ya uso en cambiarAWebView(). Debí haberlo puesto desde el principio.

Resultado: 7 de 7 verdes

IntelliJ con WebViewTest 7 de 7 tests pasados mostrando DataProviders de login exitoso y fallido en saucedemo
7 de 7. Dos logins exitosos, dos fallidos, más los tres tests originales sin DataProvider.
WebViewTest                        2 min 4 sec
  cargarUrlYVerificarContextos                                                            7 sec 288 ms
  loginExitosoEnWebView[standard_user, secret_sauce]                                      9 sec 714 ms
  loginExitosoEnWebView[visual_user, secret_sauce] (1)                                   10 sec 230 ms
  loginFallidoEnWebView[locked_out_user, secret_sauce, Sorry, this user has been locked out]   9 sec 831 ms
  loginFallidoEnWebView[standard_user, wrong_password, Username and password do not match] (1) 9 sec 828 ms
  navegarAWebView                                                                         1 sec 997 ms
  switchAWebViewYVerificarTitulo                                                          8 sec  41 ms

36 tests verdes

IntelliJ con suite completa de 36 tests pasados en 11 minutos mostrando DataProviders en LoginTest ProductDetailTest y WebViewTest
36 de 36. De 30 a 36 con DataProviders, cero fallos, nada roto.
Tests passed: 36 of 36 tests – 11 min 5 sec

CartTest                    1 min 11 sec
  agregarDosProductosDistintos                   13 sec 545 ms
  eliminarProductoDelCarrito                      9 sec  16 ms
  verificarCantidadMultipleEnCarrito              4 sec 168 ms
  verificarProductoEnCarrito                      3 sec 964 ms

LoginTest                   1 min 34 sec
  loginConClearYReingreso                         8 sec 100 ms
  loginConCredencialesInvalidas[...]               2-4 sec (x3)
  loginExitoso[[email protected], 10203040]         5 sec 680 ms
  scrollHastaProducto                            13 sec 391 ms

GestosTest                  55 sec 723 ms
  dibujarLimpiarYGuardar
  dibujarYGuardar
  navegarADrawing

WebViewTest                 2 min 4 sec
  cargarUrlYVerificarContextos                    7 sec 288 ms
  loginExitosoEnWebView (x2)                     ~10 sec c/u
  loginFallidoEnWebView (x2)                     ~10 sec c/u
  navegarAWebView                                 1 sec 997 ms
  switchAWebViewYVerificarTitulo                  8 sec  41 ms

CheckoutTest                2 min 46 sec
  checkoutCompleto                               30 sec
  continuarComprandoVuelveAProducts              33 sec
  verificarCheckoutComplete                      31 sec
  verificarDatosEnReview                         34 sec

PermisosTest                52 sec 152 ms
  navegarAGeoLocation
  navegarAQRScanner
  startStopObserving
  verificarCoordenadasGeoLocation

LifecycleTest               (2 tests)
  appEnBackground
  terminateYReactivar

ProductDetailTest           1 min 19 sec
  agregarAlCarrito
  modificarCantidad
  seleccionarColor
  verificarDetalleProducto (x3 con DataProvider)

De 30 a 36 tests. Los 6 nuevos vienen de DataProviders (3 en LoginTest, 2 en ProductDetailTest, 1 en WebViewTest — el visual_user que se sumó). Más el fix del flaky test.


Nota sobre DataProviders con datos externos

En mi serie de Selenium implementé DataProviders leyendo datos desde Excel con Apache POI. La mecánica es la misma: el DataProvider lee el archivo y devuelve Object[][]. Acá lo pueden ver. En esta serie mantengo los datos inline porque el foco es mobile, no repetir lo que ya cubrí.


DataProviders dentro del test vs clase separada

Los tres DataProviders están dentro de su respectivo test class. Se pueden externalizar usando dataProviderClass:

// En una clase separada
public class TestData {
    @DataProvider(name = "credencialesValidas")
    public static Object[][] credencialesValidas() {
        return new Object[][] {
            {"[email protected]", "10203040"}
        };
    }
}

// En el test
@Test(dataProvider = "credencialesValidas", dataProviderClass = TestData.class)
public void loginExitoso(String username, String password) { ... }

Cuándo conviene separar: cuando varios test classes comparten los mismos datos, cuando los DataProviders crecen mucho (20+ filas), o cuando los datos vienen de una fuente externa.

Con el volumen que tengo ahora (3 DataProviders chicos, datos simples), no vale la pena. Leés el test de arriba a abajo sin saltar a otra clase.


Estructura actual

appium-java-framework/
├── chromedriver-win64/
│   └── chromedriver.exe
├── appium-java-framework/
│   ├── src/
│   │   ├── main/java/
│   │   │   └── pages/
│   │   │       ├── BasePage.java
│   │   │       ├── CartPage.java
│   │   │       ├── CheckoutCompletePage.java
│   │   │       ├── CheckoutPaymentPage.java
│   │   │       ├── CheckoutShippingPage.java
│   │   │       ├── DrawingPage.java
│   │   │       ├── GeoLocationPage.java
│   │   │       ├── LoginPage.java             (+ obtenerErrorPassword)
│   │   │       ├── ProductDetailPage.java
│   │   │       ├── ProductsPage.java
│   │   │       ├── QRScannerPage.java
│   │   │       ├── ReviewOrderPage.java
│   │   │       └── WebViewPage.java
│   │   └── test/java/
│   │       └── tests/
│   │           ├── BaseTest.java
│   │           ├── CartTest.java
│   │           ├── CheckoutTest.java
│   │           ├── GestosTest.java
│   │           ├── LifecycleTest.java
│   │           ├── LoginTest.java             ← DataProvider (credencialesValidas, credencialesInvalidas)
│   │           ├── PermisosTest.java
│   │           ├── PrimerTest.java
│   │           ├── ProductDetailTest.java     ← DataProvider (productos)
│   │           └── WebViewTest.java           ← DataProvider (loginWebExitoso, loginWebFallido)
│   ├── apk/
│   │   └── mda-2.2.0-25.apk
│   └── pom.xml

13 pages, 10 test classes, 36 tests.


Estado actual

  • 13 pages, 36 tests (de 30 a 36 con DataProviders)
  • Nuevo: 5 DataProviders en 3 test classes (credencialesValidas, credencialesInvalidas, productos, loginWebExitoso, loginWebFallido)
  • Fix: flaky test en cargarUrlYVerificarContextos por falta de wait
  • Hallazgos: My Demo App no valida credenciales, error de password es un placeholder, Bike Light y Onesie crashean al abrir detalle
  • Los DataProviders están inline en cada test class — se pueden externalizar si crecen

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