В этой статье будут рассмотрены основные возможности платформы 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 обеспечивает удобство в разработке и лаконичность кода.
Спасибо!