«1. Обзор

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

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

2. Настройка теста

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

JUnit предлагает инициализацию и очистку на двух уровнях, до и после каждого метода и класса. У нас есть аннотации @BeforeEach, @AfterEach на уровне метода и @BeforeAll и @AfterAll на уровне класса:

public class SummationServiceTest {

    private static List<Integer> 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 заменяют @BeforeClass и @AfterClass JUnit 4.

Подобно JUnit, TestNG также обеспечивает инициализацию и очистку на уровне методов и классов. В то время как @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 и @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, если нам нужна какая-либо конфигурация до или после тестовых случаев, включенных в тег \u003ctest\u003e в XML-файле конфигурации TestNG:

<test name="test setup">
    <classes>
        <class name="SummationServiceTest">
            <methods>
                <include name="givenNumbers_sumEquals_thenCorrect" />
            </methods>
        </class>
    </classes>
</test>

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

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

Оба фреймворка поддерживают игнорирование тестовых случаев, хотя делают это совершенно по-разному. 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, но делают они это по-разному.

Мы можем использовать аннотации @RunWith, @SelectPackages и @SelectClasses для группировки тестовых случаев и запускать их как набор в JUnit 5. Набор — это набор тестовых случаев, которые мы можем сгруппировать вместе и запустить как один тест. .

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

@RunWith(JUnitPlatform.class)
@SelectPackages({ "org.baeldung.java.suite.childpackage1", "org.baeldung.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-файла:

<suite name="suite">
    <test name="test suite">
        <classes>
            <class name="com.baeldung.RegistrationTest" />
            <class name="com.baeldung.SignInTest" />
        </classes>
    </test>
</suite>

Это указывает, что 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 для выполнения групп:

<test name="test groups">
    <groups>
        <run>
            <include name="regression" />
        </run>
    </groups>
    <classes>
        <class 
          name="com.baeldung.SummationServiceTest" />
    </classes>
</test>

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

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 мы можем использовать API assertThrows для проверки исключений:

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

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

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

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

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

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

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

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

    @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 — передает внешние методы, генерирующие потоки:
static Stream<String> 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, чтобы использовать обозначения значений параметров для модульного теста.

В TestNG мы можем параметризовать тесты, используя аннотации @Parameter или @DataProvider. При использовании XML-файла аннотируйте тестовый метод с помощью @Parameter:

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

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

<suite name="My test suite">
    <test name="numbersXML">
        <parameter name="value" value="1"/>
        <parameter name="isEven" value="false"/>
        <classes>
            <class name="baeldung.com.ParametrizedTests"/>
        </classes>
    </test>
</suite>

Хотя использование информации в 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. Порядок выполнения теста

Нет определенного неявного порядка в которых методы тестирования будут выполняться в JUnit 4 или 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 и MethodSorter.JVM.

В то время как TestNG также предоставляет несколько способов контролировать порядок выполнения тестовых методов. Мы предоставляем параметр приоритета в аннотации @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 у нас может быть взаимозависимый тест, в котором порядок выполнения должен быть одинаковым для каждого запуска теста. Для этого мы должны использовать параметр dependOnMethods для аннотации @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.