Меня зовут Александр Чекунков, я — Android‑разработчик в СБЕРе. Занимаюсь разработкой CSI‑опросов в мобильном приложении «СберБанк Онлайн». Я несу ответственность за функциональность, которую используют бизнес‑команды для оценки удовлетворённости клиентов.
Не так давно, в нашем проекте мы приняли решение перейти с JUnit4 на JUnit5. Эта статья поможет разобраться в причинах выбора JUnit5 в качестве целевого решения для написания unit‑тестов. Я постараюсь ответить на ключевые вопросы: что делает JUnit5 предпочтительным выбором для современных проектов, и какие конкретные улучшения и нововведения он предлагает?
Основные отличия и преимущества
Написание тестов на JUnit5 очень похоже на работу с JUnit4, но есть серьезные отличия, которые будут рассмотрены ниже.
Если у вас на проекте используется JUnit4 и много тестов написано именно на старой библиотеке, просто переехать не получится — необходимо переписывать и конвертировать каждый тест под новую версию фреймворка.
Важное примечание! Для запуска тестов JUnit4 вместе с тестами JUnit5 можно использовать библиотеку Vintage, но в рамках статьи рассматривать её не будем.
Пакет
Первое и самое явное отличие — новая версия библиотеки использует другой пакет для работы с тестами:
import org.junit.jupiter.* // JUnit5 import org.junit.* // JUnit4
Модульная структура
JUnit5 разделён на модули (JUnit Jupiter, JUnit Platform, JUnit Vintage), что обеспечивает более гибкую архитектуру и возможность использовать только необходимые части библиотеки. Более подробно про каждый модуль можно прочитать здесь.
Аннотации
JUnit5 использует новый набор аннотаций и расширяемых моделей, которые обеспечивают более гибкую конфигурацию тестовых сценариев:
JUnit4 | JUnit5 | Назначение |
|---|---|---|
|
| используется для обозначения метода, который будет выполняться перед каждым тестом |
|
| используется для обозначения метода, который будет выполняться после каждого теста |
|
| используется для обозначения метода, который будет выполняться один раз перед запуском всех тестов |
|
| используется для обозначения метода, который будет выполняться один раз после всех тестов |
|
| используется для временного отключения выполнения тестового метода или класса, |
|
| помечает тесты тегами, что упрощает группировку и выборочное выполнение тестов по категориям |
|
| используется для регистрации расширений для аннотированного тестового класса, метода, параметра |
|
| используется для регистрации расширений (extensions) в тестовом классе |
- |
| задаёт читаемое имя для теста, которое будет отображаться в отчетах о выполнении тестов |
- |
| устанавливает максимальное время выполнения теста, после чего он будет автоматически прерван |
- |
| используется для создания вложенных классов для логической группировки тестов |
- |
| позволяет динамически отключать тесты на основе определенных условий пользователя |
- |
| позволяет динамически включать тесты на основе определенных условий пользователя |
Более подробно со всеми аннотациями в JUnit5 можно ознакомиться здесь.
Поддержка Java8 и выше
JUnit5 полностью совместим с Java 8 и более поздними версиями. Это позволяет использовать новые возможности языка, такие как лямбда‑выражения и функциональные интерфейсы, для более удобного написания тестов.
Новые Assertions
JUnit5 ввёл ряд новых утверждений, расширяя возможности проверки условий в тестах.
// assertAll() - позволяет группировать несколько утверждений в один блок. @Test fun `assertAll test`() { val person = Person(name = "Alexandr", age = 22) assertAll( "Assertions of person", { assertEquals("Alexandr", person.name) }, { assertEquals(22, person.age) } ) }
// assertThrows() - проверяет, что выполнение блока кода // выбрасывает определенное исключение. @Test fun `assertThrows test`() { val expectedMessage = "For input string: \"twenty two\"" assertThrows<NumberFormatException> { "twenty two".toInt() }.also { exception -> assertThat(exception.message).isEqualTo(expectedMessage) } } // На самом деле тестирование исключение есть и в JUnit4, // но реализованно это немного по-другому: @Test(expected = NumberFormatException::class) fun `assertThrows test in JUni4`() { val testMessage = "twenty two" val expected = 22 val actual = testMessage.toInt() }
// assertTimeout() - проверяет, что выполнение блока кода // завершается в течение указанного времени. @Test fun `assertTimeout test`() { assertTimeout(Duration.ofMillis(100)) { // Код, который должен выполниться за 100 миллисекунд testRepository.getInfo() } }
// assertTimeoutPreemptively() - предоставляет аналогичную функциональность // как assertTimeout(), но прерывает выполнение теста, если // время ожидания превышено. @Test fun `assertTimeoutPreemptively test`() { assertTimeoutPreemptively(Duration.ofMillis(10)) { // Код, который должен прервать выполнение, так как // он занимает больше 10 миллисекунд (25 миллисекунд) testRepository.fetchDataInTwentyFiveMillis() } }
// assertDoesNotThrow() - проверяет, что выполнение блока кода // не выбрасывает исключение. @Test fun `assertDoesNotThrow test`() { assertDoesNotThrow { "22".toInt() } }
Более детально все Assertions в JUnit5 описаны здесь.
Параметризованные тесты
JUnit5 предоставляет встроенную поддержку параметризованных тестов, что позволяет создавать и запускать тесты с различными входными данными. Это делает код тестов более читаемым и обеспечивает более полное покрытие различных сценариев.
Параметризированный тест — это способ написания тестов, который позволяет запускать один и тот же тест с разными входными данными. Про параметризованные тесты я рассказывал более подробно в этой статье.
internal class CalculatorTest { private val calculator = Calculator() @ParameterizedTest @CsvSource( "1, 2, 3", "-1, 1, 0", "0, 0, 0", "10, -5, 5" ) fun `parameterized test`(first: Int, second: Int, expected: Int) { val actual = calculator.calculateTwoNumbers(first, second) assertThat(actual).isEqualTo(expected) } }
Динамические тесты
JUnit5 предоставляет поддержку динамических тестов, которые позволяют создавать тесты во время выполнения программы, вместо того чтобы определять их статически в коде. Такой сценарий используют, когда количество тестов зависит от данных, внешних условий или других факторов, которые известны только во время выполнения.
internal class CalculatorTest { private val calculator = Calculator() @TestFactory fun `dynamic test`(): List<DynamicTest> { val testData = listOf( Triple(2, 2, 4), Triple(-1, 1, 0), Triple(5, -6, -1) ) return testData.map { (first, second, expected) -> DynamicTest.dynamicTest("Test $first + $second = $expected") { val actual = calculator.calculateTwoNumbers(first, second) Truth.assertThat(actual).isEqualTo(expected) } } } }
Основное отличие динамических тестов от параметризованных состоит в том, что параметризованные тесты используют заранее определённые наборы данных, а динамические тесты создаются программно во время их выполнения.
Больше информации о динамических тестах здесь.
Вложенные тесты
Ещё одной новой фичей JUnit5 стала поддержка вложенных тестов, которые позволяют логически группировать тестовые методы внутри других тестовых классов. Это полезно для организации тестов в иерархическую структуру, что облегчает понимание и поддержку кода.
internal class CalculatorTest { private val calculator = Calculator() @Nested inner class MultiplyCalculatorTest { @Test fun `3 multiply 3 equals 9`() { val firstNumber = 3.0 val secondNumber = 3.0 val expected = 9.0 val actual = calculator.multiplyTwoNumbers(firstNumber, secondNumber) assertThat(actual).isEqualTo(expected) } @Test fun `2 multiply 2 not equals 5`() { val firstNumber = 2.0 val secondNumber = 2.0 val expected = 5.0 val actual = calculator.multiplyTwoNumbers(firstNumber, secondNumber) assertThat(actual).isNotEqualTo(expected) } } @Nested inner class DivideCalculatorTest { @Test fun `12 divide 4 equals 3`() { val firstNumber = 12.0 val secondNumber = 4.0 val expected = 3.0 val actual = calculator.divideTwoNumbers(firstNumber, secondNumber) assertThat(actual).isEqualTo(expected) } @Test fun `cannot divide by zero`() { val firstNumber = 12.0 val secondNumber = 0.0 val expectedMessage = "Делить на ноль нельзя!" assertThrows<ArithmeticException> { calculator.divideTwoNumbers(firstNumber, secondNumber) }.also { exception -> assertThat(exception.message).isEqualTo(expectedMessage) } } } }
При запуске тестов библиотека будет выполнять тесты из каждого вложенного класса как отдельные группы тестов. Это улучшает структурирование и читаемость тестового кода.
Условное выполнение тестов
Также в JUnit5 добавили возможность условного выполнения тестов, что позволяет определять, должен ли конкретный тест быть выполнен в зависимости от определённых условий или окружения. Это особенно полезно, когда необходимо исключить выполнение тестов в определённых сценариях или на определённых платформах.
internal class СonditionalExecutionTest { // Тест запуститься только на операционной системе MacOS @Test @EnabledOnOs(org.junit.jupiter.api.condition.OS.MAC) fun `test for mac os`() { } // Тест запуститься только на версии Java 11 @Test @EnabledOnJre(JRE.JAVA_11) fun `test for Java 11`() { } // Тест запуститься только при выполнении какого-то кастомного условия // ВАЖНО ОТМЕТИТЬ: в аннотациии необходимо указывать полный путь к методу @Test @EnabledIf("com.example.СonditionalExecutionTest#isCustomCondition") fun `test with custom condition`() { } private fun isCustomCondition(): Boolean = false }
Подробнее с условным выполнением тестов можно ознакомиться здесь.
Скорость работы
Отдельно хочется отметить, что в JUnit5 были внесены некоторые улучшения в производительность: улучшили параллельное выполнение тестов, добавили новые возможности расширений и более гибкую архитектуру.
Для проверки скорости работы я произвел небольшие замеры, а именно запустил тесты в двух модулях, которые отвечают за функциональность оценки удовлетворённости клиентов в «СберБанк Онлайн».
Кол-во тестов | Покрытие кода | JUnit4 (среднее) | JUnit5 (среднее) | |
Feature-модуль | 160 | 95,6% | 3 сек. 597 мс. | 2 сек. 995 мс. |
|---|---|---|---|---|
Common-модуль | 132 | 96% | 3 сек. 463 мс. | 3 сек. 045 мс. |
Результаты каждого замера для JUnit4 и JUnit5 отображены на двух графиках ниже.


Исходя из данных в графиках и таблице, можно сделать вывод, что в среднем JUnit5 увеличивает скорость выполнения тестов на 15%.
Конечно, скорость работы JUnit4 и JUnit5 зависит от большого количества факт��ров: структуры тестов, конфигурации среды выполнения, используемых расширений и многого другого. На просторах интернета я нашёл обсуждения, в которых рассматривалось, что новая версия библиотеки отрабатывает медленнее, чем старая, поэтому, если хотите более детально разобраться в скорости работы библиотек, лучше провести замеры на своих проектах. Про скорость и проблемы выполнения тестов можно почитать здесь и здесь.
Поддержка других JVM-языков
И наконец, JUnit5 разработан с учётом возможности использования других языков программирования, совместимых с JVM — Java, Kotlin, Groovy, Scala. Это делает библиотеку более привлекательной для разработчиков, использующих Kotlin или другие языки JVM.
Все примеры, которые были рассмотрены выше, можно найти и изучить в этом репозитории.
В целом, JUnit5 является значительным улучшением по сравнению с JUnit4, предоставляя более гибкий набор инструментов для написания и запуска unit‑тестов.
Модульная архитектура, поддержка Java 8 и более новых версий, новые аннотации, параметризованные, вложенные и динамические тесты делают JUnit5 предпочтительным выбором для разработчиков команды СБЕРа.
Переход на JUnit5 не только облегчает тестирование, но и значительно повышает его эффективность и надёжность, что в конечном итоге способствует улучшению качества кода и ускорению разработки.
Если вы пишете свой pet‑проект или р��ботаете над большим приложением, попробуйте JUnit5 сами и убедитесь, что работа с ним значительно упрощает и улучшает процесс тестирования.
