Миграция с JUnit 4 на JUnit 5/6 без боли

Введение

JUnit 5 стал крупнейшим архитектурным обновлением платформы JUnit за всю его историю. В отличие от JUnit 4, это уже не просто библиотека с аннотациями, а платформа тестирования, спроектированная с учётом современных возможностей Java и потребностей больших проектов.
На практике переход на JUnit 5 чаще всего не вызывает серьёзных сложностей:
  • достаточно обновить зависимости
  • подключить нужные модули
  • начать писать новые тесты с использованием JUnit Jupiter
При этом существующие тесты JUnit 4 могут продолжать выполняться без изменений благодаря движку JUnit Vintage.
Однако для команд, которые давно используют JUnit 4, особенно с кастомными runners, rules, параметризованными тестами и интеграциями со Spring, Mockito и другими фреймворками, миграция может показаться нетривиальной.
Что касается JUnit 6, появившегося позже, то он не отменяет JUnit 5 и не вводит принципиально новый API. Его задача — закрепить архитектурные решения JUnit 5 и отказаться от легаси.
1. Почему стоит переходить с JUnit 4
Ниже — основные причины, актуальные сегодня:
  • JUnit 5 ориентирован на Java 8+, а JUnit 6 — на Java 17+
  • Улучшена читаемость и выразительность тестов: отображаемые имена, вложенные тесты, сценарный стиль
  • Полностью переработана модель расширений: extensions вместо runners и rules
  • JUnit больше не монолит — подключаются только нужные модули
  • Поддерживается параллельное выполнение тестов
  • Современные библиотеки и фреймворки (Spring Boot, Mockito, Testcontainers) ориентируются в первую очередь на JUnit 5+
Переход не обязательно должен быть одномоментным. На практике чаще всего используется следующая стратегия:
  1. Подключается JUnit 5/6
  2. Добавляется JUnit Vintage для запуска старых тестов
  3. Новые тесты пишутся уже на JUnit Jupiter
  4. Старые тесты постепенно переписываются по мере необходимости

2. Важные различия

Тесты JUnit5 выглядят в основном так же, как и тесты JUnit4, но присутствует несколько отличий, о которых вам следует знать.
1
Новые пакеты
JUnit5 использует новый пакет для своих аннотаций и классов:
org.junit.jupiter.api
Например:
  • org.junit.Test → org.junit.jupiter.api.Test
  • org.junit.Assert → org.junit.jupiter.api.Assertions
JUnit 4-классы остаются доступными только при использовании Vintage-движка и не смешиваются с Jupiter-API.
2
Аннотации
Аннотация @Test в JUnit 5 больше не содержит параметров, поскольку все они были перемещены в метод.
Например, ниже показан тест из JUnit 4, который должен выкинуть исключение:
@Test(expected = Exception.class)
public void throwsExceptionTest() {
    // ...
}
В JUnit 5 тот же самый тест выглядит так:
@Test
public void throwsExceptionTest() {
    assertThrows(Exception.class, () -> {
        //...
    });
}
А так в JUnit 4 выглядит настройка времени ожидания выполнения теста:
@Test(timeout = 5)
public void failWithTimeoutTest() {
    service.process(request);
}
Тот же тест в JUnit5:
@Test
public void failWithTimeoutTest() {
    assertTimeout(Duration.ofMillis(5), () ->
            service.process(request));
}
Также изменились и другие аннотации:
  • @Before → @BeforeEach
  • @After → @AfterEach
  • @BeforeClass → @BeforeAll
  • @AfterClass → @AfterAll
  • @Ignore → @Disabled
  • @Category → @Tag
  • @Rule и @ClassRule удалены (используйте вместо них @ExtendWith и @RegisterExtension)
3
Assertions
Assertions (утверждения) в JUnit 5 теперь находятся в
org.junit.jupiter.api.Assertions
Основные отличия:
  1. Сообщение об ошибке теперь является последним аргументом:
// JUnit 4
assertEquals(«my message», 1, 2);

// JUnit 5
assertEquals(1, 2, «my message»);
2. Большинство утверждений также стали принимать лямбду для сообщений об ошибке, вызываемые только в случае невыполнения условия утверждения
3. Появились новые утверждения:
  • assertAll
  • assertIterableEquals
  • assertLinesMatch
  • assertThrows
  • assertDoesNotThrow
  • assertTimeout
  • assertTimeoutPreemptively
