Статический анализ Kotlin-приложений и модульное тестирование

Статический анализ Kotlin-приложений #

Документация на статический анализ #

Статический анализ исходного кода #

Статический анализ исходного кода применяется для решения следующих задач:

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

Изучите особенности анализа исходного кода

Проверка форматирования кода с помощью ktlint #

ktlint — инструмент для статического анализа форматирования исходного кода в соответствии с официальным соглашением о стилистике кода.

Для подключения к проекту необходимо:

  • Добавить поддержку инструмента в систему сборки проекта Gradle.
  • Добавить настройку стиля программирования в среду сборки IDEA.
  • Использовать Gradle-задачи для проверки исходного кода на соответствие стиля.

Добавление поддержки в Gradle #

Инструмент ktlint может быть интегрирован разными способами, рассмотрим использование плагина Ktlint Gradle.

Необходимо добавить плагин в конфигурацию build.gradle.kts:

plugins{
    id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}

Внутри конфигурации необходимо указать версию инструмента. Для этого в файле gradle.properties укажите версию инструмента:

ktlintVersion=1.2.1

И в build.gradle.kts необходимо указать используемую версию инструмента:

val ktlintVersion: String by project

ktlint {
    version.set(ktlintVersion)
}

Настройка инструмента ktlint #

Ktlint поддерживает несколько стандартов оформления. Настройка производится путём формирования файла .editorconfig, который должен располагаться рядом с файлами конфигурации системы сборки Gradle build.gradle.kts.

В рамках курса необходимо использовать следующую конфигурацию для данного файла:

[*.{kt,kts}]
ktlint_code_style = ktlint_official

Запуск проверки исходного кода #

После конфигурации можно запускать Gradle-задачу для проверки исходного кода ktlintCheck: ./gradlew ktlintCheck. Все сообщения о нарушениях форматирования необходимо исправить.

Ограничения инструмента:

  • В пути к проекту допускается использование только символов латинского алфавита.
  • Инструмент может выполнить кеширование результатов

Настройка форматирования в IDEA #

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

Анализ потенциальных проблем в исходном коде #

Инструмент detekt выполняет поиск потенциальных проблем в исходном коде. Данные проблемы связаны с типичными проблемами:

  • Слишком длинные методы.
  • Большая вложенность циклов и условных операторов.
  • Плохие названия переменных.
  • Добавление комментариев к исходному коду и т.д.

Для использования инструмента необходимо:

  • Сформировать правила проверки кода.
  • Добавить конфигурацию для запуска инструмента в Gradle.
  • Использовать Gradle-задачи для проверки исходного кода.

По желанию можно добавить расширение для IDEA.

Формирование набора правил проверки кода #

В настоящий момент никаких отличий относительно базовой конфигурации инструмента не предполагается.

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

Добавление поддержки в Gradle #

Инструмент изначально предлагает расширение для Gradle. Для его использования необходимо добавить соответствующий плагин в build.gradle.kts:

plugins {
    id("io.gitlab.arturbosch.detekt") version "1.23.3"
}

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

detekt {
    allRules = true
    buildUponDefaultConfig = true
}

Запуск проверки исходного кода #

После конфигурации для проверки исходного кода можно запустить задачу detekt: ./gradlew detekt. Все сообщения от инструмента рекомендуется исправлять.

Настройка проверки внутри IDEA #

Проект detekt предоставляет возможность встроить проверку с помощью специального расширения.

Задача № 1 #

Добавьте проверку исходного кода с помощью инструментов ktlint и detekt в рамках исходного кода последней практической работы. Исправьте все проблемы, которые были найдены с помощью

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

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

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

  1. Удостовериться, что включена настройка для запуска JUnit-тестов в build.gradle.kts:
    test {
       useJUnitPlatform()
    }
  2. Добавьте версию библиотеки Kotest в gradle.properties:
    kotestVersion=5.8.1
  3. Добавьте библиотеку для запуска Kotest-тестов в список зависимостей для запуска тестов в build.gradle.kts:
    val kotestVersion: String by project
    dependencies {
        testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion}")
    }
  4. Добавьте библиотеку для написания тестовых утверждений в build.gradle.kts:
    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, то необходимо настроить режим изоляции.

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

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

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

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

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

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

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

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

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

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

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

package ru.yarsu.web.handlers

import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.comparables.shouldBeGreaterThan
import io.kotest.matchers.should
import io.kotest.matchers.shouldNotBe
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Status
import org.http4k.kotest.haveStatus

class HomeHandlerTest : FunSpec({
})

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

  1. Создадим обработчик HTTP-запроса.
  2. Создадим объект запроса.
  3. Вызовем созданный обработчик.
  4. Проверим свойства возвращённого объекта.
test("Should respond with non-empty body and good status") {
    val request = Request(Method.GET, "/")
    val handler = HomeHandler()
    val response = handler(request)
    response should haveStatus(Status.OK)
    response.body.length shouldNotBe null
    response.body.length?.shouldBeGreaterThan(1L)
}

Задача № 3. Тестирование HTTP-обработчика для показа списка треугольников #

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

  • Передан наполненный список треугольников.
  • Передан пустой список треугольников.

Для каждого теста определите утверждения для проверки.

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

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

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

Рассмотрим следующий пример использования привязанных фукнций:

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 класса Triangle создадим объект, который будет являться функциональным типом, сигнатура которого совпадает с сигнатурой оригинального метода.

Задача № 4. Выделение операции по получению списка треугольников #

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

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