---
title: "Введение в Ktor и Exposed"
url: "https://romankryvolapov.com/ru/ktor-manual/"
description: "Серверное приложение на Kotlin с Ktor: работа с базой через Exposed и JDBI, внедрение зависимостей Koin, тесты на Mockk и Testcontainers, миграции Flyway."
language: ru
updated: 2026-01-21
---
Ktor это набирающий в последнее время популярность асинхронный фреймворк для разработки приложений на языке Kotlin. Он позволяет создавать как серверные, так и клиентские приложения, предоставляя инструментарий для работы с HTTP, WebSockets и другими сетевыми протоколами.

## Преимущества Ktor {#Преимущества-Ktor}

**Асинхронность и высокопроизводительность:** \
Ktor использует корутины Kotlin, что обеспечивает эффективную асинхронную обработку запросов и высокую производительность приложения.

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

**Простота и выразительность кода:** \
Благодаря использованию DSL (Domain-Specific Language), код становится более читаемым и понятным, упрощая процесс разработки.

**Кроссплатформенность:** \
Ktor поддерживает JVM, JavaScript и Native, что позволяет создавать приложения для различных платформ, включая серверы, десктопные и мобильные устройства.

**Интеграция с экосистемой Kotlin:** \
Полная совместимость с другими библиотеками и инструментами Kotlin, такими как kotlinx.serialization для сериализации данных и kotlinx.coroutines для управления потоками.

**Расширяемость:** \
Возможность создания собственных плагинов и расширений, что позволяет адаптировать фреймворк под специфические потребности проекта.

**Поддержка современных протоколов:** \
Встроенная поддержка HTTP/2, WebSockets и других современных сетевых технологий.

**Активное сообщество и поддержка:** \
Регулярные обновления от JetBrains и активное сообщество разработчиков, что обеспечивает стабильность и постоянное развитие фреймворка.

**Вместе с Ktor можно использовать:**

**Koin:**\
библиотека для внедрения зависимостей.

**Exposed:**\
ORM для работы с базой данных.

**JDBI (Java Database Interface):**\
библиотека на Java для упрощенной работы с реляционными базами данных.

и многое другое

## Основные отличия между Ktor и Spring {#Основные-отличия-между-Ktor-и-Spring}

**Язык и интеграция:**

**Ktor:** \
Спроектирован специально для Kotlin и максимально использует его возможности, такие как корутины и DSL (Domain-Specific Language) для конфигурации.\
**Spring:** \
Основан на Java, но поддерживает Kotlin через специальные модули. Однако некоторые функции могут быть менее оптимизированы для Kotlin.\
Архитектурный подход:

**Ktor:** \
Предоставляет легковесный и модульный подход, позволяя разработчикам выбирать только необходимые компоненты. Это дает больше контроля над конфигурацией и зависимостями.\
**Spring:** \
Предлагает всеобъемлющий набор функций "из коробки", что может ускорить разработку, но иногда приводит к избыточности.

**Производительность:**

**Ktor:** \
Благодаря асинхронной природе и использованию корутин, Ktor может обеспечить высокую производительность при обработке большого числа одновременных запросов.\
**Spring:** \
Традиционно использует синхронную модель, хотя поддерживает асинхронность через Spring WebFlux. Однако это добавляет сложности в настройке и использовании.\
Конфигурация и настройка:

**Ktor:** \
Использует кодовую конфигурацию с помощью DSL, что делает настройки более интуитивно понятными и типобезопасными.\
**Spring:** \
Опирается на аннотации и внешние файлы конфигурации (XML или YAML), что может быть менее прозрачным и требовать большего объема кода.

**Экосистема и поддержка:**

**Ktor:** \
Новее и имеет меньшую экосистему по сравнению со Spring, но активно развивается и поддерживается JetBrains.\
**Spring:** \
Имеет долгую историю, обширную документацию и большое сообщество, что облегчает поиск решений и поддержку.

**Использование:**

**Ktor:** \
Идеален для микросервисов и приложений, где важна производительность и гибкость.\
**Spring:** \
Подходит для крупных корпоративных приложений с комплексными требованиями и где ценится богатый функционал фреймворка.

## Пример приложения Ktor {#Пример-приложения-Ktor}

**Конфигурация build.gradle.kts:**

```kotlin
plugins {
    application
    kotlin("jvm") version "1.9.10"
}


application {
    mainClass.set("com.example.ApplicationKt")
}


repositories {
    mavenCentral()
}


dependencies {

    // Ktor server and client
    implementation("io.ktor:ktor-server-core:2.3.2")
    implementation("io.ktor:ktor-server-netty:2.3.2")
    implementation("io.ktor:ktor-client-core:2.3.2")
    implementation("io.ktor:ktor-client-cio:2.3.2")

    // Logging
    implementation("ch.qos.logback:logback-classic:1.4.8")

    // Koin
    implementation("io.insert-koin:koin-ktor:3.4.0")

    // Exposed
    implementation("org.jetbrains.exposed:exposed-core:0.43.0")
    implementation("org.jetbrains.exposed:exposed-dao:0.43.0")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.43.0")

    // Database (H2 for example)
    implementation("com.h2database:h2:2.1.214")

}

```

## Сервис для работы с пользователями {#Сервис-для-работы-с-пользователями}

Создаем директорию src/main/kotlin/com/example/services и добавляем файл UserService.kt- класс, который управляет операциями с базой данных, связанными с сущностью User. Он использует библиотеку Exposed для взаимодействия с базой данных и выполняет две основные функции: добавление пользователя и получение списка всех пользователей.

```kotlin
import com.example.models.Users
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction


class UserService {

    fun addUser(name: String, email: String) {
        transaction {
            Users.insert {
                it[Users.name] = name
                it[Users.email] = email
            }
        }
    }


    fun getUsers(): List<Map<String, Any>> {
        return transaction {
            Users.selectAll().map {
                mapOf("id" to it[Users.id], "name" to it[Users.name], "email" to it[Users.email])
            }
        }
    }

}
```

