Сравнение Quick JUnit и TestNG

Быстрое сравнение JUnit с TestNG

1. обзор

JUnit и TestNG, несомненно, являются двумя наиболее популярными средами модульного тестирования в экосистеме Java. Хотя JUnit вдохновляет сам TestNG, он предоставляет свои отличительные особенности и, в отличие от JUnit, работает для функционального и более высокого уровней тестирования.

В этом постеwe’ll discuss and compare these frameworks by covering their features and common use cases.

2. Испытательная установка

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

JUnit offers initialization and cleanup at two levels, before and after each method and class. У нас есть аннотации@BeforeEach,@AfterEach на уровне метода и@BeforeAll и@AfterAll на уровне класса:

public class SummationServiceTest {

    private static List numbers;

    @BeforeAll
    public static void initialize() {
        numbers = new ArrayList<>();
    }

    @AfterAll
    public static void tearDown() {
        numbers = null;
    }

    @BeforeEach
    public void runBeforeEachTest() {
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
    }

    @AfterEach
    public void runAfterEachTest() {
        numbers.clear();
    }

    @Test
    public void givenNumbers_sumEquals_thenCorrect() {
        int sum = numbers.stream().reduce(0, Integer::sum);
        assertEquals(6, sum);
    }
}

Обратите внимание, что в этом примере используется JUnit 5. В предыдущей версии JUnit 4 нам нужно было бы использовать аннотации@Before и@After, которые эквивалентны@BeforeEach и @AfterEach.  Аналогично,@BeforeAll и@AfterAll) s заменяет JUnit 4@BeforeClass и@AfterClass.

Аналогично JUnit,TestNG also provides initialization and cleanup at the method and class level. Хотя@BeforeClass и@AfterClass остаются неизменными на уровне класса, аннотациями уровня метода являются @BeforeMethod и@AfterMethod:

@BeforeClass
public void initialize() {
    numbers = new ArrayList<>();
}

@AfterClass
public void tearDown() {
    numbers = null;
}

@BeforeMethod
public void runBeforeEachTest() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterMethod
public void runAfterEachTest() {
    numbers.clear();
}

TestNG также предлагает аннотации@BeforeSuite, @AfterSuite, @BeforeGroup and @AfterGroup для конфигураций на уровне набора и группы:

@BeforeGroups("positive_tests")
public void runBeforeEachGroup() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterGroups("negative_tests")
public void runAfterEachGroup() {
    numbers.clear();
}

Кроме того, мы можем использовать@BeforeTest и @AfterTest, если нам нужна какая-либо конфигурация до или после тестовых случаев, включенных в тег<test> в файле конфигурации TestNG XML:


    
        
            
                
            
        
    

Обратите внимание, что объявление методов@BeforeClass и@AfterClass должно быть статическим в JUnit. Для сравнения, объявление метода TestNG не имеет этих ограничений.

3. Игнорирование тестов

Both frameworks support ignoring test cases, хотя они делают это по-другому. JUnit предлагает аннотацию@Ignore:

@Ignore
@Test
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

в то время как TestNG использует@Test с параметром «enabled» с логическим значениемtrue илиfalse:

@Test(enabled=false)
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream.reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

4. Совместное выполнение тестов

Совместное выполнение тестов в виде коллекции возможно как вJUnit, так и в TestNG, но они делают это по-разному.

We can use the @RunWith, @SelectPackages, and @SelectClasses annotations to group test cases and run them as a suite in JUnit 5. Набор - это набор тестовых примеров, которые мы можем сгруппировать и запустить как единый тест.

Если мы хотим сгруппировать тестовые примеры разных пакетов для совместной работы вSuite we, нужна аннотация@SelectPackages:

@RunWith(JUnitPlatform.class)
@SelectPackages({ "org.example.java.suite.childpackage1", "org.example.java.suite.childpackage2" })
public class SelectPackagesSuiteUnitTest {

}

Если мы хотим, чтобы определенные тестовые классы работали вместе,JUnit 5 обеспечивает гибкость через@SelectClasses:

@RunWith(JUnitPlatform.class)
@SelectClasses({Class1UnitTest.class, Class2UnitTest.class})
public class SelectClassesSuiteUnitTest {

}

Ранее, используяJUnit 4, мы добились группировки и одновременного запуска нескольких тестов, используя саннотацию@Suite :

@RunWith(Suite.class)
@Suite.SuiteClasses({ RegistrationTest.class, SignInTest.class })
public class SuiteTest {

}

В TestNG мы можем группировать тесты с помощью файла XML:


    
        
            
            
        
    

