DataProviders y assertions reales con TestNG en Selenium
Reemplacé validaciones manuales por Assert de TestNG y creé DataProviders en clase aparte. 5 tests, datos separados de lógica.
Nota
Este post documenta dos cambios en el framework: DataProviders para no hardcodear datos de prueba, y Assert de TestNG para que los tests realmente fallen cuando algo está mal.
Contexto: qué tenía y qué necesitaba cambiar
En Framework TestNG migré a TestNG. Los tests corren con @Test, el setup está en BaseTest con @BeforeMethod/@AfterMethod, y testng.xml controla la suite.
Pero quedaron dos problemas pendientes:
1. check.java miente. Mi clase check.java valida con prints. Si algo falla, imprime ❌ en consola. Pero TestNG no se entera. El test sigue marcado como PASSED. Eso significa que puedo tener tests que "pasan" cuando en realidad fallaron.
2. Los datos de prueba están hardcodeados en los tests. Cada test negativo tiene usuario, contraseña y mensaje de error escritos directamente en el código. Si mañana cambio un mensaje de error, tengo que buscarlo dentro del test y modificarlo ahí. Eso no escala.
Esta sesión resuelve ambos.
El problema con check.java
Así funcionaba check.java:
public class check {
public static void equals(String actual, String expected, String context) {
if (!actual.equals(expected)) {
System.out.println("❌ fallo. " + context +
"\n Expected: [" + expected + "]" +
"\n Actual: [" + actual + "]");
} else {
System.out.println("✅ " + context + " -> OK");
}
}
public static void visible(boolean isVisible, String context) {
if (!isVisible) {
System.out.println("❌ fallo. " + context + " -> NO visible");
} else {
System.out.println("✅ " + context + " -> visible");
}
}
public static void isFalse(boolean condition, String context) {
if (condition) {
System.out.println("❌ fallo. " + context + " (se esperaba FALSE y fue TRUE)");
} else {
System.out.println("✅ " + context + " -> OK");
}
}
}
Yo lo armé cuando todavía no usaba TestNG. Me servía para ver en consola qué pasaba. Emojis, mensajes claros, contexto. Funcionaba como feedback visual.
El problema: es solo cosmético. Un System.out.println no detiene nada. Si check.equals detecta que actual ≠ expected, imprime ❌ y sigue. TestNG nunca se entera de que algo falló. El resultado final dice PASSED.
Pensalo así: es como un inspector que ve una grieta en un edificio, anota "hay grieta" en su cuaderno, pero le dice al dueño que todo está bien.
La solución: Assert de TestNG
Assert.assertEquals(actual, expected, "Mensaje de error");
Si actual no es igual a expected, el test se detiene y se marca como FAILED. TestNG lo sabe, lo reporta, lo cuenta. Herramientas como Allure, Jenkins, CI/CD reaccionan a eso: "3 de 50 fallaron, no deployes".
Los reemplazos fueron directos:
| check.java (antes) | TestNG Assert (después) |
|---|---|
check.equals(actual, expected, msg) |
Assert.assertEquals(actual, expected, msg) |
check.visible(bool, msg) |
Assert.assertTrue(bool, msg) |
check.isFalse(bool, msg) |
Assert.assertFalse(bool, msg) |
¿Y los emojis, los mensajes claros en consola? Eso lo voy a recuperar con Allure Reports. Allure da un dashboard visual con pasos, capturas, tiempos, estados — mucho más potente que prints. Lo que perdí en cosmética lo gano en confiabilidad.
DataProviders: separar datos del test
Mis tres tests negativos hacían lo mismo:
- Ir a login
- Tipear credenciales inválidas
- Verificar mensaje de error
- Verificar que el dashboard no aparece
La única diferencia eran los datos: usuario, contraseña, mensaje esperado. Tres métodos que hacen lo mismo con datos distintos es código repetido.
Antes: 3 tests separados con datos adentro
@Test
public void loginInvalido_passwordIncorrecta_muestraError() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo("admin", "mal");
DashboardPage dashboard = new DashboardPage();
check.equals(loginPage.obtenerMensajeError(),
"Error de validación: ¡Nombre de usuario o contraseña inválidos!", "Mensaje de error");
check.isFalse(dashboard.estaVisibleSafe(), "El dashboard NO debería aparecer");
}
@Test
public void loginInvalido_usuarioIncorrecto_muestraError() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo("mal", "serenity");
DashboardPage dashboard = new DashboardPage();
check.equals(loginPage.obtenerMensajeError(),
"Error de validación: ¡Nombre de usuario o contraseña inválidos!", "Mensaje de error");
check.isFalse(dashboard.estaVisibleSafe(), "El dashboard NO debería aparecer");
}
@Test
public void loginInvalido_tipearEspacios() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo(" ", " ");
DashboardPage dashboard = new DashboardPage();
check.equals(loginPage.obtenerMensajeError(),
"Por favor, valide los campos vacíos o inválidos (marcados en rojo) antes de enviar el formulario",
"Mensaje de error");
check.isFalse(dashboard.estaVisibleSafe(), "El dashboard NO debería aparecer");
}
Tres métodos. La misma lógica repetida tres veces. Solo cambian los strings.
Después: 1 test + 1 DataProvider
@Test(dataProvider = "credencialesInvalidas", dataProviderClass = TestData.class)
public void loginInvalido_muestraError(String usuario, String password, String mensajeEsperado) {
LoginPage loginPage = new LoginPage();
loginPage.loginComo(usuario, password);
DashboardPage dashboard = new DashboardPage();
Assert.assertEquals(loginPage.obtenerMensajeError(), mensajeEsperado, "Mensaje de error");
Assert.assertFalse(dashboard.estaVisibleSafe(), "El dashboard NO debería aparecer");
}