**Метод addUser(name: String, email: String):**\
Этот метод добавляет нового пользователя в базу данных. Давайте рассмотрим, как это происходит:

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

**Users.insert:**\
функция insert добавляет новую запись в таблицу Users.

**it\[Users.name\] = name и it\[Users.email\] = email:**\
в блоке insert устанавливаются значения для полей таблицы Users. Здесь name и email берутся из параметров функции addUser.

**Метод getUsers():**\
Этот метод возвращает список всех пользователей из базы данных в виде списка карт (ключ-значение). Разберем его работу.

**transaction:**\
как и в addUser, здесь используется блок транзакции для выполнения операций с базой данных. Все операции в этом блоке выполняются как единое целое.

**Users.selectAll():**\
метод selectAll() выбирает все записи из таблицы Users.

**.map { … }:**\
метод map используется для преобразования каждой строки в результатах выборки в Map. В каждом map создается карта, где ключи — это названия полей (id, name, email), а значения берутся из соответствующих столбцов в таблице Users:\
**"id" to it\[Users.id\]:** добавляет в карту id пользователя.\
**"name" to it\[Users.name\]:** добавляет имя пользователя.\
**"email" to it\[Users.email\]:** добавляет email пользователя.

## Настройка модуля Ktor {#Настройка-модуля-Ktor}

Теперь определим основной модуль приложения Ktor с маршрутом для работы с пользователями.\
Создаем src/main/kotlin/com/example/Application.kt:

```kotlin
import com.example.di.appModule
import com.example.models.initDatabase
import com.example.services.UserService
import io.ktor.application.*
import io.ktor.features.ContentNegotiation
import io.ktor.features.DefaultHeaders
import io.ktor.features.StatusPages
import io.ktor.gson.gson
import io.ktor.http.HttpStatusCode
import io.ktor.response.*
import io.ktor.request.*
import io.ktor.routing.*
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import org.koin.ktor.ext.inject
import org.koin.ktor.ext.Koin


fun main() {
    embeddedServer(Netty, port = 8080) {
        initDatabase()
        installModules()
    }.start(wait = true)
}


fun Application.installModules() {

    // Koin
    install(Koin) {
        modules(appModule)
    }


    // Ktor features
    install(DefaultHeaders)
    install(ContentNegotiation) {
        gson {
            setPrettyPrinting()
        }
    }
    install(StatusPages) {
        exception<Throwable> { cause ->
            call.respond(HttpStatusCode.InternalServerError, cause.localizedMessage)
        }
    }


    val userService by inject<UserService>()
    routing {
        route("/users") {
            post {
                val request = call.receive<Map<String, String>>()
                val name = request["name"] ?: error("Name is required")
                val email = request["email"] ?: error("Email is required")
                userService.addUser(name, email)
                call.respond(HttpStatusCode.Created)
            }

            get {
                call.respond(userService.getUsers())
            }
        }
    }

}
```

**Функция main:**

```kotlin
fun main() {
    embeddedServer(Netty, port = 8080) {
        initDatabase()
        installModules()
    }.start(wait = true)
}
```

**embeddedServer:**\
это функция для создания и запуска сервера. Она использует Netty в качестве движка и запускает сервер на порту 8080.

**initDatabase():**\
инициализация базы данных. Вызывает функцию, которая настраивает подключение к базе и создает таблицы.

**installModules():**\
основная функция конфигурации приложения, устанавливающая Koin и функции Ktor.

**Функция installModules:**

```kotlin
fun Application.installModules() {
    // Koin
    install(Koin) {
        modules(appModule)
    }
```

Эта функция настраивает Ktor и Koin и определяет маршруты для обработки запросов.

**install(Koin) { modules(appModule) }:**\
подключение Koin для внедрения зависимостей, инициализирует модуль appModule, содержащий зависимости, включая UserService.

**Установка Ktor-функций:**

```kotlin
    install(DefaultHeaders)
    install(ContentNegotiation) {
        gson {
            setPrettyPrinting()
        }
    }
    install(StatusPages) {
        exception<Throwable> { cause ->
            call.respond(HttpStatusCode.InternalServerError, cause.localizedMessage)
        }
    }
```

**DefaultHeaders:**\
добавляет стандартные HTTP-заголовки к каждому ответу (например, Date).

**ContentNegotiation:**\
настройка сериализации и десериализации данных в JSON с использованием библиотеки Gson.

**StatusPages:**\
обработка ошибок. Здесь, если возникает исключение, сервер возвращает статус 500 Internal Server Error и сообщение об ошибке.

**Инъекция UserService и маршруты Ktor:**

```kotlin
    val userService by inject<UserService>()
    routing {
        route("/users") {
            post {
                val request = call.receive<Map<String, String>>()
                val name = request["name"] ?: error("Name is required")
                val email = request["email"] ?: error("Email is required")
                userService.addUser(name, email)
                call.respond(HttpStatusCode.Created)
            }

            get {
                call.respond(userService.getUsers())
            }
        }
    }
```

**val userService by inject():**\
здесь inject от Koin внедряет экземпляр UserService в приложение. Этот сервис используется для взаимодействия с базой данных.

**routing:**\
блок маршрутизации Ktor, где определены пути и обработчики для запросов.

**Маршрут /users:**

**POST /users**: \
Получает данные из тела запроса (в формате JSON), затем вызывает userService.addUser(name, email) для добавления пользователя в базу данных. При успешном добавлении отправляется ответ с кодом 201 Created.

**GET /users**: \
Возвращает список всех пользователей из базы данных с помощью userService.getUsers() и отправляет результат клиенту.

**HTTP-клиент для запросов к внешним API:**\
Если нужно добавить HTTP-клиент для запросов к внешним API, можно создать в src/main/kotlin/com/example/services файл ExternalApiService.kt:

```kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*


class ExternalApiService {

    private val client = HttpClient(CIO)


    suspend fun getExternalData(): String {
        return client.get("https://jsonplaceholder.typicode.com/todos/1")
    }

}
```

**Подробное объяснение:**

**private val client = HttpClient(CIO):**\
Здесь создаётся объект client типа HttpClient, который используется для отправки HTTP-запросов.\
Конструктору HttpClient передаётся CIO — это асинхронный HTTP-движок, который Ktor предоставляет для выполнения сетевых запросов. Этот движок поддерживает асинхронное выполнение операций ввода-вывода и позволяет оптимально использовать ресурсы.\
Созданный экземпляр HttpClient будет использован для всех запросов, выполненных с помощью этого клиента, пока он не будет закрыт.

**suspend fun getExternalData(): String:**\
suspend — ключевое слово, обозначающее, что функция является асинхронной и её выполнение может быть приостановлено.\
Асинхронные функции в Kotlin позволяют запускать операции, требующие времени (например, сетевые запросы), без блокировки основного потока.\
Эта функция возвращает результат типа String, который содержит тело ответа от внешнего API.

**return client.get("https://jsonplaceholder.typicode.com/todos/1"):**

**client.get(…):**\
это метод, который делает HTTP GET-запрос по указанному URL.

**URL https://jsonplaceholder.typicode.com/todos/1:**\
это тестовый API-эндпоинт, предоставляющий данные о задаче с ID 1.

**Метод get:**\
это обобщённая функция, поэтому Kotlin автоматически определяет тип возвращаемого значения на основе ожидаемого типа результата, в данном случае String.\
Когда функция getExternalData() вызывается, клиент делает запрос и возвращает результат в виде строки, которая содержит JSON-ответ от сервера.

**Особенности работы:**

**Асинхронность:** \
Так как функция suspend, запрос выполняется асинхронно. Это позволяет программе не блокировать поток ожиданием ответа от сервера, что особенно полезно при большом количестве сетевых запросов.

**Клиент CIO:** \
Использование асинхронного движка позволяет оптимально управлять потоками, снижая нагрузку на систему и повышая производительность.

**Использование API:**\
POST /users с телом запроса { "name": "John", "email": "john@example.com" } для добавления пользователя.\
GET /users для получения списка пользователей.

## Настройка базы данных с помощью Exposed {#Настройка-базы-данных-с-помощью-Exposed}

Создадим таблицу и DAO-класс для работы с базой данных. Создадим директорию src/main/kotlin/com/example/models и добавим туда файл User.kt для модели пользователя.

```kotlin
import org.jetbrains.exposed.dao.IntIdTable
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transaction


object Users : IntIdTable() {
    val name = varchar("name", 50)
    val email = varchar("email", 100).uniqueIndex()
}


fun initDatabase() {
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
    transaction {
        SchemaUtils.create(Users)
    }
}
```

**Базовый пример таблицы Users:**\
Класс Users наследуется от IntIdTable(), что означает, что таблица будет иметь автоинкрементное целочисленное поле id в качестве первичного ключа. Поля name и email определяются как строки, где email помечен как уникальный:

**Функция initDatabase:**\
Подключается к базе данных (здесь используется H2 в памяти для тестирования) и создает схему (таблицы) с помощью SchemaUtils.create.

**Пример создания связей «Один ко многим»:**\
Добавим таблицу Posts, которая будет иметь внешний ключ, связанный с таблицей Users, создавая отношение «пользователь — посты» (один пользователь может иметь много постов).

```kotlin
object Posts : IntIdTable() {
    val title = varchar("title", 255)
    val content = text("content")
    // внешний ключ, ссылающийся на пользователя
    val user = reference("user_id", Users)
}
```

Теперь функция инициализации базы данных будет создавать обе таблицы:

```kotlin
fun initDatabase() {
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
    transaction {
        SchemaUtils.create(Users, Posts)
    }
}
```

**Вставка данных в связанные таблицы:**

```kotlin
transaction {
    // Вставка пользователя
    val userId = Users.insertAndGetId {
        it[name] = "Alice"
        it[email] = "alice@example.com"
    }

    // Вставка поста для пользователя
    Posts.insert {
        it[title] = "Первый пост"
        it[content] = "Содержание первого поста"
        it[user] = userId
    }
}

```

**Запрос с использованием связи:**\
Чтобы получить все посты конкретного пользователя, можно выполнить запрос:

```kotlin
transaction {
    val userPosts = Posts.select { Posts.user eq userId }.map {
        it[Posts.title] to it[Posts.content]
    }

    userPosts.forEach { (title, content) ->
        println("Post title: $title, content: $content")
    }
}
```

**Пример создания связи «Многие ко многим»:**\
Добавим таблицу Tags и связь «многие ко многим» между Posts и Tags.

```kotlin
object Tags : IntIdTable() {
    val name = varchar("name", 50)
}


// Промежуточная таблица для связи «многие ко многим»
object PostTags : Table() {
    val post = reference("post_id", Posts)
    val tag = reference("tag_id", Tags)
    override val primaryKey = PrimaryKey(post, tag)
}
```

**Теперь функция инициализации базы данных должна создать три таблицы:**

```kotlin
fun initDatabase() {
    Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
    transaction {
        SchemaUtils.create(Users, Posts, Tags, PostTags)
    }
}
```

**Вставка данных в связанные таблицы:**\
Создадим теги и свяжем их с постом

```kotlin
transaction {
    val postId = Posts.insertAndGetId {
        it[title] = "Второй пост"
        it[content] = "Содержание второго поста"
        it[user] = userId
    }

    val tagId1 = Tags.insertAndGetId { it[name] = "Kotlin" }
    val tagId2 = Tags.insertAndGetId { it[name] = "Programming" }

    PostTags.insert {
        it[post] = postId
        it[tag] = tagId1
    }
    PostTags.insert {
        it[post] = postId
        it[tag] = tagId2
    }
}
```

**Запрос для получения всех тегов поста:**\
Для получения тегов, связанных с постом

```kotlin
transaction {
    val tags = (PostTags innerJoin Tags).slice(Tags.name)
        .select { PostTags.post eq postId }
        .map { it[Tags.name] }
    println("Tags for post: $tags")
}
```

**Пример обновления и удаления данных:**

**Обновление данных:**\
Предположим, нам нужно обновить email пользователя. Используем запрос update

```kotlin
transaction {
    Users.update({ Users.id eq userId }) {
        it[email] = "newalice@example.com"
    }
}

```

**Удаление данных:**\
Для удаления пользователя (и, возможно, связанных записей) используется delete

```kotlin
transaction {
    Users.deleteWhere { Users.id eq userId }
}
```

**Пример сложного запроса с сортировкой и лимитом:**\
Запросим последние 5 постов пользователя, отсортированные по дате создания (предполагаем, что поле created в таблице Posts существует и автоматически заполняется)

```kotlin
object Posts : IntIdTable() {
    val title = varchar("title", 255)
    val content = text("content")
    val created = datetime("created").defaultExpression(CurrentDateTime())
    val user = reference("user_id", Users)
}
```

**Теперь, чтобы получить последние 5 постов пользователя:**

```kotlin
transaction {
    val latestPosts = Posts.select { Posts.user eq userId }
        .orderBy(Posts.created, SortOrder.DESC)
        .limit(5)
        .map {
            it[Posts.title] to it[Posts.created]
        }
    latestPosts.forEach { (title, created) ->
        println("Title: $title, Created: $created")
    }
}
```

## Настройка базы данных с помощью JDBI {#Настройка-базы-данных-с-помощью-JDBI}

**JDBI (Java Database Interface):**\
это библиотека на Java для упрощенной работы с реляционными базами данных. Она предоставляет удобный API поверх JDBC (Java Database Connectivity), делая взаимодействие с базой данных менее сложным и более понятным. JDBI позволяет писать SQL-запросы в чистом виде, но при этом автоматизирует преобразование данных между SQL и Java-объектами, что упрощает процесс работы с базами данных и делает код более читабельным и поддерживаемым.

**Основные особенности JDBI:**

**Легковесность:**\
JDBI ориентирован на использование простых SQL-запросов, не навязывая ORM (Object-Relational Mapping), как Hibernate или Exposed.

**Поддержка SQL Object API:**\
В JDBI можно описывать SQL-запросы с помощью аннотаций в интерфейсах и автоматически связывать их с методами, что позволяет избежать написания большого количества кода.

**Маппинг объектов:**\
JDBI автоматически преобразует строки результата SQL-запроса в объекты Java.

**Гибкость:**\
В отличие от ORM-решений, JDBI позволяет разрабатывать приложения, в которых SQL-запросы пишутся напрямую, сохраняя полный контроль над выполнением запросов и производительностью.

**Основные компоненты JDBI:**

**Handle:**\
основной объект JDBI для выполнения запросов. Он открывает и управляет соединением с базой данных.

**SQL Object API:**\
позволяет писать SQL-запросы как методы интерфейсов с аннотациями, что делает код лаконичным.

**Fluent API:**\
предоставляет "текучий" API для работы с SQL-запросами, где можно гибко настраивать параметры и выполнение запросов.

**Пример работы с JDBI:**\
Предположим, у нас есть база данных с таблицей users. Рассмотрим базовый пример использования JDBI для взаимодействия с этой таблицей.

**Настройка JDBI:**\
Подключаем зависимость JDBI в build.gradle.kts:

```kotlin
dependencies {
    implementation("org.jdbi:jdbi3-core:3.29.0")
    // для SQL Object API
    implementation("org.jdbi:jdbi3-sqlobject:3.29.0")
     // для поддержки Kotlin
    implementation("org.jdbi:jdbi3-kotlin:3.29.0")
}
```

**Создаем класс модели:**

```kotlin
data class User(
  val id: Int,
  val name: String,
  val email: String
)
```

**Создаем интерфейс для SQL-запросов, используя аннотации JDBI:**

```kotlin
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper
import org.jdbi.v3.sqlobject.statement.SqlQuery
import org.jdbi.v3.sqlobject.statement.SqlUpdate


// Указывает JDBI автоматически маппить результаты запросов в User
@RegisterBeanMapper(User::class)
interface UserDao {

    @SqlUpdate("INSERT INTO users (name, email) VALUES (:name, :email)")
    fun insertUser(name: String, email: String)


    @SqlQuery("SELECT * FROM users WHERE id = :id")
    fun findById(id: Int): User?


    @SqlQuery("SELECT * FROM users")
    fun listUsers(): List<User>

}
```

**Использование UserDao для выполнения запросов:**

```kotlin
import org.jdbi.v3.core.Jdbi

fun main() {

    // Создаем соединение JDBI
    val jdbi = Jdbi.create("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", "username", "password")


    // Работа с базой данных через DAO
    jdbi.useHandle<Exception> { handle ->
        val userDao = handle.attach(UserDao::class.java)


        // Создаем таблицу
        handle.execute("CREATE TABLE users (id IDENTITY PRIMARY KEY, name VARCHAR(50), email VARCHAR(100))")


        // Добавляем пользователей
        userDao.insertUser("Alice", "alice@example.com")
        userDao.insertUser("Bob", "bob@example.com")


        // Получаем пользователя по ID
        val user = userDao.findById(1)
        println("User: $user")


        // Получаем список всех пользователей
        val users = userDao.listUsers()
        println("All users: $users")
    }

}
```

**Объяснение примера:**

