Миграция с JUnit4 на JUnit5: важные отличия и преимущества

Введение
JUnit5 — это мощное и гибкое обновление фреймворка JUnit, которое предоставляет множество улучшений и новых функций для написания тестов. Обновление текущей версии фреймворка до JUnit5 является быстрой и несложной процедурой: просто обновите зависимости проекта и начните использовать новые функции.
При этом, если вы достаточно давно пользуетесь JUnit4, то миграция уже написанных тестов может показаться довольно сложной задачей. Но есть и хорошая новость, если все же вам не нужно конвертировать какие-либо тесты под новую версию фреймворка, то тесты, написанные под JUnit4, будут запускаться и для JUnit5 с помощью библиотеки Vintage.
Ниже приведены четыре достаточно веских причины начать писать новые тесты в JUnit 5:
  • JUnit5 использует функционал Java 8 или более поздних релизов, например лямбда-функции, делая тесты более мощными и простыми в поддержке
  • JUnit5 добавил несколько очень полезных новых функций для описания, организации и выполнения тестов. Например, тесты получили улучшенные видимые имена и могут быть организованы иерархически
  • JUnit5 представляет собой несколько библиотек, поэтому в проект импортируются только те функции, которые вам нужны. С такими системами сборки как Maven и Gradle очень просто подключить нужные библиотеки
  • JUnit5 может использовать более одного процесса тестирования за раз, чего нельзя сделать в JUnit4 (одновременно может использоваться только один процесс — runner). Это означает, что вы можете легко комбинировать расширение Spring с другими расширениями (например, написанными вами)
Переключиться с JUnit4 на JUnit5 довольно просто, даже если у вас уже есть тесты в JUnit4. Большинству организаций не требуется преобразовывать старые тесты JUnit в JUnit5, если не требуются новые функции. Если у вас это так же, то используйте следующие шаги:
  1. Обновите свои библиотеки и перестройте системы с JUnit4 до JUnit5. Обязательно включите артефакт junit-vintage-engine в путь выполнения тестов (runtime path), чтобы разрешить выполнение существующих тестов.
  2. Начните создавать новые тесты, используя новые конструкции JUnit5
  3. (Необязательно) Преобразуйте JUnit тесты в JUnit5
1. Важные различия
Тесты JUnit5 выглядят в основном так же, как и тесты JUnit4, но присутствует несколько отличий, о которых вам следует знать.
Импорт. JUnit5 использует новый пакет org.junit.jupiter для своих аннотаций и классов. Например, org.junit.Test становится org.junit.jupiter.api.Test.
Аннотации. Аннотация @Test больше не имеет параметров, поскольку все они были перемещены в функцию. Например, ниже показан тест из JUnit4, который должен выкинуть исключение:

@Test(expected = Exception.class)
public void testThrowsException() throws Exception {
    // ...
}
В JUnit5 тот же самый тест выглядит так:

@Test
void testThrowsException() throws Exception {
    Assertions.assertThrows(Exception.class, () -> {
        //...
    });
}
А так в JUnit4 выглядит настройка времени ожидания выполнения теста:

@Test(timeout = 10)
public void testFailWithTimeout() throws InterruptedException {
    Thread.sleep(100);
}
Тот же самый тест, но уже в JUnit5:

@Test
void testFailWithTimeout() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(10), () -> Thread.sleep(100));
}
Также изменились и другие аннотации:
  • @Before стала @BeforeEach
  • @After стала @AfterEach
  • @BeforeClass стала @BeforeAll
  • @AfterClass стала @AfterAll
  • @Ignore стала @Disabled
  • @Category стала @Tag
  • @Rule и @ClassRule удалены; используйте вместо них @ExtendWith и @RegisterExtension
Утверждения (Assertions). Утверждения в JUnit5 теперь находятся в org.junit.jupiter.api.Assertions. Большинство общих утверждений, таких как assertEquals() и assertNotNull(), выглядят так же, как и раньше, но есть несколько отличий:
  • Сообщение об ошибке теперь является последним аргументом, например: assertEquals («my message», 1, 2) теперь выглядит так assertEquals (1, 2, «my message»)
  • Большинство утверждений также стали принимать лямбду для сообщений об ошибке, вызываемые только в случае невыполнения условия утверждения
  • Аннотации assertTimeout() и assertTimeoutPreemptively() заменили аннотацию @Timeout (в JUnit5 есть аннотация @Timeout, но она работает иначе, чем в JUnit4)
  • Появились и несколько новых утверждений, которые будут описаны ниже
