10 интересных нововведений в JUnit 5

  • Tutorial
В минувшее воскресенье Sam Brannen анонсировал выход JUnit 5! Ура!


Поздравляю всех участников @JUnitTeam а также всех, кто использует JUnit в своей работе! Давайте посмотрим, что же нам приготовили в этом релизе.

Содержание


0. Введение
1. Начало работы
2. Обзор нововведений
   2.1. public — всё
   2.2. Продвинутый assert
   2.3. Работа с исключениями
   2.4. Новый Test
   2.5. Новые базовые аннотации
   2.6. Вложенные классы
   2.7. Разделяемый инстанс класса для запуска тестов
   2.8. Автоматический повторный запуск теста
   2.9. Параметризированные тесты
   2.10. Аннотированные default методы в интерфейсах
3. Заключение

1. Введение


Итак, официальный сайт начинает с того, что сообщает нам о новом строении JUnit:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage (← офф.сайт).

JUnit Platform — фундаментальная основа для запуска на JVM фреймворков для тестирования. Платформа предоставляет TestEngine API, для разработки фреймворков (для тестирования), которые могут быть запущены на платформе. Кроме этого, в платформе имеется Console Launcher для запуска платформы из коммандной строки а также для запуска любого JUnit 4 Runner'а на платформе. Уже, кстати, есть плагины для Gradle и Maven.

JUnit Jupiter — сердце JUnit 5. Этот проект предоставляет новые возможности для написания тестов и создания собственных расширений. В проекте реализован специальный TestEngine для запуска тестов на ранее описанной платформе.

JUnit Vintage — поддержка легаси. Определяется TestEngine для запуска тестов ориентированных на JUnit 3 и JUnit 4.

1. Начало работы


В интернете уже полно примеров для настройки Gradle и Maven проектов. В блоге JetBrains есть отдельный пост, посвященный настройке JUnit 5 в IDEA.

2. Обзор нововведений


А теперь наконец-то перейдем к примерам!

2.1. public — всё
JUnit больше не требует, чтобы методы были публичными.

@Test
void test() {
    assertEquals("It " + " works!" == "It works!");
}


2.2. Продвинутый assert
Опциональное сообщение сделали последним аргументом.

assertEquals(2017, 2017, "The optional assertion message is now the last parameter.");

В пятой версии для конструирования сообщения можно использовать Supplier<String>.

assertTrue("habr" == "habr", () -> "Assertion messages can be lazily evaluated");

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

// в группе все ассерты исполняются независимо,
// успех - когда прошли успешно все ассерты
assertAll("habr",
    () -> assertThat("https://habrahabr.ru", startsWith("https")),
    () -> assertThat("https://habrahabr.ru", endsWith(".ru"))
);

Появился метод для работы с Iterable.

assertIterableEquals(asList(1, 2, 3), asList(1, 2, 3));

Добавили интересный метод для сравнения набора строк. Поддерживаются регулярные выражения!

Assertions.assertLinesMatch(
    asList("можно сравнивать строки", "а можно по regex: \\d{2}\\.\\d{2}\\.\\d{4}"),
    asList("можно сравнивать строки", "а можно по regex: 12.09.2017")
);

2.3. Работа с исключениями
Работа с исключениями стала более линейной.

Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
    throw new IllegalArgumentException("что-то пошло не так");
});

assertEquals("что-то пошло не так", exception.getMessage());

2.4. Новый Test
JUnit 5 привнес новую аннотацию Test, которая находится в пакете org.junit.jupiter.api.Test. В отличии от четвертой версии, новая аннотация служит исключительно маркером.

Посмотреть различия
// JUnit 4
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Test {
    Class<? extends Throwable> expected() default Test.None.class;

    long timeout() default 0L;

    public static class None extends Throwable {
        private static final long serialVersionUID = 1L;

        private None() {
        }
    }
}

Новая аннотация выглядит так.

// JUnit 5
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(Stable)
@Testable
public @interface Test {
}

2.5. Новые базовые аннотации
В пятой версии добавили новые базовые аннотации.

Посмотреть хороший пример.
import static org.junit.jupiter.api.Assertions.fail;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class StandardTests {

    // вместо @BeforeClass
    @BeforeAll
    static void initAll() {
    }

    // вместо @Before
    @BeforeEach
    void init() {
    }

    @Test
    void succeedingTest() {
    }

    @Test
    void failingTest() {
        fail("a failing test");
    }

    // Вместо @Ignore
    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    // Новая аннотация для улучшения читаемости при выводе результатов тестов.
    @DisplayName("╯°□°)╯")
    void testWithDisplayNameContainingSpecialCharacters() {}

    // вместо @After
    @AfterEach
    void tearDown() {
    }

    // вместо @AfterClass
    @AfterAll
    static void tearDownAll() {
    }

}

2.6. Вложенные классы
Аннотация @Nested позволяет использовать внутренние классы при разработке тестов, что позволяет иногда более удобным способом группировать/дополнять тесты.

Пример из официальной документации.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        @DisplayName("throws EmptyStackException when popped")
        void throwsExceptionWhenPopped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        @DisplayName("throws EmptyStackException when peeked")
        void throwsExceptionWhenPeeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when peeked but remains not empty")
            void returnElementWhenPeeked() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

2.7. Разделяемый инстанс класса для запуска тестов
Для гарантии независимости и изоляциии тестов JUnit во всех предыдущих версиях всегда создавал по инстансу на тест (т.е. на каждый запуск метода отдельный инстанс). В пятой версии такое поведение можно изменить используя новую аннотацию @TestInstance(Lifecycle.PER_CLASS). В таком случае инстанс будет создан только один раз и будет переиспользован для запуска всех тестов, определенных внутри этого класса.