4
Assumptions
Assumptions (предположения) были расширены и перенесены в
org.junit.jupiter.api.Assumptions
Предположения стали поддерживать BooleanSupplier, Hamcrest Matchers для тестирования совпадения с определенным условием, а также ламбды для выполнения кода при удовлетворении условия.
JUnit 4:
@Test
public void nothingInParticularTest() {
    assumeThat("foo", is("bar"));
    assertEquals(...);
}
JUnit 5:
@Test
public void nothingInParticularTest() {
    assumingThat("foo".equals("bar"), () -> {
        assertEquals(...);
    });
}

3. Extensions в JUnit

В JUnit4 настройка фреймворка подразумевала использование аннотации @RunWith для определения пользовательского процесса тестирования (custom runner). Использование нескольких процессов тестирования было проблематичным и обычно требовало или их последовательное соединение, или использование аннотации @Rule. Это было упрощено и улучшено в JUnit 5 благодаря extensions (расширениям).
Например, сборка тестов в JUnit 4 с помощью фреймворка Spring выглядела так:
@RunWith(SpringJUnit4ClassRunner.class)
class MyControllerTest {
    // ...
}
JUnit 5 вводит единый механизм расширений:
@ExtendWith(SpringExtension.class)
class MyControllerTest {
    // ...
}
Аннотация @ExtendWith может повторяться, что позволяет объединять несколько расширений.
Вы также можете легко создать свои собственные расширения, определив класс, реализующий один или несколько интерфейсов из org.junit.jupiter.api.extension, а затем добавить его в свой тест с помощью @ExtendWith.
Помимо декларативного подключения через @ExtendWith, JUnit 5 также поддерживает программную регистрацию расширений с помощью аннотации @RegisterExtension.
@RegisterExtension
static MyExtension extension = new MyExtension();

4. Преобразование тестов в JUnit 5

Для миграции JUnit 4-rules существует модуль junit-jupiter-migrationsupport, который предоставляет временные адаптеры для некоторых правил (TemporaryFolder, ExpectedException и др.). Он предназначен исключительно для перехода на новые версии JUnit.
Чтобы преобразовать существующий тест JUnit 4 в JUnit 5, используйте следующие шаги (подойдут для большинства тестов):
  1. Обновите импорт, чтобы удалить JUnit4 и добавить JUnit5. Например, обновите имя пакета для аннотации @Test, сам пакет и имя класса для утверждений (с Asserts на Assertions). Не беспокойтесь, если появились ошибки компиляции, поскольку выполнение следующих шагов должно их устранить
  2. Замените старые аннотации и имена классов новыми. Например, замените все @Before на @BeforeEach, а все Asserts на Assertions
  3. Обновите Assertions. Как было отмечено ранее, у них поменялись местами аргументы. Кроме того, обновите таймауты и ожидаемые исключения (тоже писали об этом выше)
  4. Обновите assumptions, если используете
  5. Замените любые экземпляры @RunWith, @Rule или @ClassRule соответствующими аннотациями @ExtendWith
Обратите внимание, что для миграции параметризованных тестов потребуется немного больше рефакторинга, особенно если вы использовали параметризованный JUnit 4.

5. Новый функционал

До сих пор мы говорили только о существующей функциональности и как она изменилась. Но JUnit 5 предлагает множество новых функций, чтобы сделать ваши тесты более наглядными и удобными в обслуживании.
1
DisplayName и генераторы имен
JUnit 5 позволяет задавать отображаемые имена для тестов и классов. Имя используется при создании отчетов, что упрощает описание целей тестов и отслеживание сбоев
@DisplayName("Test MyClass")
class MyClassTest {
    @Test
    @DisplayName("Verify MyClass.myMethod() returns true")
    public void myMethodTest() {
        // ...
    }
}
Вы также можете использовать генератор отображаемых имен тестов в любом формате, который вам нравится (смотрите примеры и подробности в документации).
2
Assertions
JUnit 5 представил некоторые новые утверждения:
  • assertIterableEquals() выполняет глубокую проверку двух итерируемых коллекций с использованием equals()
  • assertLinesMatch() проверяет, совпадают ли два списка строк. Принимает регулярные выражения в аргументе expected
  • assertAll() группирует несколько утверждений вместе. Дополнительным преимуществом является то, что все утверждения выполняются, даже если отдельные утверждения не выполняются
  • assertThrows() и assertDoesNotThrow() заменили свойство expected в аннотации @Test