Предположения (Assumptions). Предположения были перенесены в org.junit.jupiter.api.Assumptions.
Те же самые предположения существуют и сейчас, но теперь они поддерживают BooleanSupplier, а также, Hamcrest Matchers для тестирования совпадения с определенным условием. При этом можно использовать лямбды (тип Executable) для выполнения кода при удовлетворении условия.
Пример из JUnit4:

@Test
public void testNothingInParticular() throws Exception {
    Assume.assumeThat("foo", is("bar"));
    assertEquals(...);
}
В JUnit5 это выглядит так:

@Test
void testNothingInParticular() throws Exception {
    Assumptions.assumingThat("foo".equals(" bar"), () -> {
        assertEquals(...);
    });
}
2. Расширение JUnit
В JUnit4 настройка фреймворка как правило подразумевала использование аннотации @RunWith для определения пользовательского процесса тестирования (custom runner). Использование нескольких процессов тестирования было проблематичным и обычно требовало или их последовательное соединение, или использование аннотации @Rule. Это было упрощено и улучшено в JUnit5 благодаря расширениям.
Например, сборка тестов в JUnit4 с помощью фреймворка Spring выглядела так:

@RunWith(SpringJUnit4ClassRunner.class)
public class MyControllerTest {
    // ...
}
В JUnit5 вместо этого вы включаете расширение Spring:

@ExtendWith(SpringExtension.class)
class MyControllerTest {
    // ...
}
Аннотация @ExtendWith может повторяться, что позволяет объединять несколько расширений.
Вы также можете легко создать свои собственные пользовательские расширения, определив класс, реализующий один или несколько интерфейсов из org.junit.jupiter.api.extension, а затем добавить его в свой тест с помощью @ExtendWith.
3. Преобразование теста в JUnit5
Чтобы преобразовать существующий тест JUnit4 в JUnit5, используйте следующие шаги (подойдут для большинства тестов):
  1. Обновите импорт, чтобы удалить JUnit4 и добавить JUnit5. Например, обновите имя пакета для аннотации @Test, сам пакет и имя класса для утверждений (с Asserts на Assertions). Не беспокойтесь, если появились ошибки компиляции, поскольку выполнение следующих шагов должно их устранить
  2. Глобально замените старые аннотации и имена классов новыми. Например, замените все @Before на @BeforeEach, а все Asserts на Assertions
  3. Обновить утверждения (assertions). Любые утверждения, которые содержат сообщение, должны иметь соответствующий аргумент, перемещенный в конец (ничего не перепутайте в случае, когда все три аргумента являются строками!). Кроме того, обновите таймауты и ожидаемые исключения (см. примеры выше)
  4. Обновите предположения (assumptions), если вы их используете
  5. Замените любые экземпляры @RunWith, @Rule или @ClassRule соответствующими аннотациями @ExtendWith. Возможно, вам потребуется найти в Интернете обновленную документацию для расширений, которые вы используете в качестве примеров
Обратите внимание, что для миграции параметризованных тестов потребуется немного больше рефакторинга, особенно если вы использовали параметризованный JUnit4 (формат параметризованных тестов JUnit5 намного ближе к JUnitParams).
4. Новый функционал
До сих пор мы говорили только о существующей функциональности и о том, как она изменилась. Но JUnit5 предлагает множество новых функций, чтобы сделать ваши тесты более наглядными и удобными в обслуживании.
Отображение имен. С помощью JUnit5 вы можете добавить аннотацию @DisplayName к классам и методам. Имя используется при создании отчетов, что упрощает описание целей тестов и отслеживание сбоев, например:

@DisplayName("Test MyClass")
class MyClassTest {
    @Test
    @DisplayName("Verify MyClass.myMethod returns true")
    void testMyMethod() throws Exception {
        // ...
    }
}
Вы также можете использовать генератор отображаемых имен для обработки вашего тестового класса или метода для генерации имен тестов в любом формате, который вам нравится (Смотрите примеры и подробности в документации).
Утверждения (Assertions). JUnit 5 представил некоторые новые утверждения:
  • assertIterableEquals() выполняет глубокую проверку двух итерируемых коллекций (iterables) с использованием equals()
  • assertLinesMatch() проверяет, совпадают ли два списка строк. Принимает регулярные выражения в аргументе 'expected'
  • assertAll() группирует несколько утверждений вместе. Дополнительным преимуществом является то, что все утверждения выполняются, даже если отдельные утверждения не выполняются
  • assertThrows() и assertDoesNotThrow() заменили свойство 'expected' в аннотации @Test
Вложенные тесты. Наборы тестов в JUnit4 были полезны, но вложенные тесты в JUnit5 проще в настройке и обслуживании. Они лучше описывают отношения между группами тестов, например:

@DisplayName("Verify MyClass")
class MyClassTest {
    MyClass underTest;
 
    @Test
    @DisplayName("can be instantiated")
    public void testConstructor() throws Exception {
        new MyClass();
    }
    @Nested
    @DisplayName("with initialization")
    class WithInitialization {
        @BeforeEach
        void setup() {
            underTest = new MyClass();
            underTest.init("foo");
        }
 
        @Test
        @DisplayName("myMethod returns true")
        void testMyMethod() {
            assertTrue(underTest.myMethod());
        }
    }
}
В приведенном выше примере вы можете видеть, что я использую один класс для всех тестов, связанных с MyClass. Я могу проверить, что класс является экземпляром во внешнем классе теста, и я использую вложенный внутренний класс для всех тестов, где MyClass создается и инициализируется. Метод @BeforeEach применяется только к тестам во вложенном классе.
Аннотации @DisplayNames для тестов и классов указывают как цель, так и организацию тестов. Это помогает вам понять отчет о тестировании, потому что вы можете видеть условия, при которых выполняется тест (проверка MyClass с инициализацией) и то, что тест проверяет (myMethod возвращает true). Это хороший шаблон дизайна теста для JUnit5.
Параметризованные тесты. Параметризация тестов существовала в JUnit4 со встроенными библиотеками, такими как JUnit4Parameterized или сторонними библиотеками, такими как JUnitParams. В JUnit5 параметризованные тесты полностью встроены и используют некоторые из лучших функций из JUnit4Parameterized и JUnitParams, например:

@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
void myParameterizedTest(String arg) {
    underTest.performAction(arg);
}
Формат выглядит как JUnitParams, где параметры передаются непосредственно в метод теста. Обратите внимание, что значения для тестирования могут поступать из нескольких разных источников. Здесь у меня есть только один параметр, поэтому легко использовать @ValueSource. @EmptySource и @NullSource указывают, что вы хотите добавить пустую строку и нулевое значение в список значений для запуска (вы можете объединить их, как показано выше, если используете оба). Существует несколько других источников значений, таких как @EnumSource и @ArgumentsSource (пользовательский поставщик значений). Если вам нужно более одного параметра, вы также можете использовать @MethodSource или @CsvSource.
Другой тип теста, добавленный в JUnit5, это @RepeatedTest, где один тест повторяется указанное количество раз.
Условное выполнение теста. JUnit5 предоставляет API расширения ExecutionCondition для включения или отключения теста, или контейнера (класса теста) по условию. Это похоже на использование @Disabled в тесте, но оно может определять пользовательские условия. Существует несколько встроенных условий:
  • @EnabledOnOs и @DisabledOnOs: включает или отключает тест только в указанных операционных системах
  • @EnabledOnJre и @DisabledOnJre: указывает, что тест должен быть включен или отключен для определенных версий Java
  • @EnabledIfSystemProperty: включает тест на основе значения системного свойства JVM
  • @EnabledIf: использует логику в скрипте для включения теста, если условия в скрипте выполнены
Тестовые шаблоны. Тестовые шаблоны не являются обычными тестами — они определяют набор шагов, которые затем могут быть выполнены в другом месте с использованием определенного контекста вызова. Это означает, что вы можете определить тестовый шаблон один раз, а затем создать список контекстов вызова во время выполнения для запуска этого теста. Подробности и примеры смотрите в документации.
Динамические тесты. Динамические тесты похожи на шаблоны тестов, поскольку тесты для запуска генерируются во время выполнения. Однако, хотя шаблоны тестов определяются с определенным набором шагов и выполняются несколько раз, динамические тесты используют один и тот же контекст вызова, но могут выполнять другую логику. Одним из применений динамических тестов будет потоковая передача списка абстрактных объектов и выполнение отдельного набора утверждений для каждого из них на основе их конкретных типов. В документации есть хорошие примеры.
Заключение
Хотя вам, вероятно, не потребуется преобразовывать ваши старые тесты JUnit4 в JUnit5, если вы не хотите использовать новые функции JUnit5, все же есть веские причины для перехода на JUnit5. Например, тесты JUnit5 более мощные и их проще поддерживать. Кроме того, JUnit5 предоставляет много новых функций и возможностей. Все вместе эти изменения и новые функции обеспечивают мощное и гибкое обновление инфраструктуры JUnit.
Оцените статью, если она вам понравилась!