Это означает, чтоRegistrationTest иSignInTest будут работать вместе.

Помимо группировки классов, TestNG также может группировать методы с помощью аннотации @Test(groups=”groupName”):

@Test(groups = "regression")
public void givenNegativeNumber_sumLessthanZero_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertTrue(sum < 0);
}

Давайте использовать XML для выполнения групп:


    
        
            
        
    
    
        
    

Это выполнит метод тестирования, помеченный группойregression.

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

Функция тестирования исключений с использованием аннотаций доступна как в JUnit, так и в TestNG.

Давайте сначала создадим класс с методом, который генерирует исключение:

public class Calculator {
    public double divide(double a, double b) {
        if (b == 0) {
            throw new DivideByZeroException("Divider cannot be equal to zero!");
        }
        return a/b;
    }
}

ВJUnit 5 мы можем использоватьassertThrows API для проверки исключений:

@Test
public void whenDividerIsZero_thenDivideByZeroExceptionIsThrown() {
    Calculator calculator = new Calculator();
    assertThrows(DivideByZeroException.class, () -> calculator.divide(10, 0));
}

ВJUnit 4, we можно добиться этого, используя@Test(expected = DivideByZeroException.class) в тестовом API.

И с TestNG мы также можем реализовать то же самое:

@Test(expectedExceptions = ArithmeticException.class)
public void givenNumber_whenThrowsException_thenCorrect() {
    int i = 1 / 0;
}

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

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

Параметризованные модульные тесты полезны для тестирования одного и того же кода при нескольких условиях. С помощью параметризованных модульных тестов мы можем настроить метод тестирования, который получает данные из некоторого источника данных. Основная идея заключается в том, чтобы сделать метод модульного тестирования многоразовым и провести тестирование с другим набором входных данных.

In JUnit 5, we have the advantage of test methods consuming data arguments directly from the configured source. По умолчанию JUnit 5 предоставляет несколько аннотацийsource, например:

  • @ValueSource:, мы можем использовать это с массивом значений типаShort, Byte, Int, Long, Float, Double, Char, иString:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void givenString_TestNullOrNot(String word) {
    assertNotNull(word);
}
  • @EnumSource – выделяетEnum констант в качестве параметров метода испытаний:

@ParameterizedTest
@EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"})
void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) {
    assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit));
}
  • @MethodSource – pклассифицирует внешние методы, генерирующие потоки:

static Stream wordDataProvider() {
    return Stream.of("foo", "bar");
}

@ParameterizedTest
@MethodSource("wordDataProvider")
void givenMethodSource_TestInputStream(String argument) {
    assertNotNull(argument);
}
  • @CsvSource – использует значения CSV в качестве источника для параметров:

@ParameterizedTest
@CsvSource({ "1, Car", "2, House", "3, Train" })
void givenCSVSource_TestContent(int id, String word) {
    assertNotNull(id);
    assertNotNull(word);
}

Точно так же у нас есть другие источники, такие как@CsvFileSource , если нам нужно прочитать файл CSV из пути к классам, и@ArgumentSource, чтобы указать настраиваемый, повторно используемыйArgumentsProvider.

ВJUnit 4 тестовый класс должен быть аннотирован@RunWith, чтобы сделать его параметризованным классом, и@Parameter, чтобы использовать обозначения значений параметров для модульного теста.

In TestNG, we can parametrize tests using @Parameter or @DataProvider annotations. При использовании файла XML аннотируйте метод тестирования с помощью @Parameter:

@Test
@Parameters({"value", "isEven"})
public void
  givenNumberFromXML_ifEvenCheckOK_thenCorrect(int value, boolean isEven) {
    Assert.assertEquals(isEven, value % 2 == 0);
}

и предоставить данные в файле XML:


    
        
        
        
            
        
    

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

Для этого мы можем использовать аннотацию@DataProvider, которая позволяет нам отображать сложные типы параметров для методов тестирования.

Вот пример использования@DataProvider для примитивных типов данных:

@DataProvider(name = "numbers")
public static Object[][] evenNumbers() {
    return new Object[][]{{1, false}, {2, true}, {4, true}};
}

@Test(dataProvider = "numbers")
public void givenNumberFromDataProvider_ifEvenCheckOK_thenCorrect
  (Integer number, boolean expected) {
    Assert.assertEquals(expected, number % 2 == 0);
}

И@DataProvider  для объектов:

@Test(dataProvider = "numbersObject")
public void givenNumberObjectFromDataProvider_ifEvenCheckOK_thenCorrect
  (EvenNumber number) {
    Assert.assertEquals(number.isEven(), number.getValue() % 2 == 0);
}