**Настройка подключения:** \
JDBI создается с помощью Jdbi.create(…), где указывается строка подключения, имя пользователя и пароль.

**Использование интерфейса UserDao:** \
В JDBI интерфейс UserDao связан с соединением базы данных через handle.attach(UserDao::class.java), что позволяет использовать SQL-запросы как методы.

**Аннотации SQL:** \
Методы в UserDao аннотированы @SqlUpdate (для запросов на изменение) и @SqlQuery (для запросов на выборку). Они определяют, какие SQL-запросы будут выполняться при вызове соответствующих методов.

**Преобразование результатов:** \
@RegisterBeanMapper(User::class) указывает JDBI автоматически преобразовать результаты в объекты User, что упрощает работу с результатами запросов.

## Настройка Koin для внедрения зависимостей {#Настройка-Koin-для-внедрения-зависимостей}

Koin — это библиотека для внедрения зависимостей (Dependency Injection, DI) в Kotlin. Она позволяет упростить и автоматизировать управление зависимостями, делая код более чистым и легко тестируемым. Koin отличается от других DI-фреймворков тем, что не требует аннотаций или кодогенерации, а использует Kotlin DSL для декларации зависимостей.

**Основные особенности Koin:**

**Легкость и простота:**\
Koin разработан специально для Kotlin, поэтому он использует Kotlin DSL и позволяет писать декларации зависимостей с минимальной настройкой.

**Без аннотаций и прокси-классов:**\
В отличие от Dagger или Hilt, Koin не использует аннотации и не требует предварительной компиляции для работы. Это делает его быстрым и гибким для настройки.

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

**Тестируемость:**\
С Koin можно легко подменять зависимости для тестов, благодаря чему юнит-тестирование становится проще.

**Основные компоненты Koin:**

**Модули:**\
контейнер для зависимостей, где описаны все необходимые классы и их зависимости.

**single и factory:**\
Koin поддерживает два типа деклараций зависимостей:\
**single:**\
создаёт синглтон, один и тот же экземпляр класса на протяжении всего времени работы приложения.\
**factory:**\
создаёт новый экземпляр класса при каждом обращении.

**inject и get:**\
inject используется для внедрения зависимостей через делегаты, а get — для получения зависимости в любом месте кода, где доступен контекст Koin.

**Пример использования Koin:**\
Рассмотрим, как внедрить зависимости с помощью Koin на примере простого приложения с классами UserRepository и UserService.

**Добавление зависимости Koin в проект:**\
В build.gradle.kts добавим зависимость для Koin

```kotlin
dependencies {
    implementation("io.insert-koin:koin-core:3.4.0")
    implementation("io.insert-koin:koin-ktor:3.4.0") // для интеграции с Ktor
}
```

**Создание классов и зависимостей:**

```kotlin
// UserRepository.kt
class UserRepository {
    fun getUser() = "User data from repository"
}


// UserService.kt
class UserService(private val userRepository: UserRepository) {
    fun getUserData() = userRepository.getUser()
}
```

**Создание модуля Koin:**\
Создадим модуль, который объявляет зависимости для UserRepository и UserService:

```kotlin
import org.koin.dsl.module


val appModule = module {
  	// единственный экземпляр UserRepository
    single { UserRepository() }
    // новый экземпляр UserService при каждом запросе
    factory { UserService(get()) }
}
```

**single { UserRepository() }:**\
определяет UserRepository как синглтон.

**factory { UserService(get()) }:**\
определяет UserService как фабрику, которая создаёт новый экземпляр каждый раз, когда он запрашивается. get() указывает Koin использовать экземпляр UserRepository для создания UserService.

**Запуск Koin в приложении:**\
Koin запускается, добавляя модули в его контекст. В Ktor это делается через install(Koin):

```kotlin
import io.ktor.application.*
import org.koin.core.context.startKoin
import org.koin.ktor.ext.Koin


fun main() {
    embeddedServer(Netty, port = 8080) {
        install(Koin) {
            modules(appModule)
        }
        // остальная конфигурация Ktor
    }.start(wait = true)
}
```

**Использование зависимостей с inject:**\
Теперь мы можем использовать UserService с помощью inject

```kotlin
import org.koin.ktor.ext.inject


class UserController {

    private val userService by inject<UserService>()


    fun printUserData() {
        println(userService.getUserData())
    }
}
```

**Пример использования Koin для тестов:**\
Koin позволяет легко заменить зависимости в тестах. Например

```kotlin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
import org.koin.test.KoinTest
import org.koin.test.inject
import kotlin.test.BeforeTest
import kotlin.test.AfterTest
import kotlin.test.Test


class UserServiceTest : KoinTest {

    private val userService by inject<UserService>()


    @BeforeTest
    fun setup() {
        val testModule = module {
            single { UserRepository() }
            factory { UserService(get()) }
        }
        startKoin { modules(testModule) }
    }


    @AfterTest
    fun tearDown() {
      	// Остановка Koin после тестов
        stopKoin()
    }


    @Test
    fun testUserData() {
        assertEquals("User data from repository", userService.getUserData())
    }

}
```

## Примеры тестов с использованием Mockk {#Примеры-тестов-с-использованием-Mockk}

Вот пример, как можно протестировать UserService с использованием библиотеки Koin для внедрения зависимостей и MockK для создания моков (фальш-объектов).\
Предположим, что мы тестируем UserService, который использует UserRepository для получения данных о пользователе. В тесте мы будем замещать реальный UserRepository мок-объектом, чтобы изолировать тест от базы данных или других внешних

**Настройка зависимостей:**\
В build.gradle.kts добавим зависимости для Koin, MockK и Koin-test:зависимостей.

```kotlin
dependencies {
    // Основные зависимости
    implementation("io.insert-koin:koin-core:3.4.0")


    // Зависимости для тестов
    testImplementation("io.insert-koin:koin-test:3.4.0")
    testImplementation("io.mockk:mockk:1.12.0")
    testImplementation("org.jetbrains.kotlin:kotlin-test")
}

```

