Написание модульных тестов

Написание модульных тестов #

Подключение библиотеки Kotest #

Для написания тестов с помощью библиотеки Kotest необходимо добавить зависимости в проект. Внесите следующие доработки в файлы настроек системы сборки проекта.

  1. Удостовериться, что включена настройка для запуска JUnit-тестов в build.gradle:
    test {
       useJUnitPlatform()
    }
  2. Добавьте версию библиотеки Kotest в gradle.properties:
    kotestVersion=5.7.2
  3. Добавьте библиотеку для запуска Kotest-тестов в список зависимостей для запуска тестов в build.gradle:
    dependencies {
        testImplementation "io.kotest:kotest-runner-junit5:${kotestVersion}"
    }
  4. Добавьте библиотеку для написания тестовых утверждений в build.gradle:
    dependencies {
        testImplementation "io.kotest:kotest-assertions-core:${kotestVersion}"
    }

Написание тестов #

Библиотека Kotest поддерживает множество стилей тестирования. В рамках занятия будем рассматривать исключительно Fun Spec. Для написания тестов можете использовать любой стиль, который понравится.

Перед написанием новых тестов удалите тесты, которые были сгенерированы вместе с шаблоном приложения. Приведение их в рабочий порядок не является целью данного занятия.

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

package ru.yarsu.domain

data class Triangle(val sideA: Double, val sideB: Double, val sideC: Double) {
    fun area(): Double { ... }
    fun perimetr(): Double { ... }
}

В каталоге src/test/kotlin создадим пакет ru.yarsu.doman, в котором разместим тест для класса треугольник. Пока что создадим

package ru.yarsu.domain
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class TriangleTest : FunSpec({
    test("test the length") {
        "test".length.shouldBe(5)
    }
})

Обратите внимание на способ написания теста. Класс теста наследуется от класса FunSpec, которому в качестве аргумента передаётся лямбда-вырежение. Внутри данного выражения доступны методы для описания тестов, их расширенного контекста и так далее.

Запустите тесты с помощью команды test системы сборки Gradle: ./gradlew test (или через систему запуска команд IDEA gradle test).

Удостоверьтесь, что тест не проходит. Исправьте его, чтобы он проходил.

Добавим тесты для проверки работы методов класса треугольник:

test("Should calculate perimeter") {
    val triangle = Triangle(5.0, 4.0, 3.0)
    triangle.perimeter().shouldBe(12.0.plusOrMinus(0.01))
}

test("Should calculate area") {
    val triangle = Triangle(5.0, 4.0, 3.0)
    triangle.area().shouldBe(6.0.plusOrMinus(0.01))
}

Изначальный тест, “test the length”, можно удалить.

Описание тестовых утверждений #

Библиотека Kotest предоставляет свой набор утверждений для удобного описания требований к работе тестируемого кода. В рамках конфигурации была добавлена библиотека Core Matchers, которая включает в себя большое количество тестовых утверждений для классов и данных из стандартной библиотеки языка Kotlin.

Особенности запуска тестов #

Библиотека Kotest по умолчанию создаёт класс теста один раз для всех тестов, которые описаны внутри него. В результате надо аккуратно относиться к состоянию, которое доступно всем тестам: запуск отдельных тестов не должен влиять на данное состояние. Если вы хотите получить поведение, аналогичное JUnit, то необходимо настроить режим изоляции.

Задание № 1. Написание тестов для класса Triangles #

Создайте класс для тестирования всех методов класса Triangles, который был разработан для хранения списка треугольников.

Тестирование http4k-приложений #

При разработке веб-приложения в рамках модульных тестов можно тестировать логику следующих компонентов9

  • Обработчики HTTP-запросов.
  • Фильтры.
  • Классы предметной области.
  • Отдельные компоненты.

Для обеспечения быстроты написания тестов с помощью Kotest разработчики библиотеки http4k предоставляют свои наборы тестовых утверждений.

Для поддержки данных выражений в шаблоне приложения уже добавлена библиотека

dependencies {
    testImplementation "org.http4k:http4k-testing-kotest:${http4kVersion}"
}

Написание тестов для HTTP-обработчиков #

Разработаем тест для HTTP-обработчика корневого маршрута приложения-треугольника. Данный HTTP-обработчик должен перенаправлять на маршрут документа со списком треугольников.

Создадим соответствующий класс-тест:

package ru.yarsu.handlers

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.and
import io.kotest.matchers.should
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Status
import org.http4k.kotest.haveHeader
import org.http4k.kotest.haveStatus

class RedirectToTrianglesHandlerTest : FunSpec({
})

Сформируем тест на проверку ключевой функциональности данного обработчика:

  1. Создадим обработчик HTTP-запроса.
  2. Создадим объект запроса.
  3. Вызовем созданный обработчик.
  4. Проверим свойства возвращённого объекта.
test("Should redirect to the /triangles route") {
    val handler = redirectToTrianglesHandler()
    val request = Request(Method.GET, "/")
    val result = handler(request)
    result.should(
        haveStatus(Status.FOUND)
            .and(haveHeader("Location", "/triangles"))
    )
}

Задача № 2. Написание тестов HTTP-обработчиков #

Разработайте модульные тесты для следующих HTTP-обработчиков:

  • Обработчик для отображения информации об одном треугольнике.
  • Обработчик для отображения информации о списке треугольников.

Упрощение тестирования HTTP-обработчиков #

Обработчики HTTP-запросов, разработанные на прошлом занятии, в качестве зависимости имеют объект класса Triangles. Для написания тестов необходимо правильным образом инициализировать данный класс, наполнить его данными. В текущей ситуации класс достаточно простой, но при усложнении приложения инициализация зависимостей будет усложнять логику тестов.

В качестве решения данной проблемы можно воспользоваться привязанными функции и свойствами. Рассмотрим следующий пример:

class Triangle(val sideA: Double, val sideB: Double, val sideC: Double) {
    fun perimeter() = sideA + sideB + sideC

    fun enlarge(coefficient: Double) = Triangle(
        coefficient * sideA,
        coefficient * sideB,
        coefficient * sideC,
    )
}

Проверим работу привязанной функции.

test("Should create bound function") {
    val triangle = Triangle(3.0, 4.0, 5.0)
    val enlargeFunction: (Double) -> Triangle = triangle::enlarge
    val newTriangle = enlargeFunction(2.0)
    newTriangle.perimeter().shouldBe(24.0.plusOrMinus(0.01))
}

Для функции enlarge создадим ссылку, которая будет являться функциональным типом, сигнатура которого совпадает с сигнатурой оригинального метода.

Задача № 3. Упрощение тестирования HTTP-обработчиков #

Замените зависимость от объекта класса Triangles в обработчиках на зависимость от следующих функциональных типов:

  • Для HTTP-обработчика страницы информации об одном треугольнике: (Int) -> Triangle.
  • Для HTTP-обработчика страницы со списком треугольников: () -> List<Triangle>.

Обновите тесты для данных обработчиков. Вместо создания класса Triangles предоставляйте лямбда-выражения, которые будут предоставлять HTTP-обработчикам необходимые данные.

© A. M. Васильев, 2023, CC BY-SA 4.0, andrey@crafted.su