3
Вложенные тесты
Наборы тестов в JUnit 4 были полезны, но вложенные тесты в JUnit 5 проще в настройке и обслуживании. Они лучше описывают отношения между группами тестов:
@DisplayName("Verify MyClass")
class MyClassTest {
    MyClass underTest;
 
    @Test
    @DisplayName("can be instantiated")
    public void constructorTest() {
        new MyClass();
    }
    @Nested
    @DisplayName("with initialization")
    class WithInitialization {
        @BeforeEach
        public void setup() {
            underTest = new MyClass();
            underTest.init("foo");
        }
 
        @Test
        @DisplayName("myMethod returns true")
        public void myMethodTest() {
            assertTrue(underTest.myMethod());
        }
    }
}
В этом примере показано, как с помощью вложенных тестов логически сгруппировать проверки одного класса и разделить тесты с разными условиями выполнения. Во внешнем тестовом классе располагаются проверки, не требующие предварительной подготовки состояния объекта. Вложенный класс (@Nested) используется для тестов, которым необходима инициализация объекта перед выполнением. Метод @BeforeEach, объявленный во вложенном классе, выполняется только для тестов этого вложенного класса, что позволяет избежать лишней инициализации и сделать тесты более изолированными и читаемыми.
Аннотации @DisplayNames для тестов и классов указывают как цель, так и организацию тестов. Это помогает лучше понять отчет о тестировании, потому что вы можете видеть условия, при которых выполняется тест и то, что тест проверяет.
4
Параметризованные тесты
Параметризация тестов существовала в JUnit 4 со встроенными библиотеками, такими как JUnit4Parameterized или сторонними библиотеками, такими как JUnitParams. В JUnit 5 параметризованные тесты полностью встроены и используют некоторые из лучших функций из JUnit4Parameterized и JUnitParams, например:
@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
public void myParameterizedTest(String arg) {
    underTest.performAction(arg);
}
Формат выглядит как JUnitParams, где параметры передаются непосредственно в метод теста. Обратите внимание, что значения для тестирования могут поступать из нескольких разных источников.
В примере указан только один параметр, поэтому легко использовать @ValueSource. @EmptySource и @NullSource указывают, что вы хотите добавить пустую строку и нулевое значение в список значений для запуска (вы можете объединить их, как показано выше, если используете оба). Существует несколько других источников значений, таких как @EnumSource и @ArgumentsSource (пользовательский поставщик значений). Если вам нужно более одного параметра, вы также можете использовать @MethodSource или @CsvSource.
Ещё один тип тестов, появившийся в JUnit 5, — это @RepeatedTest. Он используется, когда один и тот же тест необходимо выполнить несколько раз подряд, например для проверки нестабильной логики или повторяемого поведения.
5
Условное выполнение теста
JUnit 5 предоставляет API расширения ExecutionCondition для включения/отключения теста или класса теста по условию. Это похоже на использование @Disabled в тесте, но оно может определять пользовательские условия. Существует несколько встроенных условий:
  • @EnabledOnOs и @DisabledOnOs: включает или отключает тест только в указанных операционных системах
  • @EnabledOnJre и @DisabledOnJre: указывает, что тест должен быть включен или отключен для определенных версий Java
  • @EnabledIfSystemProperty: включает тест на основе значения системного свойства JVM
  • @EnabledIf: использует логику в скрипте для включения теста, если условия в скрипте выполнены

6. JUnit 6

JUnit 6 не является революцией уровня JUnit 5, однако это не просто «версия с другим номером». Основные изменения JUnit 6 направлены на устранение временных компромиссов, накопленных за годы поддержки JUnit 4, а также на повышение строгости и предсказуемости поведения тестов.
Это осознанный шаг, позволяющий:
  • убрать поддержку устаревших JVM
  • упростить код фреймворка
  • улучшить производительность и стабильность
