Тестирование с помощью JUnit 5 на Kotlin

Автор: admin от 12-01-2018, 10:00, посмотрело: 46

В этой статье будут рассмотрены основные возможности платформы JUnit 5 и приведены примеры их использования на Kotlin. Материал ориентирован на новичков в Kotlin и/или JUnit, однако, и более опытные разработчики найдут интересные вещи.

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 на Kotlin



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



Отображаемое название теста



В значении аннотации @DisplayName, как и в названии функции Kotlin, помимо удобочитаемого отображаемого названия теста можно указать спецсимволы и emoji:



import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

class HelloJunit5Test {

    @DisplayName("uD83DuDC4D")
    @Test
    fun `First test ?°?°)?`() {
        print("Hello, JUnit5!")
    }
}


Как видно, значение аннотации имеет приоритет перед названием функции:



Тестирование с помощью JUnit 5 на Kotlin



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



@DisplayName("Override class name")
class HelloJunit5Test {


Тестирование с помощью JUnit 5 на Kotlin



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'ом не падает, а прерывается:



Тестирование с помощью JUnit 5 на Kotlin



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()
    }
}


Тестирование с помощью JUnit 5 на Kotlin



Помимо коллекций 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()
    }


Тестирование с помощью JUnit 5 на Kotlin



Параметризованные тесты



Параметризованные тесты, как и динамические, позволяют создавать набор тестов на основе одного метода, но делают это отличным от @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)
    }


Тестирование с помощью JUnit 5 на Kotlin



Есть возможность указать метод, который будет использован как источник данных:



    @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 `Повторяемый тест`() {

    }


Тестирование с помощью JUnit 5 на Kotlin



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



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

    }


Тестирование с помощью JUnit 5 на Kotlin



Доступ к информации о текущем тесте и о группе повторяемых тестов можно получить через соответствующие объекты:



    @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 на Kotlin



Заключение



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



Спасибо!

Источник: Хабрахабр

Категория: Программирование » Веб-разработка

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *