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.

TestData.java con DataProvider credencialesInvalidas y estructura del proyecto mostrando el nuevo paquete data en src/test/java
TestData.java — los datos de prueba viven en su propia clase, separados de los tests. El paquete com.cesar.qa.data es nuevo.

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:

  1. Ir a login
  2. Tipear credenciales inválidas
  3. Verificar mensaje de error
  4. 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");
}
LoginNegativeTests.java con un solo método @Test conectado a DataProvider externo y assertions de TestNG reemplazando check.java
LoginNegativeTests.java — de 3 métodos a 1. El DataProvider inyecta los datos, Assert valida de verdad.

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)
}
TestData.java mostrando DataProvider con 3 filas de credenciales inválidas y secciones comentadas para futuros módulos de clientes y pedidos
TestData.java — cada fila del array es una ejecución del test. Agregar un caso nuevo es agregar una línea.

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

Resultado de ejecución en IntelliJ: 5 tests passed en 1 minuto 33 segundos, LoginPositiveTests con 2 tests y LoginNegativeTests con 3 ejecuciones del DataProvider
5 tests, todos verdes. LoginNegativeTests muestra 3 ejecuciones generadas por el DataProvider desde un solo método.
Reporte HTML de TestNG mostrando 15 total 15 passed, con LoginPositiveTests (2 tests) y LoginNegativeTests mostrando 3 ejecuciones del DataProvider con los datos de cada fila visibles en el nombre del test
Reporte HTML automático de TestNG. Los tests negativos muestran los datos del DataProvider en el nombre: usuario, contraseña y mensaje esperado. 5 tests, todos verdes.

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_muestraError con 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"}
    };
}
estData.java con flechas verdes señalando los caseName agregados: Password incorrecta, Usuario incorrecto, Espacios en blanco como primer parámetro de cada fila del DataProvider
TestData.java actualizado — el primer string de cada fila es el nombre del caso. No es dato de prueba, es identificación.

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

Reporte HTML de TestNG con caseName resaltado en amarillo mostrando Password incorrecta, Usuario incorrecto y Espacios en blanco al inicio de cada ejecución del DataProvider
Reporte HTML con caseName. Cada test negativo ahora se identifica por nombre, no por datos crudos.

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

Temas conectados:

Page Object Model: separar Pages de Tests 

Framework TestNG