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ísticaTestNGJUnit 5Notas
Antes de todos los tests en clase@BeforeClass@BeforeAllJUnit 5 requiere método estático
Después de todos los tests en clase@AfterClass@AfterAllJUnit 5 requiere método estático
Antes de cada test@BeforeMethod@BeforeEachFuncionalidad similar
Después de cada test@AfterMethod@AfterEachFuncionalidad similar
Configuración de suite@BeforeSuiteN/AVentaja de TestNG
Limpieza de suite@AfterSuiteN/AVentaja de TestNG
Grupos de tests@Test(groups)@TagEnfoque diferente
Ignorar test@Test(enabled=false)@DisabledJUnit 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@ParameterizedTestParadigmas 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 separado
  • parallel="classes": Cada clase de test en hilo separado
  • parallel="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 UsoRecomendaciónRazonamiento
Nuevo proyecto JavaJUnit 5Arquitectura moderna, desarrollo activo
Proyecto Spring BootJUnit 5Soporte nativo Spring, mejor integración
Suites complejas con dependenciasTestNGGestión superior de dependencias
Grandes proyectos empresarialesTestNGConfiguración XML madura, gestión de suites
Testing de microserviciosJUnit 5Mejor modularidad, más ligero
Tests end-to-end de integraciónTestNGDependencias de flujo, hooks a nivel suite
CI/CD con JenkinsAmbosSoporte igual
Tests Selenium WebDriverAmbosCapacidades iguales
Testing de APIAmbosCapacidades iguales
Testing móvil (Appium)TestNGMejor 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

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