TL;DR

  • Elegí JUnit 5 para: proyectos nuevos, Spring Boot, características modernas de Java, microservicios, independencia estricta de tests
  • Elegí TestNG para: suites complejas con dependencias, hooks a nivel de suite, configuración XML, Selenium con ejecución paralela
  • JUnit 5 domina las descargas de Maven Central; TestNG sigue siendo fuerte en automatización enterprise
  • Ambos soportan ejecución paralela, tests parametrizados e integración con CI/CD

Tiempo de lectura: 14 minutos

JUnit 5 y TestNG son los dos frameworks de testing Java dominantes, pero han divergido significativamente en filosofía y casos de uso. JUnit 5, que tuvo su release estable en 2017, se ha convertido en el framework de testing predeterminado para proyectos Spring Boot y desarrollo Java moderno — con miles de millones de descargas mensuales en Maven Central. TestNG, creado en 2004 por Cédric Beust, sigue siendo el framework elegido para automatización Selenium enterprise y suites de pruebas de integración complejas, con una fuerte audiencia en equipos que dependen de su gestión de suites basada en XML y sus características de dependencia entre tests. Según la JetBrains Developer Ecosystem Survey 2024, JUnit lo usan el 79% de los desarrolladores Java que escriben tests, mientras que TestNG representa aproximadamente el 25% — con una superposición significativa en entornos enterprise. La elección entre ellos raramente se reduce a brechas de capacidad (ambos pueden manejar la mayoría de las necesidades de testing) sino más bien al ajuste con el ecosistema, el background del equipo y las características específicas que requiere tu arquitectura de tests.

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](/es/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](/es/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

«Los equipos que más dificultades tienen con esta decisión son los que la tratan como puramente técnica. En la práctica, si tu equipo ya tiene 500 tests de Selenium en TestNG y usa el suite XML para ejecución paralela en distintos entornos, cambiar a JUnit 5 es meses de migración por una ganancia marginal. Pero si estás empezando desde cero con microservicios Spring Boot, usar JUnit 5 por defecto hace que todo el stack sea consistente — Spring Test, Mockito, AssertJ funcionan sin ningún puente.» — Yuri Kan, Senior QA Lead

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.

Recursos Oficiales

See Also