Migrar de main() a TestNG: framework Selenium con Java
De un main() manual a TestNG con @Test, @BeforeMethod, BaseTest y testng.xml. 5 tests independientes, setup centralizado.
Contexto: el problema del runner manual
Hasta Page Object Model: separar Pages de Tests , mis tests se ejecutaban así:
public class LoginTestRunner {
public static void main(String[] args) {
LoginPositiveTests positive = new LoginPositiveTests();
positive.loginValido_deberiaIngresar();
positive.clickLoginSinTipear();
LoginNegativeTests negative = new LoginNegativeTests();
negative.loginInvalido_passwordIncorrecta_muestraError();
negative.loginInvalido_usuarioIncorrecto_muestraError();
negative.loginInvalido_tipearEspacios();
}
}Un main() que instancia clases y llama métodos a mano.
Funcionaba. Pero tenía problemas reales:
- Si un test fallaba con excepción, los siguientes no corrían. El
main()se cortaba ahí. - Si quería ejecutar solo los negativos, tenía que comentar líneas.
- No había reporte automático. Solo los prints de mi clase
check.java. - Cada test repetía las mismas 3 líneas de setup:
initDriver(),get(url),quitDriver(). - No podía ejecutar tests en paralelo.
El runner manual me sirvió para aprender. Pero ya no escala.
Qué es TestNG
TestNG (Test Next Generation) es un framework de testing para Java. Reemplaza la necesidad de un main() para ejecutar tests.
Con el runner manual:
LoginTestRunner.java (main) → crea instancias → llama métodos uno por unoCon TestNG:
TestNG detecta @Test automáticamente → ejecuta → reporta resultadosNo necesitás un runner. TestNG ES el runner.
Por qué TestNG y no JUnit
JUnit es el estándar más común en el mundo dev. TestNG me encaja mejor en este punto porque me da suites y parametrización muy cómoda para automation.
Lo que TestNG me da y que voy a usar:
@Test — Marco un método y TestNG lo ejecuta solo. Si uno falla, los demás siguen corriendo. Eso con el main() no pasaba.
@BeforeMethod / @AfterMethod — Código que se ejecuta antes y después de CADA test. Ahí va el setup (abrir browser, navegar a la URL) y el teardown (cerrar browser). Se escribe una vez y aplica a todos los tests.
testng.xml — Archivo donde defino qué tests correr. Puedo ejecutar solo positivos, solo negativos, o todos. Sin comentar código.
Reportes automáticos — TestNG genera un HTML con resultados: pasó, falló, cuánto tardó cada test.

