JUnit и TestNG два самых популярных тестовых фреймворка для Java. Оба сильно схожи по функциональности, однако интереснее разобраться в отличиях.
TestNG реализует core логику JUnit, который старше него и послужил вдохновением для первого. Но TestNG (NG означает Next Generation) изначально создавался для функционального и более высоких уровней тестирования, позиционировался как более простой в использовании, чем заслужил признание автоматизаторов.
Название JUnit (сокращение от Java Unit) как бы намекало, что это инструмент unit-тестирования. Отчасти так и было во времена JUnit 4, но с выходом JUnit 5 возможности фреймворков уравнялись.
JUnit 5 написан с нуля и сильно отличается от предыдущих версий. В то же время JUnit 4 все еще имеет некоторую популярность. Поэтому сравнивать будем не два, а целых три фреймворка: JUnit 4, JUnit 5 и TestNG.
Отличие номер один
Как писал Седрик Бейст, он создал TestNG, потому что был разочарован недостатками JUnit. По его мнению, главный недостаток заключался в том, что JUnit заново создает новый инстанс тестового класса перед каждым тестовым методом. В TestNG создается один инстанс на все тестовые методы.
public class SomeTest {
public SomeTest() {
Sysrem.out.println("Created test");
}
@Test public void testOne(){}
@Test public void testTwo(){}
@Test public void testThree(){}
}
Пример кода выше в JUnit выведет:
Created test
Created test
Created test
В TestNG результат будет таким:
Created test
Пожалуй, это принципиальное различие, которое существовало у фреймворков изначально. Хорошо это или плохо – вопрос спорный. Но почему это может быть важно?
В JUnit класс и необходимые поля можно объявить так.
public class SomeTest {
Foo foo = mock(Foo.class);
Counter cnt = new Counter(0);
}
В TestNG лучше так не делать. Из-за того, что класс создается один раз, при объявлении полей как в примере выше, в переменных может остаться что-то от прохождения предыдущих тестов. Это несет риск зависимых тестов. Поэтому в TestNG сначала нужно объявить поля, а инициализировать их в сетап-методе.
public class SomeTest {
Foo foo;
Counter cnt;
@BeforeEach
public void setUp() {
foo = mock(Foo.class);
cnt = new Counter(0);
}
}
Аннотации
Основные аннотации фреймворков приведены в таблице ниже. В целом выглядят похоже.
JUnit 4 | JUnit 5 | TestNG | |
Аннотация теста | @Test | @Test | @Test |
Название теста | — | @DisplayName | @Test(description = «test name») |
Запуск перед сьютом | — | — | @BeforeSuite |
Запуск после сьюта | — | — | @AfterSuite |
Запуск перед тестированием | — | — | @BeforeTest |
Запуск после тестирования | — | — | @AfterTest |
Запуск перед тестом из группы | — | — | @BeforeGroups |
Запуск после теста из группы | — | — | @AfterGroups |
Запуск перед классом | @BeforeClass | @BeforeAll | @BeforeClass |
Запуск после класса | @AfterClass | @AfterAll | @AfterClass |
Запуск перед каждым тестовым методом | @Before | @BeforeEach | @BeforeMethod |
Запуск после каждого тестого метода | @After | @AfterEach | @AfterMethod |
Игнорировать тест | @ignore | @Disabled | @Test(enbale=false) |
Test factory для динамических тестов | — | @TestFactory | @Factory |
Тегирование | @Category | @Tag | — |
Таймаут | @Test(timeout = 1000) | @Test(timeout = 1000) | @Test(timeout = 1000) |
Подробнее об аннотациях можно узнать в официальной документации:
Сьюты в JUnit и TestNG
В JUnit 4 мы группировка нескольких тестов в сьюты возможна с помощью аннотаций @Suite и @RunWith. В примере ниже, тесты Test1 и Test2 будут запущены после Test3.
@RunWith(Suite.class)
@Suite.SuiteClasses({
Test1.class,
Test2.class
})
public class Test3 {
}
JUnit 5 для создания сьютов предоставляет аннотации @SelectPackages и @SelectClasses.
Если нужно сгруппировать тест-кейсы различных пакетов для запуска вместе в Suite необходимо использовать аннотацию @SelectPackages.
@RunWith(JUnit.class)
@SelectPackages({ "org.suite.package1", "org.suite.package2" })
public class JUnit5TestSuiteExample {
}
Если нужно запустить определенные тестовые классы вместе, необходимо использовать @SelectClasses.
@RunWith(JUnit.class)
@SelectClasses({Class1Test.class, Class2Test.class})
public class JUnit5TestSuiteExample {
}
В TestNG тестовые наборы определяются в XML-файле. Пример конфигурации ниже, запустит тесты из TestNGTests1 и TestNGTests2.
<suite name="TestSuite1">
<test name="Demo">
<classes>
<class name="org.testng.TestNGTests1" />
<class name="org.testng.TestNGTests2" />
</classes>
</test>
</suite>
Еще в TestNG можно сгруппировать методы с помощью @Test (groups = «groupName»).
@Test(groups="method1")
public void testingMethod1_1() {
System.out.println("Method - testingMethod1_1()");
}
@Test(groups="method1")
public void testingMethod1_2() {
System.out.println("Method - testingMethod1_2()");
}
@Test(groups = { "method2", "smoke" })
public void testingMethod2() {
System.out.println("Method - testingMethod2()");
}
@Test(groups="method3")
public void testingMethod3() {
System.out.println("Method - testingMethod4()");
}
А затем запустить указав нужные в сьюте.
<suite name="TestSuite2">
<test name="Group">
<groups>
<run>
<include name="method1"/>
</run>
</groups>
<classes>
<class name="org.testng.TestNGTests1" />
</classes>
</test>
</suite>
Before и After
Аннотации типа Before и After используются для выполнения некоторого кода перед или после выполнения тестов, например для установки переменных или настройки конфигурации.
Обратите внимание, что в JUnit 4 @BeforeClass и @AfterClass объявляются как статичные методы.
@BeforeClass
public static void MethodName() {
// one-time initialization code
System.out.println("@BeforeClass - oneTimeSetUp");
}
В TestNG указанного ограничения нет, и самих типов аннотаций Before и After больше (см. таблицу с аннотациями выше).
@BeforeClass
public void MethodName() {
// one-time initialization code
System.out.println("@BeforeClass - oneTimeSetUp");
}
@AfterClass
public void MethodName() {
// one-time initialization code
System.out.println("@AfterClass - oneTimeSetUp");
}
В том числе TestNG предлагает аннотации @BeforeSuite, @AfterSuite, @BeforeGroup и @AfterGroup для конфигураций на уровне сьюта и группы:
В JUnit 5 @BeforeAll и @AfterAll (пришедшие на смену @BeforeClass и @AfterClass из JUnit 4) статические. Однако @BeforeEach и @AfterEach вызываются для каждого экземпляра теста и статическими быть не должны.
public class AppTest {
@BeforeAll
static void setup(){
System.out.println("@BeforeAll executed");
}
@BeforeEach
void setupThis(){
System.out.println("@BeforeEach executed");
}
@Test
void testOne()
{
System.out.println("TEST ONE EXECUTED");
}
@Test
void testTwo()
{
System.out.println("TEST TWO EXECUTED");
}
@AfterEach
void tearThis(){
System.out.println("@AfterEach executed");
}
@AfterAll
static void tear(){
System.out.println("@AfterAll executed");
}
}
@BeforeAll executed
@BeforeEach executed
TEST ONE EXECUTED
@AfterEach executed
@BeforeEach executed
TEST TWO EXECUTED
@AfterEach executed
@AfterAll executed
Игнорирование тестов
Фреймворки поддерживают игнорирование тестовых методов (например, чтобы временно исключить падающий тест из запуска), но делают это по-разному.
JUnit 4 предлагает аннотацию @Ignore.
@Ignore
@Test
public void someTest() {
//code
}
В JUnit 5 есть @Disabled.
@Disabled
@Test
public void someTest() {
//code
}
TestNG использует @Test с атрибутом «enabled» со значением true или false.
@Test(enabled=false)
public void someTest() {
//code
}
Тестирование исключений
TestNG
@Test(expectedExceptions = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}
JUnit 4
@Test(expected = ArithmeticException.class)
public void divisionWithException() {
int i = 1/0;
}
В JUnit 5 для тестирования исключений можно использовать API assertThrows.
public class ExceptionExample1 {
@Test
void test_exception() {
Exception exception = assertThrows(
ArithmeticException.class,
() -> divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
assertTrue(exception.getMessage().contains("zero"));
}
int divide(int input, int divide) {
return input / divide;
}
}
Тайм-ауты
Тайм-аут обеспечивает ограничение по времени выполнение теста. Если лимит времени будет превышен – тест упадет. Для JUnit и TestNG синтаксис одинаковый.
@Test(timeout = 1000)
public void someTest()
{
//code
}
Параметризованные тесты
Параметризованные тесты могут быть полезны для запуска одного и того же теста с управляемым набором входных данных.
Для запуска параметризованных тестов в JUnit 4 используется комбинация @RunWith и @Parameters.
@RunWith(value = Parameterized.class)
public class JunitTest {
private int number;
public JunitTest(int number) {
this.number = number;
}
@Parameters
public static Collection<Object[]> data() {
Object[][] data = new Object[][] { { 1 }, { 2 }, { 3 }, { 4 } };
return Arrays.asList(data);
}
@Test
public void pushTest() {
System.out.println("Parameterized Number is : " + number);
}
}
В JUnit 5 тесты могут использовать данные из настроенного источника, для этого существует несколько
Например, @ValueSource в качестве параметров передает массив значений типа Short, Byte, Int, Long, Float, Double, Char или String методу тестирования.
@ParameterizedTest
@ValueSource(strings = {"value1", "value2"})
void someTest(String word) {
assertNotNull(word);
}
Аннотация @CsvSource использует значения CSV в качестве источника для параметров.
@ParameterizedTest
@CsvSource({ "1, Car", "2, House", "3, Train" })
void givenCSVSource_TestContent(int id, String word) {
assertNotNull(id);
assertNotNull(word);
}
Аннотация @CsvFileSource позволяет взять параметры из CSV-файла.
TestNG позволяет параметризовать тесты используя @Parameters или @DataProvider.
При использовании @Parameters, в аннотации нужно передать параметры.
public class TestsWithParameters {
@Test
@Parameters({"fruit", "sauce"})
public void parameterTest(String fruit, String sauce) {
System.out.println("Cocktail with " + fruit + " and " + sauce);
}
}
Значения параметров берутся из XML-файла.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parameterized_Suite">
<test name="parameterized_test_1">
<parameter name="fruit" value="mango"/>
<parameter name="sauce" value="yogurt"/>
<classes>
<class name="TestsWithParameters" />
</classes>
</test>
<test name="parameterized_test_2">
<parameter name="fruit" value="apples"/>
<parameter name="sauce" value="honey"/>
<classes>
<class name="TestsWithParameters" />
</classes>
</test>
</suite>
Для сложных типов данных, которые нельзя представить в виде String или примитивных типов (классы, структуры данных) можно использовать аннотацию @DataProvider. Она позволяет передавать в тест данные любого типа.
public class TestsWithDataProvider {
@DataProvider(name = "ingredients")
public static Object[][] smoothiesObject() {
return new Object[][]{{"mango", "yogurt"}, {"apples", "honey"}};
}
@Test(dataProvider = "ingredients")
public void makeSmoothies(String fruit, String sauce) {
System.out.println("Cocktail with " + fruit + " and " + sauce);
}
}
Зависимые тесты
TestNG поддерживает зависимые тесты, а JUnit – нет. Однако отсутствие поддержки зависимых тестов нельзя назвать недостатком JUnit, это скорее часть идеологии фреймворка, так как он ориентирован на независимое исполнение тестов.
@Test
public void authorization() {
//code
}
@Test(dependsOnMethods = {"authorization"})
public void writeMail() {
//code
}
@Test(dependsOnMethods = {"authorization"})
public void sendMail() {
//code
}
В примере выше для TestNG, если начальный тест не пройдет, то последующие зависимые тесты будут пропущены, а не упадут.
Порядок выполнения тестов
Зависимость тестов от порядка их выполнения так же считается плохой практикой, хотя возможно существуют ситуации, когда это оправдано.
Задавать порядок тестов можно и в TestNG и в JUnit.
В JUnit 4 предлагает использовать аннотацию @FixMethodOrder на уровне класса с указанием метода сортировки.
Метод MethodSorters.DEFAULT сравнивает методы тестирования с использованием их хэш-кодов. В случае коллизий используется лексикографический порядок.
@FixMethodOrder(MethodSorters.DEFAULT)
public class DefaultOrderOfExecutionTest {
private static StringBuilder output = new StringBuilder("");
@Test
public void secondTest() {
output.append("b");
}
@Test
public void thirdTest() {
output.append("c");
}
@Test
public void firstTest() {
output.append("a");
}
@AfterClass
public static void assertOutput() {
assertEquals(output.toString(), "cab");
}
}
Метод MethodSorters.NAME_ASCENDING выполнит тесты в их лексикографическом порядке – отсортировав по имени.
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class NameAscendingOrderOfExecutionTest {
// same as above
@AfterClass
public static void assertOutput() {
assertEquals(output.toString(), "abc");
}
}
Метод MethodSorters.JVM выполнит тесты в естественном порядке JVM, который может быть разным для каждого запуска.
По умолчанию JUnit 5 запускает тесты в непредсказуемом порядке. Для управления порядком выполнения тестов используется аннотация на уровне класса @TestMethodOrder с указанием метода сортировки.
В примере ниже порядок запуска тестов задается аннотацией @Order.
@TestMethodOrder(OrderAnnotation.class)
public class OrderAnnotationUnitTest {
private static StringBuilder output = new StringBuilder("");
@Test
@Order(1)
public void firstTest() {
output.append("a");
}
@Test
@Order(2)
public void secondTest() {
output.append("b");
}
@Test
@Order(3)
public void thirdTest() {
output.append("c");
}
@AfterAll
public static void assertOutput() {
assertEquals(output.toString(), "abc");
}
}
Используя метод сортировки Alphanumeric, можно запустить тесты в алфавитно-цифровом порядке (чувствительном к регистру) их названий.
@TestMethodOrder(Alphanumeric.class)
public class AlphanumericOrderUnitTest {
private static StringBuilder output = new StringBuilder("");
@Test
public void myATest() {
output.append("A");
}
@Test
public void myBTest() {
output.append("B");
}
@Test
public void myaTest() {
output.append("a");
}
@AfterAll
public static void assertOutput() {
assertEquals(output.toString(), "ABa");
}
}
Наконец, можно использовать собственный метод сортировки, реализовав интерфейс MethodOrderer. В примере ниже, переопределяется порядок запуска тестов на основе их имен в алфавитно-цифровом порядке без учета регистра.
public class CustomOrder implements MethodOrderer {
@Override
public void orderMethods(MethodOrdererContext context) {
context.getMethodDescriptors().sort(
(MethodDescriptor m1, MethodDescriptor m2)->
m1.getMethod().getName().compareToIgnoreCase(m2.getMethod().getName()));
}
}
@TestMethodOrder(CustomOrder.class)
public class CustomOrderUnitTest {
// ...
@AfterAll
public static void assertOutput() {
assertEquals(output.toString(), "AaB");
}
}
В TestNG порядок выполнения тестов задается параметром priority в аннотации @Test.
@Test(priority = 1)
public void someTest() {
//code
}
@Test(priority = 2)
public void anotherTest() {
//code
}
Пользовательское описание тестов
Тестам можно присвоить человекопонятное имя для JUnit 5 или описание для TestNG, что позволяет легче читать код тестов и результаты тестирования. В JUnit 4 такой фичи нет.
JUnit 5 предоставляет аннотацию @DisplayName. При запуске теста имя тестового метода будет выводиться в консоль.
@Test
@DisplayName("Some Test Method")
void someTest(String word) {
//code
}
В TestNG описание можно указать в аннотации @Test через атрибут description.
@Test(description = "Some Test Method")
public void someTest() {
//code
}
Отчеты
В TestNG включен функционал генерации отчетов. После каждого запуска тестов формируется html документ с результатами (количество пройденных, упавших и скипнутых тестов, время выполнения) и логом. Данные так же хранятся в XML и их можно использовать в собственном шаблоне отчета.
В JUnit данные с результатами выполнения также доступны в XML, но готовых отчетов нет, поэтому придется полагаться на дополнительные плагины, однако недостатка в них нет.
Параллельное выполнение тестов
Есть распространённое заблуждение о том, что JUnit 5 в сравнении TestNG не поддерживает параллельное выполнение тестов. Это не так. В JUnit 5 указанный функционал существует с версии 5.3 (июнь 2018). На момент написания статьи (апрель 2021) он все еще числится в списке экспериментальных фичей, но работает стабильно.
JUnit 4 параллелизацию не поддерживает.
Как подключить к проекту
Номер версии в конфигах ниже приведен для примера, при использовании не забывайте указывать актуальную.
Как подключить к проекту TestNG если используете Maven
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.6.0</version>
<scope>test</scope>
</dependency>
Как подключить к проекту TestNG если используете Gradle
dependencies {
testCompile 'org.testng:testng:7.6.0'
}
Как подключить к проекту JUnit если используете Maven
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
Как подключить к проекту JUnit если используете Gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
Заключение
Адепты TestNG называют свой инструмент «продвинутым». Приверженцы JUnit ругают TestNG за риск зависимых тестов.
У обоих фреймворков примерно одинаковый функционал. Есть небольшие различия в реализации, а также, что еще менее существенно, в названии аннотаций.
Сейчас выбор между JUnit 5 или TestNG – это скорее личные предпочтения, либо уже используемый на проекте стек технологий. Чего точно не стоит делать, так это переписывать тесты с одного фреймворка на другой.
Однако JUnit 5 гораздо более распространен чем TestNG и считается фактически стандартом индустрии.
Тем временем JUnit 4 сдает позиции, хоть и относительно популярный. Тесты на JUnit 4 в шутку называют не legacy, а vintage (на самом деле Vintage – это название модуля JUnit 5 для обратной совместимости и запуска тестов 4 версии). На новых проектах его использовать не нужно.
Полезные ссылки
На этом всё. Но вы можете поддержать проект. Даже небольшая сумма поможет нам писать больше полезных статей.
Если статья помогла или понравилась, пожалуйста поделитесь ей в соцсетях.