JUnit vs TestNG

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 4JUnit 5TestNG
Аннотация теста@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 тесты могут использовать данные из настроенного источника, для этого существует несколько source-аннотаций.

Например, @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 называют свой инструмент «продвинутым». Приверженцы JUnit ругают TestNG за риск зависимых тестов.

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

Сейчас выбор между JUnit 5 или TestNG – это скорее личные предпочтения, либо уже используемый на проекте стек технологий. Чего точно не стоит делать, так это переписывать тесты с одного фреймворка на другой.

Тем временем JUnit 4 сдает позиции, хоть и относительно популярный. Тесты на JUnit 4 в шутку называют не legacy, а vintage (на самом деле Vintage – это название модуля JUnit 5 для обратной совместимости и запуска тестов 4 версии). На новых проектах его использовать не нужно.

Полезные ссылки

Страница по JUnit 4
Официальный сайт JUnit 5
Официальный сайт TestNG
JUnit pain

На этом всё. Но вы можете помочь проекту. Даже небольшая сумма поможет нам писать больше полезных статей.

Если статья помогла или понравилась, пожалуйста поделитесь ей в соцсетях.