Lo que voy a usar más adelante:
DataProviders — Correr el mismo test con distintos datos (múltiples usuarios, contraseñas). Viene en la sesión 9.
Ejecución paralela — Tests corriendo al mismo tiempo. Se configura en testng.xml.
Qué cambié en esta sesión
Tres cosas:
- Creé
BasePageyBaseTestpara eliminar código repetido - Migré los tests a TestNG con
@Test - Creé
testng.xmly eliminéLoginTestRunner
BasePage: eliminar repetición en las Pages
Hasta ahora, LoginPage y DashboardPage repetían lo mismo:
private final WebDriverWait wait = new WebDriverWait(DriverManager.getDriver(), Duration.ofSeconds(10));Cada Page necesita el driver y el wait. Repetir eso en cada una es mantenimiento innecesario.
Creé BasePage.java en src/main/java/com/cesar/qa/base/:
package com.cesar.qa.base;
import com.cesar.qa.config.DriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class BasePage {
protected WebDriver driver;
protected WebDriverWait wait;
public BasePage() {
this.driver = DriverManager.getDriver();
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
}protected significa que las clases que hereden de BasePage pueden usar driver y wait directamente.
Ahora LoginPage y DashboardPage extienden de BasePage:
public class LoginPage extends BasePage {
// wait y driver vienen heredados, no los declaro
}public class DashboardPage extends BasePage {
// mismo caso
}Eliminé la declaración del wait de ambas. Los métodos internos siguen funcionando igual porque wait ahora viene de BasePage.
BaseTest: eliminar repetición en los Tests
El mismo patrón pero para los tests.
Hasta ahora, cada test repetía:
DriverManager.initDriver();
String baseUrl = ConfigReader.getProperty("base.url");
DriverManager.getDriver().get(baseUrl);
// ... el test ...
DriverManager.quitDriver();Eso es setup y teardown. Es lo mismo en todos los tests. No tiene sentido escribirlo 5 veces.
Creé BaseTest.java en src/test/java/com/cesar/qa/base/:
package com.cesar.qa.base;
import com.cesar.qa.config.ConfigReader;
import com.cesar.qa.config.DriverManager;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
public class BaseTest {
@BeforeMethod
public void setUp() {
DriverManager.initDriver();
String baseUrl = ConfigReader.getProperty("base.url");
DriverManager.getDriver().get(baseUrl);
}
@AfterMethod
public void tearDown() {
DriverManager.quitDriver();
}
}@BeforeMethod se ejecuta antes de CADA método @Test. @AfterMethod se ejecuta después de CADA método @Test.
No importa si el test pasa o falla: @AfterMethod siempre corre. Eso garantiza que el browser se cierre. Con el runner manual, si un test fallaba con excepción, quitDriver() nunca se ejecutaba y quedaba un Chrome abierto.
Los tests migrados
LoginPositiveTests.java
Antes:
public class LoginPositiveTests {
public void loginValido_deberiaIngresar() {
DriverManager.initDriver();
String baseUrl = ConfigReader.getProperty("base.url");
DriverManager.getDriver().get(baseUrl);
LoginPage loginPage = new LoginPage();
loginPage.loginComo("admin", "serenity");
DashboardPage dashboard = new DashboardPage();
check.visible(dashboard.estaVisible(), "Dashboard visible luego de login válido");
check.equals(dashboard.obtenerTitulo(), "Tablero", "Titulo del dashboard");
DriverManager.quitDriver();
}
}Después:

public class LoginPositiveTests extends BaseTest {
@Test
public void loginValido_deberiaIngresar() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo("admin", "serenity");
DashboardPage dashboard = new DashboardPage();
check.visible(dashboard.estaVisible(), "Dashboard visible luego de login válido");
check.equals(dashboard.obtenerTitulo(), "Tablero", "Titulo del dashboard");
}
@Test
public void clickLoginSinTipear() {
LoginPage loginPage = new LoginPage();
loginPage.clickLogin();
DashboardPage dashboard = new DashboardPage();
check.visible(dashboard.estaVisible(), "Dashboard visible luego de login válido");
check.equals(dashboard.obtenerTitulo(), "Tablero", "Titulo del dashboard");
}
}Lo que desapareció: initDriver(), get(baseUrl), quitDriver(). Todo eso lo hace BaseTest.
Lo que se agregó: extends BaseTest y @Test en cada método.
LoginNegativeTests.java
Mismo patrón:

public class LoginNegativeTests extends BaseTest {
@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 deberia 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 deberia aparecer");
}
@Test
public void loginInvalido_tipearEspacios() {
LoginPage loginPage = new LoginPage();
loginPage.loginComo(" ", " ");
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 deberia aparecer");
}
}LoginTestRunner: eliminado
Ya no existe. TestNG es el runner ahora.
Pasé de controlar manualmente qué tests corren a dejar que TestNG lo haga por mí.
testng.xml: el control de la suite
Creé testng.xml en src/test/resources/:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="QA Automation Lab">
<test name="Login Tests">
<classes>
<class name="com.cesar.qa.tests.login.LoginPositiveTests"/>
<class name="com.cesar.qa.tests.login.LoginNegativeTests"/>
</classes>
</test>
</suite>Desde acá controlo qué se ejecuta. Si mañana agrego tests de otra funcionalidad, agrego otra sección <test> con sus clases.
Cómo ejecutar
Desde IntelliJ:
- Click derecho en un método
@Test→ Run (ejecuta solo ese test) - Click derecho en la clase → Run (ejecuta todos los @Test de esa clase)
- Click derecho en
testng.xml→ Run (ejecuta toda la suite)
Desde Maven (terminal):
mvn testEsto funciona porque en el pom.xml ya tenía configurado el plugin maven-surefire-plugin apuntando a testng.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>Ejecución
Ejecuté la suite completa desde testng.xml.
5 tests. Todos pasaron.
===============================================
QA Automation Lab
Total tests run: 5, Passes: 5, Failures: 0, Skips: 0
===============================================
Process finished with exit code 0