@DataProvider(name = "numbersObject")
public Object[][] parameterProvider() {
    return new Object[][]{{new EvenNumber(1, false)},
      {new EvenNumber(2, true)}, {new EvenNumber(4, true)}};
}

Таким же образом любые конкретные объекты, которые должны быть проверены, могут быть созданы и возвращены с использованием поставщика данных. Это полезно при интеграции с такими фреймворками, как Spring.

Обратите внимание, что в TestNG, поскольку метод@DataProvider не обязательно должен быть статическим, мы можем использовать несколько методов поставщика данных в одном тестовом классе.

7. Тайм-аут теста

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

@Test
public void givenExecution_takeMoreTime_thenFail() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(10000));
}

ВJUnit 4 и TestNG мы можем провести тот же тест, используя @Test(timeout=1000)

@Test(timeOut = 1000)
public void givenExecution_takeMoreTime_thenFail() {
    while (true);
}

8. Зависимые тесты

TestNG поддерживает тестирование зависимостей. Это означает, что в наборе методов тестирования, если первоначальный тест не пройден, все последующие зависимые тесты будут пропущены, а не помечены как неудачные, как в случае с JUnit.

Давайте рассмотрим сценарий, в котором нам нужно проверить электронную почту, и в случае успеха мы перейдем к входу в систему:

@Test
public void givenEmail_ifValid_thenTrue() {
    boolean valid = email.contains("@");
    Assert.assertEquals(valid, true);
}

@Test(dependsOnMethods = {"givenEmail_ifValid_thenTrue"})
public void givenValidEmail_whenLoggedIn_thenTrue() {
    LOGGER.info("Email {} valid >> logging in", email);
}

9. Порядок выполнения теста

There is no defined implicit order in which test methods will get executed in JUnit 4 or TestNG. Методы просто вызываются в том виде, в каком они возвращены Java Reflection API. Начиная с JUnit 4 он использует более детерминированный, но не предсказуемый порядок.

Чтобы иметь больший контроль, мы аннотируем тестовый класс аннотацией@FixMethodOrder и упомянем сортировщик методов:

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SortedTests {

    @Test
    public void a_givenString_whenChangedtoInt_thenTrue() {
        assertTrue(
          Integer.valueOf("10") instanceof Integer);
    }

    @Test
    public void b_givenInt_whenChangedtoString_thenTrue() {
        assertTrue(
          String.valueOf(10) instanceof String);
    }

}

ПараметрMethodSorters.NAME_ASCENDING сортирует методы по имени метода в лексикографическом порядке. Помимо этого сортировщика у нас естьMethodSorter.DEFAULT and MethodSorter.JVM as well.

В то время как TestNG также предоставляет несколько способов контроля в порядке выполнения метода тестирования. Мы предоставляем параметрpriority в аннотации@Test:

@Test(priority = 1)
public void givenString_whenChangedToInt_thenCorrect() {
    Assert.assertTrue(
      Integer.valueOf("10") instanceof Integer);
}

@Test(priority = 2)
public void givenInt_whenChangedToString_thenCorrect() {
    Assert.assertTrue(
      String.valueOf(23) instanceof String);
}

Обратите внимание, что приоритет вызывает методы тестирования, основанные на приоритете, но не гарантирует, что тесты на одном уровне завершены до вызова следующего уровня приоритета.

Иногда при написании функциональных тестовых примеров в TestNG у нас может быть взаимозависимый тест, в котором порядок выполнения должен быть одинаковым для каждого запуска теста. Для этого мы должны использовать параметрdependsOnMethods для аннотации @Test, как мы видели в предыдущем разделе.

10. Название пользовательского теста

По умолчанию всякий раз, когда мы запускаем тест, класс теста и имя метода теста печатаются в консоли или IDE. JUnit 5 предоставляет уникальную возможность, в которой мы можем упоминать настраиваемые описательные имена для классов и методов тестирования, используя аннотацию@DisplayName.

Эта аннотация не дает никаких преимуществ при тестировании, но дает возможность легко прочитать и понять результаты тестирования даже для нетехнического специалиста:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
@DisplayName("Test Method to check that the inputs are not nullable")
void givenString_TestNullOrNot(String word) {
    assertNotNull(word);
}

Всякий раз, когда мы запускаем тест, в выводе будет отображаться отображаемое имя вместо имени метода.

Прямо сейчас вTestNG нет возможности указать собственное имя.

11. Заключение

И JUnit, и TestNG являются современными инструментами для тестирования в экосистеме Java.

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

Реализации всех фрагментов кода можно найти вTestNG иjunit-5 Github project.