Un método. TestNG lo ejecuta 3 veces, una por cada fila del DataProvider. Cada fila es una ejecución independiente: si la fila 1 falla, Assert detiene esa ejecución, pero las otras 2 arrancan desde cero y siguen corriendo.
¿Qué es un DataProvider?
Es un método que devuelve datos en forma de array bidimensional (Object[][]). Cada fila es un set de parámetros para una ejecución del test.
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return new Object[][] {
{"admin", "mal", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{"mal", "serenity", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{" ", " ", "Por favor, valide los campos vacíos o inválidos (marcados en rojo) antes de enviar el formulario"}
};
}
Tres filas = tres ejecuciones. Cada fila tiene tres valores que TestNG inyecta como parámetros del método de test (usuario, password, mensajeEsperado).
Si mañana necesito agregar un caso nuevo (por ejemplo, campos vacíos sin espacios), agrego una fila más al DataProvider. No toco el test.
¿Dónde vive el DataProvider?
Decidí crear una clase aparte: TestData.java. No dentro del test.
¿Por qué? Porque prefiero tener los datos separados de la lógica desde el principio. Hoy tengo 3 filas de login. Mañana voy a tener datos de clientes, pedidos, productos. Todo liviano en un solo lugar.
TestData.java (archivo nuevo)
Ubicación: src/test/java/com/cesar/qa/data/TestData.java
package com.cesar.qa.data;
import org.testng.annotations.DataProvider;
public class TestData {
// ===== LOGIN =====
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return new Object[][] {
{"admin", "mal", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{"mal", "serenity", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{" ", " ", "Por favor, valide los campos vacíos o inválidos (marcados en rojo) antes de enviar el formulario"}
};
}
// ===== CLIENTES =====
// (futuro)
// ===== PEDIDOS =====
// (futuro)
}

Paquete com.cesar.qa.data porque no es test ni page — es datos. Cuando implemente lectura desde Excel, ExcelReader.java va a vivir en este mismo paquete.
Para conectar el DataProvider externo al test, uso dos atributos en @Test:
@Test(dataProvider = "credencialesInvalidas", dataProviderClass = TestData.class)
dataProvider → nombre del método marcado con @DataProvider dataProviderClass → clase donde vive ese DataProvider
El método en TestData es static porque TestNG lo invoca sin instanciar la clase.
¿DataProvider reemplaza a Excel?
No. DataProvider es el mecanismo de TestNG para inyectar datos. La fuente puede ser cualquier cosa:
- Array en código (lo que hice hoy)
- Excel con Apache POI
- CSV
- JSON
- Base de datos
Hoy los datos están hardcodeados en el array. Cuando implemente Excel, el DataProvider va a seguir existiendo, pero en vez de tener strings escritos a mano, va a llamar a un método que lee el archivo:
// Hoy (sesión 9)
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return new Object[][] {
{"admin", "mal", "Error de validación..."},
};
}
// Futuro (con Excel)
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return ExcelReader.leer("login-data.xlsx", "negativos");
}
El DataProvider no se mueve. Lo que cambia es de dónde saca los datos.
Excel es la fuente más usada en empresas argentinas y enterprise porque los equipos de QA ya trabajan con planillas. Para el lab, empecé con arrays porque son más simples y no agregan dependencias. Excel viene en una sesión posterior, junto con la validación de la grilla de clientes.
Los cambios en código
Archivos modificados: 2
LoginPositiveTests.java — solo cambió check por Assert:
package com.cesar.qa.tests.login;
import com.cesar.qa.base.BaseTest;
import com.cesar.qa.pages.DashboardPage;
import com.cesar.qa.pages.LoginPage;
import org.testng.Assert;
import org.testng.annotations.Test;
public class LoginPositiveTests extends BaseTest {
@Test
public void loginValido_deberiaIngresar() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo("admin", "serenity");
DashboardPage dashboard = new DashboardPage();
Assert.assertTrue(dashboard.estaVisible(), "Dashboard visible luego de login válido");
Assert.assertEquals(dashboard.obtenerTitulo(), "Tablero", "Título del dashboard");
}
@Test
public void clickLoginSinTipear() {
LoginPage loginPage = new LoginPage();
loginPage.clickLogin();
DashboardPage dashboard = new DashboardPage();
Assert.assertTrue(dashboard.estaVisible(), "Dashboard visible luego de login válido");
Assert.assertEquals(dashboard.obtenerTitulo(), "Tablero", "Título del dashboard");
}
}
LoginNegativeTests.java — de 3 tests a 1 con DataProvider, check por Assert:
package com.cesar.qa.tests.login;
import com.cesar.qa.base.BaseTest;
import com.cesar.qa.data.TestData;
import com.cesar.qa.pages.DashboardPage;
import com.cesar.qa.pages.LoginPage;
import org.testng.Assert;
import org.testng.annotations.Test;
public class LoginNegativeTests extends BaseTest {
@Test(dataProvider = "credencialesInvalidas", dataProviderClass = TestData.class)
public void loginInvalido_muestraError(String usuario, String password, String mensajeEsperado) {
LoginPage loginPage = new LoginPage();
loginPage.loginComo(usuario, password);
DashboardPage dashboard = new DashboardPage();
Assert.assertEquals(loginPage.obtenerMensajeError(), mensajeEsperado, "Mensaje de error");
Assert.assertFalse(dashboard.estaVisibleSafe(), "El dashboard NO debería aparecer");
}
}
Archivo nuevo: 1
TestData.java — clase centralizada para datos de prueba livianos:
package com.cesar.qa.data;
import org.testng.annotations.DataProvider;
public class TestData {
// ===== LOGIN =====
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return new Object[][] {
{"admin", "mal", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{"mal", "serenity", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{" ", " ", "Por favor, valide los campos vacíos o inválidos (marcados en rojo) antes de enviar el formulario"}
};
}
// ===== CLIENTES =====
// (futuro)
// ===== PEDIDOS =====
// (futuro)
}
Lo que no toqué
- BaseTest.java → igual
- BasePage.java → igual
- LoginPage.java → igual
- DashboardPage.java → igual
- check.java → sigue en el proyecto, pero ya no se usa en ningún test
- testng.xml → igual (TestNG detecta el DataProvider automáticamente)
Ejecución


5 tests. Todos verdes.
En el panel de IntelliJ se ve:
LoginPositiveTests: 2 tests (loginValido_deberiaIngresar,clickLoginSinTipear)LoginNegativeTests: 3 ejecuciones generadas por el DataProvider (loginInvalido_muestraErrorcon cada set de datos)
Consola:
===============================================
QA Automation Lab
Total tests run: 5, Passes: 5, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0
Los warnings de CDP siguen apareciendo (Chrome DevTools Protocol). No son errores. No afectan la ejecución. Los expliqué en la sesión 1.
Referencia: anotaciones de TestNG
TestNG tiene varias anotaciones. Estas son las que importan según la etapa del framework:
Las que ya uso
@Test → marca un método como test. Si uno falla, los demás siguen corriendo.
@BeforeMethod / @AfterMethod → se ejecuta antes y después de CADA test. Mi setUp (abrir browser, navegar a URL) y tearDown (cerrar browser) viven acá en BaseTest.
@DataProvider → inyecta datos al test. Lo que implementé en esta sesión.
Las que voy a necesitar pronto
@BeforeClass / @AfterClass → se ejecuta UNA vez antes/después de todos los tests de una clase. Útil para preparar datos que no necesitan repetirse en cada test.
@BeforeSuite / @AfterSuite → se ejecuta UNA vez antes/después de toda la suite. Para configuración global.
@Listeners → hooks para eventos de la ejecución. Necesario para integrar Allure Reports.
Las que existen pero no necesito ahora
@BeforeTest / @AfterTest → nivel intermedio entre class y suite.
@BeforeGroups / @AfterGroups → para cuando use groups de tests.
@Parameters → inyectar valores desde testng.xml.
La jerarquía es: Suite → Test → Class → Method. Cada nivel tiene su Before/After. El más usado para automation es Method (el que ya tengo implementado).
Referencia: Assert de TestNG — los que importan
De todos los métodos de Assert que existen, estos son el 90% de lo que uso y voy a usar:
| Assert | Qué hace | Cuándo lo uso |
|---|---|---|
assertEquals(actual, expected, msg) |
Compara dos valores | Verificar que un texto sea exactamente el esperado |
assertTrue(condición, msg) |
Verifica que sea true | Verificar que un elemento es visible |
assertFalse(condición, msg) |
Verifica que sea false | Verificar que un elemento NO aparece |
assertNotNull(objeto, msg) |
Verifica que no sea null | Verificar que un elemento existe |
assertNull(objeto, msg) |
Verifica que sea null | Verificar que un mensaje de error no aparece |
assertNotEquals(actual, expected, msg) |
Verifica que sean distintos | Verificar que la URL cambió después de login |
Con assertEquals, assertTrue y assertFalse cubro el 80% de los casos. Son los tres que implementé hoy.
Uno que va a servir para la grilla de clientes: Assert.assertEquals(listaActual, listaEsperada). TestNG puede comparar listas enteras. Si tengo una columna de la grilla como List<String>, la comparo contra los valores esperados en una línea.
El string msg al final de cada Assert es el mensaje que aparece si falla. Es lo que va a verse en Allure cuando lo implemente. Vale la pena escribir mensajes descriptivos desde ahora.
Estructura actual del proyecto
selenium-java/
├── src/
│ ├── main/java/com/cesar/qa/
│ │ ├── base/
│ │ │ └── BasePage.java
│ │ ├── config/
│ │ │ ├── ConfigReader.java
│ │ │ └── DriverManager.java
│ │ ├── pages/
│ │ │ ├── DashboardPage.java
│ │ │ └── LoginPage.java
│ │ └── utils/
│ │ └── check.java ← ya no se usa
│ └── test/
│ ├── java/com/cesar/qa/
│ │ ├── base/
│ │ │ └── BaseTest.java
│ │ ├── data/ ← NUEVO paquete
│ │ │ └── TestData.java ← NUEVO archivo
│ │ └── tests/login/
│ │ ├── LoginPositiveTests.java ← modificado
│ │ └── LoginNegativeTests.java ← modificado
│ └── resources/
│ ├── config.properties
│ ├── logback.xml
│ └── testng.xml
Lo nuevo: paquete com.cesar.qa.data con TestData.java.
Estado actual
Tengo:
- Assertions reales que marcan PASSED/FAILED de verdad
- DataProvider separando datos de lógica
- TestData.java como clase centralizada para datos livianos
- 5 tests corriendo (2 positivos + 3 ejecuciones del DataProvider)
- check.java todavía en el proyecto pero sin uso
Próxima sesión: validar grillas como en un ERP de verdad (filas, columnas, asserts y datos externos).
Update: caseName — ponerle nombre a cada caso del DataProvider
Después de publicar miré el reporte HTML de TestNG. Los tests negativos se veían así:
loginInvalido_muestraError[admin, mal, Error de validación: ¡Nombre de usuario o contraseña inválidos!]
loginInvalido_muestraError[mal, serenity, Error de validación: ¡Nombre de usuario o contraseña inválidos!] (1)
loginInvalido_muestraError[ , , Por favor, valide los campos vacíos o inválidos...] (2)Funciona pero es difícil de leer. Con 3 casos se entiende. Con 20 sería un desastre.
La solución: agregar un nombre descriptivo como primer parámetro de cada fila.
Cambio en TestData.java
@DataProvider(name = "credencialesInvalidas")
public static Object[][] credencialesInvalidas() {
return new Object[][] {
{"Password incorrecta", "admin", "mal", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{"Usuario incorrecto", "mal", "serenity", "Error de validación: ¡Nombre de usuario o contraseña inválidos!"},
{"Espacios en blanco", " ", " ", "Por favor, valide los campos vacíos o inválidos (marcados en rojo) antes de enviar el formulario"}
};
}
Primer string de cada fila: el nombre del caso. No es dato de prueba — es identificación.
Cambio en LoginNegativeTests.java
@Test(dataProvider = "credencialesInvalidas", dataProviderClass = TestData.class)
public void loginInvalido_muestraError(String caseName, String usuario, String password, String mensajeEsperado) {
LoginPage loginPage = new LoginPage();
loginPage.loginComo(usuario, password);
DashboardPage dashboard = new DashboardPage();
Assert.assertEquals(loginPage.obtenerMensajeError(), mensajeEsperado, caseName + " - Mensaje de error");
Assert.assertFalse(dashboard.estaVisibleSafe(), caseName + " - El dashboard NO debería aparecer");
}caseName se agrega al mensaje del Assert. Si falla, el error dice "Password incorrecta - Mensaje de error" en vez de solo "Mensaje de error".
Resultado

Ahora cada ejecución del DataProvider tiene nombre. En el reporte y en IntelliJ se identifica de un vistazo qué caso es cada uno.
Con 3 filas parece un detalle menor. Cuando tenga 15 casos de validación de grilla, va a ser esencial.
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/qa-automation-lab
📂 selenium-java
—