Важно понимать, что JUnit 5 изначально проектировался как платформа, а не как просто новая версия фреймворка. Его модульная архитектура (Platform, Jupiter, Vintage) позволила:
  • отделить инфраструктуру запуска тестов от API тестирования
  • сосуществовать тестам JUnit 4 и JUnit 5 в одном проекте
  • постепенно переводить кодовую базу на новые механизмы
JUnit 6 — это следующая мажорная версия платформы JUnit, которая закрепляет архитектурные решения JUnit 5 и делает осознанный шаг вперёд. В рамках этого шага платформа отказывается от поддержки устаревших сред выполнения (например, Java ниже 17) и удаляет deprecated API, что было невозможно в JUnit 5 из-за требований обратной совместимости внутри мажорной версии.
Если ваш проект:
  • уже использует JUnit Jupiter
  • отказался от @RunWith, @Rule и @ClassRule
  • перешёл на assertThrows, assertTimeout, параметризованные тесты JUnit 5
то переход на JUnit 6:
  • минимален по усилиям
  • не требует переработки тестов
  • чаще всего сводится к обновлению зависимостей
JUnit 6 не предлагает принципиально нового стиля написания тестов или радикально нового пользовательского API. Основные изменения направлены на очистку кодовой базы и отказ от legacy-механизмов. При этом в версии 6 присутствуют отдельные изменения в API, включая переработку пакетов и уточнение поведения некоторых аннотаций.
Вместо этого JUnit 6 делает следующие важные шаги:
1
Работа с Java 17+
JUnit 6 устанавливает Java 17 как минимальную поддерживаемую версию. Это не косметическое изменение: отказ от поддержки более старых версий JVM позволяет очистить кодовую базу фреймворка от legacy-кода, улучшить производительность, а также синхронизировать тестовую платформу с современными практиками разработки на Java.
Для миграции это означает осознанный разрыв с прошлым: если проект использует только Java 8 или Java 11 и не планирует обновление платформы, то переход на JUnit 6 будет невозможен без повышения версии JVM. Это важный архитектурный фактор, который нужно учитывать в планировании миграции.
2
Поддержка расширений как единой точки интеграции
JUnit 6 окончательно закрепляет модель JUnit Platform + Jupiter API + extensions как единую архитектуру расширения. Legacy-механизмы (например, @Rule из JUnit 4) остаются только в рамках миграционных адаптеров, но не являются рекомендованным способом расширения.
Это означает, что:
  • Расширения пишутся и регистрируются через @ExtendWith
  • Программная регистрация через @RegisterExtension остаётся инструментом, но не заменой
  • Сторонние фреймворки ориентируются только на Jupiter-расширения
Таким образом, современные расширения не зависят от legacy-механизмов, что упрощает поддержку и уменьшает когнитивную нагрузку при чтении и обслуживании тестов.
3
JUnit 6 не меняет способ написания тестов
Очень важно понять: JUnit 6 не меняет способ написания тестов, если вы уже используете Jupiter в JUnit 5:
  • все аннотации (@Test, @BeforeEach, @ParameterizedTest, @Nested) остаются прежними
  • модель assertions/assumptions сохраняется
  • параметры, источники параметров, динамические тесты — всё это работает как и раньше
JUnit 6 не требует от вас переписывать тесты, если они уже соответствуют Jupiter-API. Это ключевое отличие от миграции с JUnit 4

7. Таблица изменений JUnit 4/5/6

Заключение

Миграция с JUnit 4 — это не просто обновление зависимостей и не механическая замена аннотаций. Это переход к другой модели тестирования, в которой:
  • тесты становятся более выразительными и читаемыми
  • поведение тестов — более явным и предсказуемым
  • инфраструктура расширяется через единый, композиционный механизм
  • legacy-решения больше не считаются нормой
JUnit 5 стал архитектурным переломным моментом, заложившим новую платформу для тестирования на Java. JUnit 6, в свою очередь, подтвердил этот выбор, отказавшись от дальнейших компромиссов ради обратной совместимости и зафиксировав современный подход как единственно рекомендуемый.
Оцените статью, если она вам понравилась!