Введение

Выбор между TestNG и JUnit 5 (Jupiter) — одно из самых критических решений для команд автоматизации тестирования на Java. Оба фреймворка значительно эволюционировали, при этом JUnit (как обсуждается в Allure Framework: Creating Beautiful Test Reports) 5 внедрил многие функции, которые ранее были эксклюзивными для TestNG. Это подробное сравнение поможет вам принять обоснованное решение на основе возможностей, сценариев использования и стратегий миграции.

Исторический Контекст

Эволюция JUnit

  • JUnit 4 (2006): Внедрил аннотации, привнеся тестирование Java в современную эпоху
  • JUnit 5 (2017): Полная переписка с модульной архитектурой (Platform, Jupiter, Vintage)

Происхождение TestNG

  • Создан в 2004 Седриком Бейстом, вдохновленный JUnit и NUnit
  • Цель разработки: Устранить ограничения JUnit, особенно для интеграционных и функциональных тестов

Сравнение Архитектуры

Модульная Архитектура JUnit 5

JUnit 5 состоит из трех модулей:

<!-- JUnit (как обсуждается в [REST Assured: Java-Based API Testing Framework for Modern Applications](/blog/rest-assured-api-testing)) Platform: Основа для запуска тестовых фреймворков -->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit (как обсуждается в [WebdriverIO: Extensibility, Multiremote, and Migration Guide](/blog/webdriverio-extensibility-multiremote-migration))-platform-launcher</artifactId>
</dependency>

<!-- JUnit Jupiter: Новая модель программирования и расширений -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.10.1</version>
</dependency>

<!-- JUnit Vintage: Поддержка тестов JUnit 3 и JUnit 4 -->
<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <version>5.10.1</version>
</dependency>

TestNG Единый Модуль

<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>7.8.0</version>
</dependency>

Сравнение Аннотаций

ФункцияTestNGJUnit 5Примечания
Перед всеми тестами в классе@BeforeClass@BeforeAllJUnit 5 требует статический метод
После всех тестов в классе@AfterClass@AfterAllJUnit 5 требует статический метод
Перед каждым тестом@BeforeMethod@BeforeEachПохожая функциональность
После каждого теста@AfterMethod@AfterEachПохожая функциональность
Настройка сьюта@BeforeSuiteN/AПреимущество TestNG
Очистка сьюта@AfterSuiteN/AПреимущество TestNG
Группы тестов@Test(groups)@TagРазные подходы
Игнорировать тест@Test(enabled=false)@DisabledJUnit 5 более явный
Ожидаемое исключение@Test(expectedExceptions)assertThrows()JUnit 5 более гибкий
Таймаут@Test(timeOut)@Timeout или assertTimeout()Оба функциональны
Параметризованные тесты@DataProvider@ParameterizedTestРазные парадигмы

Детальное Сравнение Возможностей

1. Параметризованные Тесты

TestNG с 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 с @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);
}

// Альтернатива: @MethodSource для сложных данных
@ParameterizedTest
@MethodSource("loginDataProvider")
void testLoginMethodSource(String username, String password, boolean shouldSucceed) {
    // Реализация теста
}

static Stream<Arguments> loginDataProvider() {
    return Stream.of(
        Arguments.of("user1", "pass1", true),
        Arguments.of("user2", "wrongpass", false),
        Arguments.of("", "pass3", false)
    );
}

Вердикт: JUnit 5 предлагает больше встроенных источников (@CsvSource, @ValueSource, @EnumSource, @MethodSource), в то время как @DataProvider TestNG более гибкий для сложных сценариев.

2. Зависимости Тестов

TestNG:

@Test
public void testLogin() {
    // Тест входа
}

@Test(dependsOnMethods = "testLogin")
public void testAddToCart() {
    // Выполняется только если testLogin прошел
}

@Test(dependsOnGroups = "sanity")
public void testCheckout() {
    // Выполняется после всех тестов в группе "sanity"
}

JUnit 5:

JUnit 5 не поддерживает зависимости тестов по дизайну. Тесты должны быть независимыми. Используйте @TestMethodOrder для упорядочивания:

@TestMethodOrder(OrderAnnotation.class)
class CheckoutFlowTest {

    @Test
    @Order(1)
    void testLogin() {
        // Тест входа
    }

    @Test
    @Order(2)
    void testAddToCart() {
        // Тест добавления в корзину
    }
}

Вердикт: Управление зависимостями TestNG мощное для интеграционных тестов, где важен порядок выполнения. JUnit 5 обеспечивает лучшую изоляцию тестов.

3. Параллельное Выполнение

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>

Опции параллельного выполнения:

  • parallel="methods": Каждый метод теста в отдельном потоке
  • parallel="classes": Каждый класс теста в отдельном потоке
  • parallel="tests": Каждый тег <test> в отдельном потоке

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

Программное управление:

@Execution(ExecutionMode.CONCURRENT)
class ParallelTest {
    @Test
    void test1() { }

    @Test
    void test2() { }
}

Вердикт: TestNG имеет более зрелую параллельную конфигурацию на основе XML. Параллельное выполнение JUnit 5 новее, но быстро улучшается.

4. Группы Тестов и Тегирование

TestNG:

@Test(groups = {"sanity", "regression"})
public void testCriticalFeature() {
    // Код теста
}

@Test(groups = "regression")
public void testSecondaryFeature() {
    // Код теста
}

Запуск определенных групп через 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() {
    // Код теста
}

@Test
@Tag("regression")
void testSecondaryFeature() {
    // Код теста
}

Конфигурация Maven:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <groups>sanity</groups>
    </configuration>
</plugin>

Вердикт: Оба предлагают похожую функциональность. Подход TestNG на основе XML более гибкий для сложных конфигураций тестовых сьютов.

5. Тестирование Исключений

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"));
}

Вердикт: Подход JUnit 5 более гибкий, позволяя проверки на объекте исключения.

6. Динамические Тесты

Только JUnit 5:

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
    return Stream.of("apple", "banana", "orange")
        .map(fruit -> dynamicTest(
            "Test " + fruit,
            () -> assertFalse(fruit.isEmpty())
        ));
}

Вердикт: JUnit 5 имеет нативную поддержку динамической генерации тестов во время выполнения. TestNG требует пользовательских решений.

Сравнение Продвинутых Возможностей

Уникальные Возможности TestNG

1. Конфигурация на Уровне Сьюта:

@BeforeSuite
public void setupDatabase() {
    // Выполняется один раз перед всем набором тестов
}

@AfterSuite
public void cleanupDatabase() {
    // Выполняется один раз после всего набора тестов
}

2. Гибкая Конфигурация Тестов:

@Test(invocationCount = 5, threadPoolSize = 3)
public void stressTest() {
    // Выполняется 5 раз с 3 одновременными потоками
}

@Test(successPercentage = 80)
public void flakyTest() {
    // Проходит, если 80% вызовов успешны
}

3. Встроенная Логика Повторов:

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() {
    // Будет повторяться до 3 раз при неудаче
}

Уникальные Возможности JUnit 5

1. Вложенные Тесты:

@DisplayName("Тесты Корзины Покупок")
class ShoppingCartTest {

    @Nested
    @DisplayName("Когда корзина пуста")
    class EmptyCart {
        @Test
        void shouldHaveZeroItems() { }

        @Test
        void shouldHaveZeroTotal() { }
    }

    @Nested
    @DisplayName("Когда в корзине есть товары")
    class NonEmptyCart {
        @BeforeEach
        void addItems() { }

        @Test
        void shouldCalculateCorrectTotal() { }
    }
}

2. Условное Выполнение Тестов:

@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() { }

@Test
@EnabledIfSystemProperty(named = "env", matches = "prod")
void onlyInProduction() { }

@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void onlyInCI() { }

3. Модель Расширений:

@ExtendWith(TimingExtension.class)
class PerformanceTest {
    @Test
    void fastOperation() { }
}

