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

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.
ProductDetailTest: parametrizar productos del catálogo
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".

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

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

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

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