Привет, Хабр. Для будущих студентов курса "Java QA Automation Engineer" и всех интересующихся темой тестирования подготовили перевод материала.
Также приглашаем посетить открытый вебинар «Паттерн PageObject». Чтобы автотесты не приходилось перечитывать и рефакторить, нужно сразу продумывать их архитектуру. Тут на помощь приходят паттерны. Участники вебинара вместе с экспертом познакомятся с самым популярным из них.
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 для конфигураций на уровне набора (suite) и группы:
@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, если нам нужна какая-либо конфигурация до или после тест-кейсов, включенных в тег в 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, но они делают это по-разному.
В JUnit 5 мы можем использовать аннотации @RunWith, @SelectPackages и @SelectClasses для группировки тест-кейсов и запуска их как набора. Набор — это коллекция тест-кейсов, которые мы можем сгруппировать и запустить как единый тест.
Если мы хотим сгруппировать тест-кейсы различных пакетов для запуска вместе в Suite нам нужно использовать аннотацию @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>
Будет выполнен тестовый метод с тегом группы 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 для тестирования исключений мы можем использовать 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 предоставляет несколько 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
— передает (passes) внешние методы, генерирующие потоки:
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-файл из classpath, и @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 также предоставляет несколько способов управления порядком выполнения тестовых методов. Мы предоставляем параметр 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);
}
Обратите внимание, что priority
вызывает методы тестирования на основе приоритета, но не гарантирует, что тесты на одном уровне будут завершены до вызова со следующего уровня приоритета.
Иногда при написании функциональных тестовых примеров в 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.
Узнать подробнее о курсе "Java QA Automation Engineer".
Смотреть открытый вебинар «Паттерн PageObject».