Introducción
Elegir entre TestNG y JUnit 5 (Jupiter) es una de las decisiones más críticas para equipos de automatización de pruebas en Java. Ambos frameworks han evolucionado significativamente, con JUnit (como se discute en Allure Framework: Creating Beautiful Test Reports) 5 introduciendo muchas características que anteriormente eran exclusivas de TestNG. Esta comparación comprehensiva te ayudará a tomar una decisión informada basada en características, casos de uso y estrategias de migración.
Contexto Histórico
Evolución de JUnit
- JUnit 4 (2006): Introdujo anotaciones, llevando las pruebas Java a la era moderna
- JUnit 5 (2017): Reescritura completa con arquitectura modular (Platform, Jupiter, Vintage)
Orígenes de TestNG
- Creado en 2004 por Cédric Beust, inspirado en JUnit y NUnit
- Objetivo de diseño: Abordar las limitaciones de JUnit, particularmente para pruebas de integración y funcionales
Comparación de Arquitectura
Arquitectura Modular de JUnit 5
JUnit 5 consiste en tres módulos:
<!-- JUnit (como se discute en [REST Assured: Java-Based API Testing Framework for Modern Applications](/blog/rest-assured-api-testing)) Platform: Base para lanzar frameworks de testing -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit (como se discute en [WebdriverIO: Extensibility, Multiremote, and Migration Guide](/blog/webdriverio-extensibility-multiremote-migration))-platform-launcher</artifactId>
</dependency>
<!-- JUnit Jupiter: Nuevo modelo de programación y extensión -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
</dependency>
<!-- JUnit Vintage: Soporte para tests de JUnit 3 y JUnit 4 -->
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.1</version>
</dependency>
TestNG Módulo Único
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.8.0</version>
</dependency>
Comparación de Anotaciones
Característica | TestNG | JUnit 5 | Notas |
---|---|---|---|
Antes de todos los tests en clase | @BeforeClass | @BeforeAll | JUnit 5 requiere método estático |
Después de todos los tests en clase | @AfterClass | @AfterAll | JUnit 5 requiere método estático |
Antes de cada test | @BeforeMethod | @BeforeEach | Funcionalidad similar |
Después de cada test | @AfterMethod | @AfterEach | Funcionalidad similar |
Configuración de suite | @BeforeSuite | N/A | Ventaja de TestNG |
Limpieza de suite | @AfterSuite | N/A | Ventaja de TestNG |
Grupos de tests | @Test(groups) | @Tag | Enfoque diferente |
Ignorar test | @Test(enabled=false) | @Disabled | JUnit 5 más explícito |
Excepción esperada | @Test(expectedExceptions) | assertThrows() | JUnit 5 más flexible |
Timeout | @Test(timeOut) | @Timeout o assertTimeout() | Ambos funcionales |
Tests parametrizados | @DataProvider | @ParameterizedTest | Paradigmas diferentes |
Comparación Detallada de Características
1. Tests Parametrizados
TestNG con DataProvider:
@DataProvider(name = "loginData")
public Object[][] loginDataProvider() {
return new Object[][] {
{"user1", "pass1", true},
{"user2", "wrongpass", false},
{"", "pass3", false}
};
}
@Test(dataProvider = "loginData")
public void testLogin(String username, String password, boolean shouldSucceed) {
LoginPage page = new LoginPage();
boolean result = page.login(username, password);
assertEquals(result, shouldSucceed);
}
JUnit 5 con @ParameterizedTest:
@ParameterizedTest
@CsvSource({
"user1, pass1, true",
"user2, wrongpass, false",
", pass3, false"
})
void testLogin(String username, String password, boolean shouldSucceed) {
LoginPage page = new LoginPage();
boolean result = page.login(username, password);
assertEquals(result, shouldSucceed);
}
// Alternativa: @MethodSource para datos complejos
@ParameterizedTest
@MethodSource("loginDataProvider")
void testLoginMethodSource(String username, String password, boolean shouldSucceed) {
// Implementación del test
}
static Stream<Arguments> loginDataProvider() {
return Stream.of(
Arguments.of("user1", "pass1", true),
Arguments.of("user2", "wrongpass", false),
Arguments.of("", "pass3", false)
);
}
Veredicto: JUnit 5 ofrece más fuentes integradas (@CsvSource
, @ValueSource
, @EnumSource
, @MethodSource
), mientras que el @DataProvider
de TestNG es más flexible para escenarios complejos.
2. Dependencias de Tests
TestNG:
@Test
public void testLogin() {
// Test de login
}
@Test(dependsOnMethods = "testLogin")
public void testAddToCart() {
// Se ejecuta solo si testLogin pasa
}
@Test(dependsOnGroups = "sanity")
public void testCheckout() {
// Se ejecuta después de todos los tests en grupo "sanity"
}
JUnit 5:
JUnit 5 no soporta dependencias de tests por diseño. Los tests deben ser independientes. Usa @TestMethodOrder
para ordenamiento:
@TestMethodOrder(OrderAnnotation.class)
class CheckoutFlowTest {
@Test
@Order(1)
void testLogin() {
// Test de login
}
@Test
@Order(2)
void testAddToCart() {
// Test de agregar al carrito
}
}
Veredicto: La gestión de dependencias de TestNG es poderosa para pruebas de integración donde el orden del flujo importa. JUnit 5 fuerza mejor aislamiento de tests.
3. Ejecución Paralela
TestNG (testng.xml):
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel Suite" parallel="methods" thread-count="5">
<test name="Regression">
<classes>
<class name="com.example.LoginTest"/>
<class name="com.example.CheckoutTest"/>
</classes>
</test>
</suite>
Opciones de ejecución paralela:
parallel="methods"
: Cada método de test en hilo separadoparallel="classes"
: Cada clase de test en hilo separadoparallel="tests"
: Cada tag<test>
en hilo separado
JUnit 5 (junit-platform.properties):
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = dynamic
Control programático:
@Execution(ExecutionMode.CONCURRENT)
class ParallelTest {
@Test
void test1() { }
@Test
void test2() { }
}
Veredicto: TestNG tiene configuración paralela más madura, basada en XML. La ejecución paralela de JUnit 5 es más nueva pero mejorando rápidamente.
4. Grupos de Tests y Etiquetado
TestNG:
@Test(groups = {"sanity", "regression"})
public void testCriticalFeature() {
// Código del test
}
@Test(groups = "regression")
public void testSecondaryFeature() {
// Código del test
}
Ejecutar grupos específicos vía testng.xml:
<test name="Sanity Tests">
<groups>
<run>
<include name="sanity"/>
</run>
</groups>
<classes>
<class name="com.example.AllTests"/>
</classes>
</test>
JUnit 5:
@Test
@Tag("sanity")
@Tag("regression")
void testCriticalFeature() {
// Código del test
}
@Test
@Tag("regression")
void testSecondaryFeature() {
// Código del test
}
Configuración Maven:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>sanity</groups>
</configuration>
</plugin>
Veredicto: Ambos ofrecen funcionalidad similar. El enfoque basado en XML de TestNG es más flexible para configuraciones complejas de suites de tests.
5. Prueba de Excepciones
TestNG:
@Test(expectedExceptions = IllegalArgumentException.class,
expectedExceptionsMessageRegExp = ".*invalid input.*")
public void testInvalidInput() {
service.processInput("");
}
JUnit 5:
@Test
void testInvalidInput() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> service.processInput("")
);
assertTrue(exception.getMessage().contains("invalid input"));
}
Veredicto: El enfoque de JUnit 5 es más flexible, permitiendo aserciones sobre el objeto de excepción.
6. Tests Dinámicos
Solo JUnit 5:
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("apple", "banana", "orange")
.map(fruit -> dynamicTest(
"Test " + fruit,
() -> assertFalse(fruit.isEmpty())
));
}
Veredicto: JUnit 5 tiene soporte nativo para generación dinámica de tests en runtime. TestNG requiere soluciones personalizadas.
Comparación de Características Avanzadas
Características Únicas de TestNG
1. Configuración a Nivel de Suite:
@BeforeSuite
public void setupDatabase() {
// Se ejecuta una vez antes de toda la suite de tests
}
@AfterSuite
public void cleanupDatabase() {
// Se ejecuta una vez después de toda la suite de tests
}
2. Configuración Flexible de Tests:
@Test(invocationCount = 5, threadPoolSize = 3)
public void stressTest() {
// Se ejecuta 5 veces con 3 hilos concurrentes
}
@Test(successPercentage = 80)
public void flakyTest() {
// Pasa si 80% de las invocaciones tienen éxito
}
3. Lógica de Reintento Integrada:
public class RetryAnalyzer implements IRetryAnalyzer {
private int retryCount = 0;
private static final int maxRetryCount = 3;
@Override
public boolean retry(ITestResult result) {
if (retryCount < maxRetryCount) {
retryCount++;
return true;
}
return false;
}
}
@Test(retryAnalyzer = RetryAnalyzer.class)
public void unstableTest() {
// Se reintentará hasta 3 veces en caso de fallo
}
Características Únicas de JUnit 5
1. Tests Anidados:
@DisplayName("Tests de Carrito de Compras")
class ShoppingCartTest {
@Nested
@DisplayName("Cuando el carrito está vacío")
class EmptyCart {
@Test
void shouldHaveZeroItems() { }
@Test
void shouldHaveZeroTotal() { }
}
@Nested
@DisplayName("Cuando el carrito tiene artículos")
class NonEmptyCart {
@BeforeEach
void addItems() { }
@Test
void shouldCalculateCorrectTotal() { }
}
}
2. Ejecución Condicional de Tests:
@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() { }
@Test
@EnabledIfSystemProperty(named = "env", matches = "prod")
void onlyInProduction() { }
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void onlyInCI() { }
3. Modelo de Extensión:
@ExtendWith(TimingExtension.class)
class PerformanceTest {
@Test
void fastOperation() { }
}
// Extensión personalizada
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
@Override
public void beforeTestExecution(ExtensionContext context) {
// Iniciar temporizador
}
@Override
public void afterTestExecution(ExtensionContext context) {
// Registrar tiempo de ejecución
}
}
Matriz de Decisión
Caso de Uso | Recomendación | Razonamiento |
---|---|---|
Nuevo proyecto Java | JUnit 5 | Arquitectura moderna, desarrollo activo |
Proyecto Spring Boot | JUnit 5 | Soporte nativo Spring, mejor integración |
Suites complejas con dependencias | TestNG | Gestión superior de dependencias |
Grandes proyectos empresariales | TestNG | Configuración XML madura, gestión de suites |
Testing de microservicios | JUnit 5 | Mejor modularidad, más ligero |
Tests end-to-end de integración | TestNG | Dependencias de flujo, hooks a nivel suite |
CI/CD con Jenkins | Ambos | Soporte igual |
Tests Selenium WebDriver | Ambos | Capacidades iguales |
Testing de API | Ambos | Capacidades iguales |
Testing móvil (Appium) | TestNG | Mejor control de ejecución paralela |
Guía de Migración: TestNG a JUnit 5
Paso 1: Actualizar Dependencias
Reemplazar TestNG con JUnit 5 en pom.xml
:
<!-- Eliminar -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
</dependency>
<!-- Agregar -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
Paso 2: Actualizar Anotaciones
TestNG | Equivalente JUnit 5 |
---|---|
@BeforeClass | @BeforeAll (hacer método estático) |
@AfterClass | @AfterAll (hacer método estático) |
@BeforeMethod | @BeforeEach |
@AfterMethod | @AfterEach |
@Test(enabled=false) | @Disabled |
Paso 3: Reemplazar DataProviders
Antes (TestNG):
@DataProvider
public Object[][] testData() {
return new Object[][] {{"test1"}, {"test2"}};
}
@Test(dataProvider = "testData")
public void test(String input) { }
Después (JUnit 5):
@ParameterizedTest
@ValueSource(strings = {"test1", "test2"})
void test(String input) { }
Paso 4: Manejar Dependencias de Tests
Reemplazar dependsOnMethods
con aislamiento apropiado de tests o usar @TestMethodOrder
:
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTests {
@Test
@Order(1)
void firstTest() { }
@Test
@Order(2)
void secondTest() { }
}
Consideraciones de Rendimiento
Tiempo de Inicio
- JUnit 5: Inicio más rápido debido a carga perezosa
- TestNG: Ligeramente más lento pero despreciable en la mayoría de casos
Velocidad de Ejecución
Ambos frameworks tienen velocidades de ejecución similares. Las diferencias suelen ser específicas del proyecto.
Huella de Memoria
- JUnit 5: Huella más pequeña, diseño modular
- TestNG: Ligeramente mayor pero incluye más características integradas
Comunidad y Ecosistema
JUnit 5
- Adopción: Ampliamente adoptado, especialmente en ecosistema Spring
- Documentación: Documentación oficial comprehensiva
- Integración: Excelente soporte IDE (IntelliJ, Eclipse, VS Code)
- Plugins: Extenso soporte de plugins Maven/Gradle
TestNG
- Adopción: Fuerte en Selenium y automatización empresarial
- Documentación: Buena documentación, muchos recursos de terceros
- Integración: Buen soporte IDE, herramientas maduras
- Plugins: Ecosistema robusto de plugins
Conclusión
Elige JUnit 5 si:
- Inicias un nuevo proyecto
- Usas Spring Boot
- Prefieres características modernas de Java
- Quieres huella de dependencia más ligera
- Valoras independencia estricta de tests
Elige TestNG si:
- Gestionas suites complejas con dependencias
- Necesitas hooks de configuración a nivel suite
- Requieres control flexible de ejecución paralela
- Trabajas con sistemas empresariales legacy
- Prefieres configuración de tests basada en XML
Ambos frameworks están listos para producción y son capaces. La elección a menudo depende de la familiaridad del equipo, infraestructura existente y requisitos específicos del proyecto. Muchas organizaciones usan exitosamente ambos frameworks en diferentes proyectos según la apropiación del caso de uso.