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
, если нам нужна какая-либо конфигурация до или после тестовых случаев, включенных в тег <test>
в 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, но они делают это по-разному.
Мы можем использовать аннотации @Suite,
@SelectPackages
и @SelectClasses
для группировки тестовых случаев и запускать их как пакет в JUnit 5
. Набор — это набор тестовых случаев, которые мы можем сгруппировать и запустить как один тест.
Если мы хотим сгруппировать тестовые примеры разных пакетов для совместного запуска в Suite
, нам понадобится аннотация @SelectPackages
:
@Suite
@SelectPackages({ "org.foreach.java.suite.childpackage1", "org.foreach.java.suite.childpackage2" })
public class SelectPackagesSuiteUnitTest {
}
Если мы хотим, чтобы определенные тестовые классы выполнялись вместе, JUnit 5
обеспечивает гибкость через @SelectClasses
:
@Suite
@SelectClasses({Class1UnitTest.class, Class2UnitTest.class})
public class SelectClassesSuiteUnitTest {
}
Ранее, используя JUnit 4
, мы группировали и запускали несколько тестов вместе, используя аннотации @RunWith
и @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.foreach.RegistrationTest" />
<class name="com.foreach.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.foreach.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-файла аннотируйте тестовый метод с помощью параметра @:
@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="foreach.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 также предоставляет несколько способов контролировать порядок выполнения тестовых методов. Мы предоставляем параметр 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 у нас может быть взаимозависимый тест, в котором порядок выполнения должен быть одинаковым для каждого запуска теста. Для этого мы должны использовать параметр 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 .