2.8. Автоматический повторный запуск теста
Еще одна приятная добавка! Аннотация @RepeatedTest сообщает JUnit, что данный тест нужно запустить несколько раз. При этом, каждый такой вызов будет независимым тестом, а значит для него будут работать аннотации @BeforeAll, @BeforeEach, @AfterEach и @AfterAll.

@RepeatedTest(5)
void repeatedTest() {
    System.out.println("Этот тест будет запущен пять раз. ");
}

Стоит отметить, что можно настроить дополнительный вывод информации о запусках теста. Например, показывать номер запуска. За это отвечают специальные константы определенные внутри этой же аннотации.

2.9. Параметризированные тесты
Параметризированные тесты позволяют запускать тест несколько раз с различными входными данными. На данный момент поддерживаются только данные примитивных типов: int, long, double, String. Но не стоит отчаиваться! JUnit 5 определяет несколько дополнительных аннотаций для указания источника данных для параметризированных тестов. Итак, начнём!

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
void testWithStringParameter(String argument) {
    assertNotNull(argument);
}

Еще один вдохновляющий пример с @ValueSource.

@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithConverter(@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate date) {
    assertEquals(2017, date.getYear());
}

Пример с разбором CSV.

@ParameterizedTest
@CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
// или даже так: @CsvFileSource(resources = "/two-column.csv")
void testWithCsvSource(String first, int second) {
    assertNotNull(first);
    assertNotEquals(0, second);
}

Пример с Enum.

@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

Пример с источником данных.

@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}

static class MyArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("foo", "bar").map(Arguments::of);
    }
}

Еще больше крутых примеров можно найти на официальном сайте в разделе 3.13. Parameterized Tests.

2.10. Аннотированные default методы в интерфейсах
JUnit теперь умеет работать с default методами в интерфейсах! Вот один из официальных примеров применения этого нововведения. Предлагаю посмотреть интересный пример с Equals Contract.

public interface Testable<T> {
    T createValue();
}

public interface EqualsContract<T> extends Testable<T> {

    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertFalse(value.equals(null));
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }

}

Заключение


Очень здорово, что популярный фреймворк для тестирования решается на такие серьезные эксперименты с API и старается идти в ногу со временем!

Напоследок оставлю парочку ссылок: официальный сайт JUnit 5 и очень дружелюбное руководство.

Еще много чего интересного осталось за рамками этой статьи. Например, отдельного обзора заслуживает механизм расширений, предоставляемый JUnit 5.
Спасибо за внимание!

Happy coding!
Планируете миграцию на JUnit 5?

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

Метки:
  • +20
  • 6,1k
  • 9
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 9
  • 0
    JUnit молодцы, что развивают свой фреймфорк. Пока читал статью, во многом создавалось впечатление, что читаю про фишки TestNG, которые использовал ранее в своем проекте, но есть и новое, чего в нем не видел. Ну и о наболевшем: в JUnit не могу понять разработчиков, почему в методах типа assertEquals у них такой порядок аргументов: «эталон(1) сравнивают с полученным значением(2)», а не «полученное значение(1) сравнивают с эталоном(2)». В реальной жизни обычно происходит по второму сценарию.
    • 0
      Я всегда пишу if (42 == x) {… в коде
      • 0
        Мне так IntelliJ IDEA рекомендует.
    • +1

      Замечу, что в JUnit 5 пока еще не нет эквивалентов для некоторых вещей из JUnit 4.


      Например нет timeout для тестов (есть только возможность проверить, выполнился ли блок кода за указанное время) — это пока только в планах. Нет эквивалента @Rule Timeout (возможность задать дэфолтный timeout для тестов) и текущее API для extensions не позволяет сделать его аналог (а в JUnit 4 — такое можно было сделать руками).


      Так что если у вас в тестах используются timeout-ы или @Rule хоть в каком-то виде, то есть вероятность, что смигрировать на JUnit 5 без потерь не получится.

      • 0
        Ну так выполнился ли блок кода за указанное время — это же и есть timeout для тестов. Просто весь код теста положить в assertTimeout и будет тоже самое.
        • +1

          Нет, совсем не то же самое. Допустим у вас блок кода по факту работает 5 минут, а на тесте вы выставили таймаут в 10 секунд.


          В JUnit 4 вы через 10 секунд получите таймаут, а thread с тестом будет остановлен. Полное время выполнения теста — около 10 секунд.


          В JUnit 5 тест отработает все 5 минут и уже после этого будет отмечен как неудачный. Полное время работы теста — 5 минут.


          Еще хуже, если у вас тест иногда виснет (и как раз по этой причине вы выставили таймаут) — в JUnit 5 такой тест никогда не остановится.

          • 0

            Отмечу, что проблему для одного теста можно решить используя assertTimeoutPreemptively. Но это не подходит, если вы хотите задать таймаут по-умолчанию — ведь придётся завернуть в лямбду тело каждого теста, что "совсем не круто".

      • 0
        Жаль, @BeforeAll/@AfterAll так и оставили статическими. Значит как примитивная запускалка и JUnit4 сойдет, а для всего остального как и раньше TestNG.
        • 0
          Если вы укажите над классом @TestInstance(Lifecycle.PER_CLASS) то вы можете не делать @BeforeAll/@AfterAll статическими. Это же работает и для Котлина.

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