В этой статье будут рассмотрены основные возможности платформы JUnit 5 и приведены примеры их использования на Kotlin. Материал ориентирован на новичков в Kotlin и/или JUnit, однако, и более опытные разработчики найдут интересные вещи.
Официальный user guide
Исходный код тестов из этой статьи: GitHub
Перед созданием первого теста укажем в pom.xml зависимость:
Создадим первый тест:
Тест проходит успешно:

Перейдём к обзору основных фич JUnit 5 и различных технических нюансов.
В значении аннотации
Как видно, значение аннотации имеет приоритет перед названием функции:

Аннотация применима и к классу:

Assertion'ы находятся в классе
JUnit включает несколько вариантов проверки ожидаемого и реального значений. В одном из них последним аргументом является сообщение, выводимое в случае ошибки, а в другом — лямбда-выражение, реализующее функциональный интерфейс
Для тестирования групповых assertion'ов предварительно создадим класс
Будут выполнены оба assertion'а:
Более прозрачная по сравнению с JUnit 4 работа с исключениями:
Как и в остальных примерах, всё делается просто:
При этом лямбда-выражение выполняется полностью, даже когда время выполнения уже превысило допустимое. Для того, чтобы тест падал сразу после истечения отведённого времени, нужно использовать метод
Некоторые библиотеки предоставляют более мощные и выразительные средства использования assertion'ов, чем JUnit. В частности, Hamcrest, помимо прочих, предоставляет множество возможностей для проверки массивов и коллекций. Несколько примеров:
Assumption'ы предоставляют возможность выполнения тестов только в случае выполнения определённых условий:
При этом тест с невыполнившимся assumption'ом не падает, а прерывается:

Одной из главных фич JUnit 5 является поддержка data driven тестирования.
Перед генерацией тестов для большей наглядности сделаем класс
Следующий пример сгенерирует пачку тестов для проверки того, что возраст каждого человека не меньше заданного:

Помимо коллекций
Жизненный цикл выполнения динамических тестов отличается от

Параметризованные тесты, как и динамические, позволяют создавать набор тестов на основе одного метода, но делают это отличным от
Код теста, проверяющего, что поступающие на вход даты уже в прошлом:
Значениями аннотации
Можно оставить или исключить определённые константы:

Есть возможность указать метод, который будет использован как источник данных:
В java-коде этот метод должен быть статическим, в Kotlin это достигается его объявлянием в объекте-компаньоне и аннотированием
Число повторений теста указывается следующим образом:

Есть возможность настроить выводимое название теста:

Доступ к информации о текущем тесте и о группе повторяемых тестов можно получить через соответствующие объекты:
JUnit 5 позволяет писать вложенные тесты для большей наглядности и выделения взаимосвязей между ними. Создадим пример, используя класс
Результат будет довольно наглядным:

JUnit 5 довольно прост в использовании и предоставляет множество удобных возможностей для написания тестов. Data driven тестирование с использованием Kotlin обеспечивает удобство в разработке и лаконичность кода.
Спасибо!
Официальный user guide
Исходный код тестов из этой статьи: GitHub
Перед созданием первого теста укажем в pom.xml зависимость:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.0.2</version> <scope>test</scope> </dependency>
Создадим первый тест:
import org.junit.jupiter.api.Test class HelloJunit5Test { @Test fun `First test`() { print("Hello, JUnit5!") } }
Тест проходит успешно:

Перейдём к обзору основных фич JUnit 5 и различных технических нюансов.
Отображаемое название теста
В значении аннотации
@DisplayName, как и в названии функции Kotlin, помимо удобочитаемого отображаемого названия теста можно указать спецсимволы и emoji:import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test class HelloJunit5Test { @DisplayName("\uD83D\uDC4D") @Test fun `First test ╯°□°)╯`() { print("Hello, JUnit5!") } }
Как видно, значение аннотации имеет приоритет перед названием функции:

Аннотация применима и к классу:
@DisplayName("Override class name") class HelloJunit5Test {

Assertions
Assertion'ы находятся в классе
org.junit.jupiter.Assertions и являются статическими методами.Базовые assertion'ы
JUnit включает несколько вариантов проверки ожидаемого и реального значений. В одном из них последним аргументом является сообщение, выводимое в случае ошибки, а в другом — лямбда-выражение, реализующее функциональный интерфейс
Supplier, что позволяет вычислять значение строки только в случае неудачного прохождения теста:import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class HelloJunit5Test { @Test fun `Base assertions`() { assertEquals("a", "a") assertEquals(2, 1 + 1, "Optional message") assertEquals(2, 1 + 1, { "Assertion message " + "can be lazily evaluated" }) } }
Групповые assertion'ы
Для тестирования групповых assertion'ов предварительно создадим класс
Person с двумя свойствами:class Person(val firstName: String, val lastName: String)
Будут выполнены оба assertion'а:
import org.junit.jupiter.api.Assertions.assertAll import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.junit.jupiter.api.function.Executable class HelloJunit5Test { @Test fun `Grouped assertions`() { val person = Person("John", "Doe") assertAll("person", Executable { assertEquals("John", person.firstName) }, Executable { assertEquals("Doe", person.lastName) } ) } }
Передача лямбд и ссылок на методы в проверках на true/false
@Test fun `Test assertTrue with reference and lambda`() { val list = listOf("") assertTrue(list::isNotEmpty) assertTrue { !list.contains("a") } }
Exceptions
Более прозрачная по сравнению с JUnit 4 работа с исключениями:
@Test fun `Test exception`() { val exception: Exception = assertThrows(IllegalArgumentException::class.java, { throw IllegalArgumentException("exception message") }) assertEquals("exception message", exception.message) }
Проверка времени выполнения тестов
Как и в остальных примерах, всё делается просто:
@Test fun `Timeout not exceeded`() { // Тест упадёт после выполнения лямбда-выражения, если оно превысит 1000 мс assertTimeout(ofMillis(1000)) { print("Выполняется операция, которая займёт не больше 1 секунды") Thread.sleep(3) } }
При этом лямбда-выражение выполняется полностью, даже когда время выполнения уже превысило допустимое. Для того, чтобы тест падал сразу после истечения отведённого времени, нужно использовать метод
assertTimeoutPreemptively:@Test fun `Timeout not exceeded with preemptively exit`() { // Тест упадёт, как только время выполнения превысит 1000 мс assertTimeoutPreemptively(ofMillis(1000)) { print("Выполняется операция, которая займёт не больше 1 секунды") Thread.sleep(3) } }
Внешние assertion-библиотеки
Некоторые библиотеки предоставляют более мощные и выразительные средства использования assertion'ов, чем JUnit. В частности, Hamcrest, помимо прочих, предоставляет множество возможностей для проверки массивов и коллекций. Несколько примеров:
import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.greaterThanOrEqualTo import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.notNullValue import org.junit.jupiter.api.Test class HamcrestExample { @Test fun `Some examples`() { val list = listOf("s1", "s2", "s3") assertThat(list, containsInAnyOrder("s3", "s1", "s2")) assertThat(list, hasItem("s1")) assertThat(list.size, greaterThanOrEqualTo(3)) assertThat(list[0], notNullValue()) } }
Assumptions
Assumption'ы предоставляют возможность выполнения тестов только в случае выполнения определённых условий:
import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test class AssumptionTest { @Test fun `Test Java 8 installed`() { assumeTrue(System.getProperty("java.version").startsWith("1.8")) print("Not too old version") } @Test fun `Test Java 7 installed`() { assumeTrue(System.getProperty("java.version").startsWith("1.7")) { "Assumption doesn't hold" } print("Need to update") } }
При этом тест с невыполнившимся assumption'ом не падает, а прерывается:

Data driven тестирование
Одной из главных фич JUnit 5 является поддержка data driven тестирования.
Test factory
Перед генерацией тестов для большей наглядности сделаем класс
Person data-классом, что, помимо прочего, переопределит метод toString(), и добавим свойства birthDate и age:import java.time.LocalDate import java.time.Period data class Person(val firstName: String, val lastName: String, val birthDate: LocalDate?) { val age get() = Period.between(this.birthDate, LocalDate.now()).years }
Следующий пример сгенерирует пачку тестов для проверки того, что возраст каждого человека не меньше заданного:
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.DynamicTest import org.junit.jupiter.api.DynamicTest.dynamicTest import org.junit.jupiter.api.TestFactory import java.time.LocalDate class TestFactoryExample { @TestFactory fun `Run multiple tests`(): Collection<DynamicTest> { val persons = listOf( Person("John", "Doe", LocalDate.of(1969, 5, 20)), Person("Jane", "Smith", LocalDate.of(1997, 11, 21)), Person("Ivan", "Ivanov", LocalDate.of(1994, 2, 12)) ) val minAgeFilter = 18 return persons.map { dynamicTest("Check person $it on age greater or equals $minAgeFilter") { assertTrue(it.age >= minAgeFilter) } }.toList() } }

Помимо коллекций
DynamicTest, в методе, аннотированном @TestFactory, можно возвращать Stream, Iterable, Iterator.Жизненный цикл выполнения динамических тестов отличается от
@Test методов тем, что метод, аннотированный @BeforeEach выполнится только для @TestFactory метода, а не для каждого динамического теста. Например, при выполнении следующего кода функция Reset some var будет вызвана только один раз, в чём можно убедиться, используя переменную someVar:private var someVar: Int? = null @BeforeEach fun `Reset some var`() { someVar = 0 } @TestFactory fun `Test factory`(): Collection<DynamicTest> { val ints = 0..5 return ints.map { dynamicTest("Test №$it incrementing some var") { someVar = someVar?.inc() print(someVar) } }.toList() }

Параметризованные тесты
Параметризованные тесты, как и динамические, позволяют создавать набор тестов на основе одного метода, но делают это отличным от
@TestFactory образом. Для иллюстрации работы этого способа предварительно добавим в pom.xml зависимость:<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.0.2</version> <scope>test</scope> </dependency>
Код теста, проверяющего, что поступающие на вход даты уже в прошлом:
class ParameterizedTestExample { @ParameterizedTest @ValueSource(strings = ["2002-01-23", "1956-03-14", "1503-07-19"]) fun `Check date in past`(date: LocalDate) { assertTrue(date.isBefore(LocalDate.now())) } }
Значениями аннотации
@ValueSource могут быть массивы int, long, double и String. В случае массива строк, как видно из примера выше, будет использовано неявное преобразование к типу входного параметра, если оно возможно. @ValueSource позволяет передавать только один входной параметр для каждого вызова теста.@EnumSource позволяет тестовому методу принимать константы перечислений:@ParameterizedTest @EnumSource(TimeUnit::class) fun `Test enum`(timeUnit: TimeUnit) { assertNotNull(timeUnit) }
Можно оставить или исключить определённые константы:
@ParameterizedTest @EnumSource(TimeUnit::class, mode = EnumSource.Mode.EXCLUDE, names = ["SECONDS", "MINUTES"]) fun `Test enum without days and milliseconds`(timeUnit: TimeUnit) { print(timeUnit) }

Есть возможность указать метод, который будет использован как источник данных:
@ParameterizedTest @MethodSource("intProvider") fun `Test with custom arguments provider`(argument: Int) { assertNotNull(argument) } companion object { @JvmStatic fun intProvider(): Stream<Int> = Stream.of(0, 42, 9000) }
В java-коде этот метод должен быть статическим, в Kotlin это достигается его объявлянием в объекте-компаньоне и аннотированием
@JvmStatic. Чтобы использовать не статический метод, нужно изменить жизненный цикл экземпляра теста, точнее, создавать один инстанс теста на класс, вместо одного инстанса на метод, как делается по умолчанию:@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ParameterizedTestExample { @ParameterizedTest @MethodSource("intProvider") fun `Test with custom arguments provider`(argument: Int) { assertNotNull(argument) } fun intProvider(): Stream<Int> = Stream.of(0, 42, 9000) }
Повторяемые тесты
Число повторений теста указывается следующим образом:
@RepeatedTest(10) fun `Повторяемый тест`() { }

Есть возможность настроить выводимое название теста:
@RepeatedTest(10, name = "{displayName} {currentRepetition} из {totalRepetitions}") fun `Повторяемый тест`() { }

Доступ к информации о текущем тесте и о группе повторяемых тестов можно получить через соответствующие объекты:
@RepeatedTest(5) fun `Repeated test with repetition info and test info`(repetitionInfo: RepetitionInfo, testInfo: TestInfo) { assertEquals(5, repetitionInfo.totalRepetitions) val testDisplayNameRegex = """repetition \d of 5""".toRegex() assertTrue(testInfo.displayName.matches(testDisplayNameRegex)) }
Вложенные тесты
JUnit 5 позволяет писать вложенные тесты для большей наглядности и выделения взаимосвязей между ними. Создадим пример, используя класс
Person и собственный провайдер аргументов для тестов, возвращающий стрим объектов Person:class NestedTestExample { @Nested inner class `Check age of person` { @ParameterizedTest @ArgumentsSource(PersonProvider::class) fun `Check age greater or equals 18`(person: Person) { assertTrue(person.age >= 18) } @ParameterizedTest @ArgumentsSource(PersonProvider::class) fun `Check birth date is after 1950`(person: Person) { assertTrue(LocalDate.of(1950, 12, 31).isBefore(person.birthDate)) } } @Nested inner class `Check name of person` { @ParameterizedTest @ArgumentsSource(PersonProvider::class) fun `Check first name length is 4`(person: Person) { assertEquals(4, person.firstName.length) } } internal class PersonProvider : ArgumentsProvider { override fun provideArguments(context: ExtensionContext): Stream<out Arguments> = Stream.of( Person("John", "Doe", LocalDate.of(1969, 5, 20)), Person("Jane", "Smith", LocalDate.of(1997, 11, 21)), Person("Ivan", "Ivanov", LocalDate.of(1994, 2, 12)) ).map { Arguments.of(it) } } }
Результат будет довольно наглядным:

Заключение
JUnit 5 довольно прост в использовании и предоставляет множество удобных возможностей для написания тестов. Data driven тестирование с использованием Kotlin обеспечивает удобство в разработке и лаконичность кода.
Спасибо!