**Классы UserRepository и UserService:**\
Классы, которые мы будем тестировать, выглядят так

```kotlin
// UserRepository.kt
class UserRepository {

    fun getUser(): String {
        // В реальной ситуации этот метод мог бы обращаться к базе данных
        return "Real User Data"
    }
}


// UserService.kt
class UserService(private val userRepository: UserRepository) {

    fun getUserData(): String {
        return userRepository.getUser()
    }
}

```

**Настройка модуля Koin для тестирования:**\
Создадим тест, где будем подменять UserRepository на мок-объект с помощью MockK.

```kotlin
import io.insert-koin.core.context.startKoin
import io.insert-koin.core.context.stopKoin
import io.insert-koin.dsl.module
import io.insert-koin.test.KoinTest
import io.insert-koin.test.inject
import io.mockk.every
import io.mockk.mockk
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals


class UserServiceTest : KoinTest {

    // Зависимость UserService будет автоматически внедрена Koin
    private val userService by inject<UserService>()
    private val mockRepository = mockk<UserRepository>()


    @BeforeTest
    fun setup() {

        // Определение тестового модуля Koin, где мы заменяем UserRepository на мок
        val testModule = module {
          	// Используем mockRepository вместо реального UserRepository
            single { mockRepository }
            single { UserService(get()) }
        }

        // Запускаем Koin с тестовым модулем
        startKoin { modules(testModule) }

    }


    @AfterTest
    fun tearDown() {
        // Останавливаем Koin после выполнения теста
        stopKoin()
    }


    @Test
    fun `should return mock user data`() {

        // Настраиваем поведение мок-объекта
        every { mockRepository.getUser() } returns "Mocked User Data"


        // Выполняем тестируемый метод
        val result = userService.getUserData()


        // Проверяем, что результат соответствует ожиданиям
        assertEquals("Mocked User Data", result)

    }

}
```

**Объяснение теста:**

**Создание мок-объекта:**

```kotlin
private val mockRepository = mockk<UserRepository>()
```

Мы создаем мок-объект для UserRepository с помощью MockK, чтобы не использовать реальную базу данных или другие внешние зависимости.

**Определение тестового модуля**:

```kotlin
val testModule = module {
    single { mockRepository }
    single { UserService(get()) }
}
```

В тестовом модуле UserRepository заменяется на mockRepository, а UserService инстанцируется с использованием этой подмененной зависимости.

**Настройка поведения мока**:

```kotlin
every { mockRepository.getUser() } returns "Mocked User Data"
```

Здесь мы указываем, что при вызове getUser() у mockRepository метод должен возвращать "Mocked User Data". Это позволяет протестировать UserService изолированно от реального поведения UserRepository.

**Проверка результата**:

```kotlin
val result = userService.getUserData()
assertEquals("Mocked User Data", result)
```

Вызвав getUserData у UserService, мы ожидаем, что он вернет "Mocked User Data", так как это поведение было задано для мока UserRepository.

**Остановка Koin**:

```kotlin
stopKoin()
```

После завершения теста Koin останавливается, чтобы тестовый контекст не повлиял на другие тесты.

Этот пример демонстрирует, как использовать Koin для управления зависимостями и MockK для подмены поведения зависимостей в тестах. Благодаря этому подходу мы можем изолировать UserService и протестировать его логику, не полагаясь на реализацию UserRepository.

## Примеры тестов с использованием Testcontainers {#Примеры-тестов-с-использованием-Testcontainers}

Testcontainers — это Java-библиотека для запуска контейнеров Docker в тестовой среде. Она позволяет запускать реальные сервисы, такие как базы данных, очереди сообщений, кеши и другие инфраструктурные компоненты, как Docker-контейнеры прямо в процессе выполнения тестов. Это делает Testcontainers удобным для написания интеграционных тестов, которые требуют работы с настоящими сервисами, вместо использования фейков или встроенных баз данных.

**Основные особенности Testcontainers:**

**Изолированная тестовая среда:\**
Каждый тест запускается в отдельном контейнере, что позволяет избежать влияния тестов друг на друга и получать одинаковые результаты независимо от локальных настроек.

**Поддержка различных баз данных:**\
Testcontainers поддерживает множество популярных баз данных, таких как PostgreSQL, MySQL, Redis, MongoDB и другие. Это позволяет тестировать приложение с той же базой данных, которая используется в продакшене.

**Поддержка других сервисов:**\
Помимо баз данных, Testcontainers поддерживает такие сервисы, как Kafka, RabbitMQ, Selenium (для тестирования UI), и другие.

**Интеграция с JUnit 4 и 5:**\
Testcontainers легко интегрируется с популярными фреймворками для тестирования, такими как JUnit 4 и JUnit 5.

**Когда использовать Testcontainers:**\
Testcontainers полезен в следующих ситуациях\
Когда нужно протестировать взаимодействие с базой данных (например, с PostgreSQL или MySQL) в условиях, приближенных к реальной среде.\
При тестировании взаимодействия между различными компонентами системы, например, между приложением и брокером сообщений.\
Для написания интеграционных тестов, где важно иметь доступ к настоящему сервису, а не к фейку.\
Пример использования Testcontainers с PostgreSQL и JUnit 5\
Предположим, что у нас есть приложение на Kotlin, использующее базу данных PostgreSQL, и мы хотим написать интеграционный тест для проверки взаимодействия с базой.

**Подключение зависимостей:**\
Добавим зависимости для Testcontainers и PostgreSQL в build.gradle.kts:

```kotlin
dependencies {
    testImplementation("org.testcontainers:testcontainers:1.19.0")
    testImplementation("org.testcontainers:postgresql:1.19.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
}
```

