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 4 | JUnit 5 | TestNG | |
Аннотация теста | @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 всегда должны быть статическими, потому что выполняются до создания экземпляра тестового класса. Однако методы с @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 если используете Maven
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.6.0</version>
<scope>test</scope>
</dependency>
Как подключить к проекту TestNG если используете Gradle
dependencies {
testCompile 'org.testng:testng:7.6.0'
}
Как подключить к проекту JUnit если используете Maven
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
Как подключить к проекту JUnit если используете Gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
Заключение
Адепты TestNG называют свой инструмент «продвинутым». Приверженцы JUnit ругают TestNG за риск зависимых тестов.
У обоих фреймворков примерно одинаковый функционал. Есть небольшие различия в реализации, а также, что еще менее существенно, в названии аннотаций.
Сейчас выбор между JUnit 5 или TestNG – это скорее личные предпочтения, либо уже используемый на проекте стек технологий. Чего точно не стоит делать, так это переписывать тесты с одного фреймворка на другой.
Однако JUnit 5 гораздо более распространен чем TestNG и считается фактически стандартом индустрии.
Тем временем JUnit 4 сдает позиции, хоть и относительно популярный. Тесты на JUnit 4 в шутку называют не legacy, а vintage (на самом деле Vintage – это название модуля JUnit 5 для обратной совместимости и запуска тестов 4 версии). На новых проектах его использовать не нужно.
Полезные ссылки
Страница по JUnit 4
Официальный сайт JUnit 5
Официальный сайт TestNG
JUnit pain
На этом всё. Но вы можете поддержать проект. Даже небольшая сумма поможет нам писать больше полезных статей.
Если статья помогла или понравилась, пожалуйста поделитесь ей в соцсетях.