Введение
Выбор между 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>
Сравнение Аннотаций
Функция | TestNG | JUnit 5 | Примечания |
---|---|---|---|
Перед всеми тестами в классе | @BeforeClass | @BeforeAll | JUnit 5 требует статический метод |
После всех тестов в классе | @AfterClass | @AfterAll | JUnit 5 требует статический метод |
Перед каждым тестом | @BeforeMethod | @BeforeEach | Похожая функциональность |
После каждого теста | @AfterMethod | @AfterEach | Похожая функциональность |
Настройка сьюта | @BeforeSuite | N/A | Преимущество TestNG |
Очистка сьюта | @AfterSuite | N/A | Преимущество TestNG |
Группы тестов | @Test(groups) | @Tag | Разные подходы |
Игнорировать тест | @Test(enabled=false) | @Disabled | JUnit 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-конфигурацию тестов
Оба фреймворка готовы к продакшн и способны решать задачи. Выбор часто зависит от знакомства команды, существующей инфраструктуры и конкретных требований проекта. Многие организации успешно используют оба фреймворка в разных проектах в зависимости от соответствия сценарию использования.