**Пример использования Testcontainers для теста с базой данных:**

```kotlin
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.AfterAll
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.sql.Connection
import java.sql.DriverManager


@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PostgresTest {


    @Container
    private val postgresContainer = PostgreSQLContainer<Nothing>("postgres:15-alpine").apply {
        withDatabaseName("testdb")
        withUsername("testuser")
        withPassword("testpassword")
        // Запускаем контейнер
        start()
    }


    private lateinit var connection: Connection


    @BeforeAll
    fun setup() {
        // Подключение к базе данных внутри контейнера
        connection = DriverManager.getConnection(
            postgresContainer.jdbcUrl,
            postgresContainer.username,
            postgresContainer.password
        )

        // Создание таблицы для теста
        connection.createStatement().executeUpdate(
            """
            CREATE TABLE users (
                id SERIAL PRIMARY KEY,
                name VARCHAR(50) NOT NULL,
                email VARCHAR(100) NOT NULL
            );
            """
        )
    }


    @Test
    fun `test insert and query`() {
        // Вставка данных
        connection.createStatement().executeUpdate(
            "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');"
        )


        // Выполнение запроса
        val resultSet = connection.createStatement().executeQuery("SELECT * FROM users")
        resultSet.next()
        val name = resultSet.getString("name")
        val email = resultSet.getString("email")

        assertEquals("Alice", name)
        assertEquals("alice@example.com", email)
    }


    @AfterAll
    fun teardown() {
        connection.close()
        // Остановка контейнера после завершения тестов
        postgresContainer.stop()
    }
}

```

**Объяснение кода:**

**Создание контейнера PostgreSQL:**\
С помощью PostgreSQLContainer мы создаем контейнер с PostgreSQL. Задаем имя базы данных, имя пользователя и пароль.\
Контейнер запускается с образом postgres:15-alpine.

**Настройка соединения с базой данных:**\
В @BeforeAll устанавливается подключение к базе данных в контейнере.\
Также создается таблица users для хранения данных теста.

**Тестирование вставки и запроса данных:**\
В @Test выполняется вставка данных и их выборка из таблицы users, после чего проверяется корректность данных с помощью assertEquals.

**Остановка контейнера после завершения тестов:**\
Контейнер PostgreSQL автоматически останавливается после выполнения всех тестов благодаря @AfterAll.

**Преимущества использования Testcontainers:**

**Реальные условия тестирования:** \
Позволяет работать с реальными сервисами и инфраструктурой, что помогает выявить ошибки, которые могут не проявляться при использовании встроенных или имитированных сервисов.

**Изоляция тестов:** \
Каждый тест запускается в новом контейнере, что гарантирует чистоту данных и отсутствие зависимости от состояния других тестов.

**Совместимость с CI/CD:** \
Testcontainers легко интегрируется в CI/CD-конвейеры, что позволяет запускать тесты в изолированных средах на различных платформах.

## Пример миграций с использованием Flyway {#Пример-миграций-с-использованием-Flyway}

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

**Шаги по настройке и использованию Flyway:**\
Предположим, у нас есть проект с базой данных, в котором необходимо добавлять, изменять или удалять таблицы, а также управлять изменениями структуры с помощью миграций Flyway.

**Добавление Flyway в проект:**\
Добавим зависимости Flyway в build.gradle.kts:

1. 

```kotlin
dependencies {
    implementation("org.flywaydb:flyway-core:9.8.1")
    // Драйвер для PostgreSQL
    implementation("org.postgresql:postgresql:42.3.1")
}
```

**Настройка Flyway:**\
Добавим конфигурацию Flyway в build.gradle.kts

```kotlin
import org.flywaydb.gradle.task.FlywayMigrateTask


plugins {
    id("org.flywaydb.flyway") version "9.8.1"
}


flyway {
    url = "jdbc:postgresql://localhost:5432/mydatabase"
    user = "username"
    password = "password"
    schemas = arrayOf("public")
    // Директория миграций
    locations = arrayOf("filesystem:src/main/resources/db/migration")
}
```

**Создание миграции:**\
Flyway ищет SQL-файлы миграций в папке, указанной в locations. Для каждого изменения создается файл миграции, который будет автоматически применен при запуске Flyway. Миграции должны следовать формату имени файла: V\<номер>__\<описание>.sql.

**Пример:**\
V1__create_users_table.sql\
V2__add_email_to_users.sql\
Каждый файл миграции содержит SQL-команды для выполнения изменения в базе данных.

**Пример файла миграции V1__create_users_table.sql:**\
Создадим файл миграции для создания таблицы users

```sql
-- V1__create_users_table.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);
```

**Пример файла миграции V2__add_email_to_users.sql:**\
Добавим новый столбец created_at в таблицу users

```sql
-- V2__add_created_at_column.sql
ALTER TABLE users
ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
```

**Запуск миграций Flyway:**\
Теперь мы можем запустить миграции с помощью команды Gradle

```text
./gradlew flywayMigrate
```

Эта команда выполнит все миграции, которые ещё не применены, и обновит структуру базы данных. Flyway сохраняет информацию о выполненных миграциях в специальной таблице (flyway_schema_history), чтобы гарантировать, что каждая миграция выполняется только один раз.

**Откат миграции (если требуется):**\
Если нам нужно откатить миграцию, можно использовать команду:

```text
./gradlew flywayUndo
```

Однако следует отметить, что Flyway поддерживает откат миграций (undo) только в коммерческой версии. В бесплатной версии миграции можно отменить вручную, добавив отдельные SQL-файлы для изменения или удаления созданных данных.

**Пример кода для запуска миграций программно:**\
В некоторых случаях может понадобиться запускать миграции программно, например, при старте приложения. Flyway позволяет это сделать:

```kotlin
import org.flywaydb.core.Flyway


fun main() {
    val flyway = Flyway.configure()
        .dataSource("jdbc:postgresql://localhost:5432/mydatabase", "username", "password")
        .load()
    flyway.migrate()
}
```