En esta ejecución los corrió primero X y después Y (según el orden del testng.xml): primero LoginPositiveTests (2 tests), después LoginNegativeTests (3 tests). Cada test abrió su propio browser, navegó a la URL, ejecutó, validó, y cerró. Sin que yo escribiera una sola línea de setup en los tests.
Las advertencias de CDP en el log son las mismas que aparecen desde la sesión 1. No son errores, no afectan la ejecución.
TestNG genera reportes (por ejemplo en test-output) con el resumen de ejecución y detalle de fallos.
Estructura actual del proyecto
src/
├── main/java/com/cesar/qa/
│ ├── base/
│ │ └── BasePage.java ← NUEVO
│ ├── config/
│ │ ├── ConfigReader.java
│ │ └── DriverManager.java
│ ├── pages/
│ │ ├── DashboardPage.java ← extends BasePage
│ │ └── LoginPage.java ← extends BasePage
│ └── utils/
│ └── check.java
│
├── test/java/com/cesar/qa/
│ ├── base/
│ │ └── BaseTest.java ← NUEVO
│ └── tests.login/
│ ├── LoginPositiveTests.java ← extends BaseTest + @Test
│ └── LoginNegativeTests.java ← extends BaseTest + @Test
│
└── test/resources/
├── config.properties
├── logback.xml
└── testng.xml ← NUEVOLoginTestRunner.java → eliminado.
Qué gané con esta sesión
| Antes (runner manual) | Después (TestNG) |
|---|---|
| Si un test fallaba, los siguientes no corrían | Cada test es independiente |
| Setup repetido en cada test (3 líneas) | Setup centralizado en BaseTest |
| Wait repetido en cada Page | Wait centralizado en BasePage |
| Para elegir qué correr: comentar código | testng.xml define qué corre |
| Sin reportes | HTML automático con resultados |
| Un solo punto de ejecución (main) | Ejecutar por test, por clase, o por suite |
Este fue el primer salto de mi lab de ‘código que corre’ a ‘framework que se ejecuta como suite’.
Sobre check.java
Sigue funcionando. No lo toqué.
Hoy mis validaciones pasan por check.equals(), check.visible(), check.isFalse(). Son prints con emojis que yo construí.
TestNG tiene su propio sistema de assertions (Assert.assertEquals, Assert.assertTrue). La diferencia es que las assertions de TestNG marcan el test como FAILED cuando algo no coincide. Mi check.java solo imprime, no corta la ejecución.
Voy a migrar a assertions de TestNG en la próxima sesión, junto con DataProviders.
Por ahora lo dejo así porque me sirve como log visual estilo TestComplete.
Próximo paso: DataProviders para ejecutar el mismo test con múltiples credenciales, y reemplazar check.java por assertions que fallen el test de TestNG.
Update:
Modifiqué el BasePage para que lea el timeout de config.properties


Así si mañana quiero cambiar el timeout de 10 a 15 segundos, lo toco en config.properties y aplica a todas las Pages sin modificar código.
🔗 Todo el código de esta serie está en: github.com/cesarbeassuarez/qa-automation-lab
📂 selenium-java
—