// Пользовательское расширение
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
    @Override
    public void beforeTestExecution(ExtensionContext context) {
        // Запустить таймер
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        // Записать время выполнения
    }
}

Матрица Решений

Сценарий ИспользованияРекомендацияОбоснование
Новый Java проектJUnit 5Современная архитектура, активная разработка
Spring Boot проектJUnit 5Нативная поддержка Spring, лучшая интеграция
Сложные сьюты с зависимостямиTestNGПревосходное управление зависимостями
Крупные корпоративные проектыTestNGЗрелая XML конфигурация, управление сьютами
Тестирование микросервисовJUnit 5Лучшая модульность, легковесность
End-to-end интеграционные тестыTestNGЗависимости рабочего процесса, хуки уровня сьюта
CI/CD с JenkinsОбаРавная поддержка
Тесты Selenium WebDriverОбаРавные возможности
Тестирование APIОбаРавные возможности
Мобильное тестирование (Appium)TestNGЛучший контроль параллельного выполнения

Руководство по Миграции: TestNG в JUnit 5

Шаг 1: Обновить Зависимости

Заменить TestNG на JUnit 5 в pom.xml:

<!-- Удалить -->
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
</dependency>

<!-- Добавить -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>

Шаг 2: Обновить Аннотации

TestNGЭквивалент JUnit 5
@BeforeClass@BeforeAll (сделать метод статическим)
@AfterClass@AfterAll (сделать метод статическим)
@BeforeMethod@BeforeEach
@AfterMethod@AfterEach
@Test(enabled=false)@Disabled

Шаг 3: Заменить DataProviders

До (TestNG):

@DataProvider
public Object[][] testData() {
    return new Object[][] {{"test1"}, {"test2"}};
}

@Test(dataProvider = "testData")
public void test(String input) { }

После (JUnit 5):

@ParameterizedTest
@ValueSource(strings = {"test1", "test2"})
void test(String input) { }

Шаг 4: Обработать Зависимости Тестов

Заменить dependsOnMethods на правильную изоляцию тестов или использовать @TestMethodOrder:

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTests {
    @Test
    @Order(1)
    void firstTest() { }

    @Test
    @Order(2)
    void secondTest() { }
}

Соображения Производительности

Время Запуска

  • JUnit 5: Более быстрый запуск благодаря ленивой загрузке
  • TestNG: Немного медленнее, но незначительно в большинстве случаев

Скорость Выполнения

Оба фреймворка имеют похожую скорость выполнения. Различия обычно специфичны для проекта.

След Памяти

  • JUnit 5: Меньший след, модульный дизайн
  • TestNG: Немного больше, но включает больше встроенных возможностей

Сообщество и Экосистема

JUnit 5

  • Принятие: Широко принят, особенно в экосистеме Spring
  • Документация: Комплексная официальная документация
  • Интеграция: Отличная поддержка IDE (IntelliJ, Eclipse, VS Code)
  • Плагины: Обширная поддержка плагинов Maven/Gradle

TestNG

  • Принятие: Сильная позиция в Selenium и корпоративной автоматизации
  • Документация: Хорошая документация, много сторонних ресурсов
  • Интеграция: Хорошая поддержка IDE, зрелые инструменты
  • Плагины: Надежная экосистема плагинов

Заключение

Выбирайте JUnit 5, если:

  • Начинаете новый проект
  • Используете Spring Boot
  • Предпочитаете современные возможности Java
  • Хотите меньший след зависимостей
  • Цените строгую независимость тестов

Выбирайте TestNG, если:

  • Управляете сложными сьютами с зависимостями
  • Нужны хуки конфигурации на уровне сьюта
  • Требуется гибкий контроль параллельного выполнения
  • Работаете с legacy корпоративными системами
  • Предпочитаете XML-конфигурацию тестов

Оба фреймворка готовы к продакшн и способны решать задачи. Выбор часто зависит от знакомства команды, существующей инфраструктуры и конкретных требований проекта. Многие организации успешно используют оба фреймворка в разных проектах в зависимости от соответствия сценарию использования.