## Использование переменных окружения {#Использование-переменных-окружения}

Глобальные переменные окружения часто используются для хранения конфиденциальной информации или настройки, зависящей от среды выполнения приложения (например, базы данных, ключей API и других настроек), чтобы не хранить их в коде напрямую.\
В Kotlin (и Java) переменные окружения можно получать через системные свойства или специальные библиотеки, такие как dotenv. Давайте рассмотрим несколько способов использования переменных окружения.

**Пример доступа к переменным окружения напрямую:**

Переменные окружения можно получить в Kotlin с помощью System.getenv().\
Предположим, что нам нужно получить данные для подключения к базе данных, такие как URL, имя пользователя и пароль, которые заданы в переменных окружения DB_URL, DB_USER, DB_PASSWORD.

```kotlin
fun main() {
    val dbUrl = System.getenv("DB_URL") ?: "jdbc:postgresql://localhost:5432/defaultdb"
    val dbUser = System.getenv("DB_USER") ?: "default_user"
    val dbPassword = System.getenv("DB_PASSWORD") ?: "default_password"
}
```

Такой подход удобен для простых приложений и подходит для переменных окружения, которые определяются в системе (например, в файле .bashrc или .zshrc на локальной машине).

**Использование .env файла с библиотекой dotenv:**

Для более гибкого управления переменными окружения, особенно в разных средах (разработка, тестирование, продакшен), можно использовать библиотеку dotenv. Эта библиотека позволяет хранить переменные окружения в файле .env и загружать их при старте приложения.

**Добавление зависимости:**\
Добавим библиотеку dotenv в build.gradle.kts:

```kotlin
dependencies {
    implementation("io.github.cdimascio:java-dotenv:5.2.2")
}
```

**Создание .env файла:**\
Создаем .env файл в корне проекта и добавляем в него переменные окружения:

```text
DB_URL=jdbc:postgresql://localhost:5432/mydatabase
DB_USER=myuser
DB_PASSWORD=supersecretpassword
```

**Использование переменных окружения из .env файла в коде:**\
Затем можно использовать библиотеку dotenv для загрузки переменных из .env файла:

```kotlin
import io.github.cdimascio.dotenv.dotenv


fun main() {
    val dotenv = dotenv()
    val dbUrl = dotenv["DB_URL"] ?: "jdbc:postgresql://localhost:5432/defaultdb"
    val dbUser = dotenv["DB_USER"] ?: "default_user"
    val dbPassword = dotenv["DB_PASSWORD"] ?: "default_password"
}
```

**Использование переменных окружения с Ktor:**

Ktor также поддерживает использование переменных окружения для конфигурации приложения. Переменные окружения можно загружать через application.conf или получать их с помощью System.getenv().

**Пример конфигурации базы данных в application.conf:**\
В application.conf можно указать путь к переменным окружения через ${}:

```kotlin
ktor {
    deployment {
        port = 8080
    }
    database {
        url = ${DB_URL}
        user = ${DB_USER}
        password = ${DB_PASSWORD}
    }
}
```

**Пример использования в коде Ktor:**\
В коде Ktor можно получить значения из конфигурации и использовать их для подключения к базе данных:

```kotlin
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*


fun main() {
    embeddedServer(Netty, port = 8080) {
        environment.config.config("database").apply {
            val dbUrl = property("url").getString()
            val dbUser = property("user").getString()
            val dbPassword = property("password").getString()
        }
    }.start(wait = true)
}
```

**Пример запуска с глобальными переменными окружения:**\
Для работы с переменными окружения на разных средах (например, в Docker или на CI/CD-системах) можно указать переменные окружения при запуске приложения.

```kotlin
DB_URL="jdbc:postgresql://production-db:5432/proddb" \
DB_USER="produser" \
DB_PASSWORD="prodpassword" \
java -jar myapp.jar
```

**В IntelliJ IDEA можно легко настроить загрузку переменных окружения из .env файла для проекта на Kotlin/Java:**\
Чтобы добавить переменные окружения из .env файла в конфигурацию запуска Application в IntelliJ IDEA, выполните следующие шаги

**Открытие конфигурации запуска:**\
В меню выберите Run > Edit Configurations….\
Найдите конфигурацию запуска вашего приложения в списке (например, Application).

**Добавление переменных окружения:**\
В настройках конфигурации найдите поле Environment variables.\
Нажмите на значок … рядом с этим полем, чтобы открыть редактор переменных окружения.\
Добавьте переменные вручную, копируя значения из .env файла, в формате КЛЮЧ=ЗНАЧЕНИЕ. Например:

```kotlin
DB_URL=jdbc:postgresql://localhost:5432/mydatabase
DB_USER=myuser
DB_PASSWORD=supersecretpassword
```

Переменные можно добавлять по одной, нажимая +, или сразу все, разделяя их символом ; на Windows или : на macOS/Linux.\
Нажмите OK для сохранения переменных.

**Сохранение и запуск:**\
Нажмите Apply и OK для сохранения конфигурации запуска.\
Теперь переменные окружения будут доступны вашему приложению при запуске. Вы можете получить их с помощью System.getenv("КЛЮЧ") в коде.

**Альтернативный способ:** \
Использование плагина EnvFile\
Чтобы автоматически загрузить переменные из .env файла:\
Установите плагин EnvFile в IntelliJ IDEA:\
Откройте File > Settings > Plugins.\
Найдите EnvFile и установите его.\
Перезапустите IDE после установки.\
В Run > Edit Configurations… откройте конфигурацию запуска вашего приложения.\
Во вкладке EnvFile отметьте Enable EnvFile и выберите .env файл.\
Сохраните настройки и запустите проект.

[ Copyright: Roman Kryvolapov ](https://t.me/RomanKryvolapov)
