Тестирование в Kotlin: unit, integration, e2e продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему продвинутое тестирование — основа надежного Kotlin-кода

В современной Kotlin-разработке написание кода — это лишь половина дела. Настоящее мастерство проявляется в умении создавать надежные, тестируемые и легко поддерживаемые системы. Базовые unit-тесты знакомы каждому, но когда проект вырастает до микросервисной архитектуры или сложной бизнес-логики, простых проверок становится недостаточно. Продвинутое тестирование включает в себя не только изоляцию классов, но и проверку взаимодействия между компонентами (интеграционные тесты) и полное воспроизведение пользовательских сценариев (E2E).

Эта статья предназначена для разработчиков, которые уже знакомы с основами Kotlin и хотят поднять качество своих проектов на новый уровень. Мы разберем три ключевых уровня тестирования: unit, integration и e2e, используя современные инструменты: MockK, Testcontainers и Ktor Client. Вы узнаете, как писать тесты, которые не просто "проходят", а реально защищают ваш код от регрессий.

1. Unit-тестирование: Изоляция и моки с MockK

Unit-тесты — это фундамент пирамиды тестирования. На продвинутом уровне мы не просто проверяем функции, а тестируем сложные взаимодействия между объектами, используя моки. В Kotlin лучшей библиотекой для этого является MockK.

1.1. Продвинутые возможности MockK

В отличие от Mockito, MockK написан специально для Kotlin и поддерживает корутины, лямбды и extension-функции. Рассмотрим пример тестирования сервиса, который использует репозиторий и корутины:

// Сервис для проверки
class UserService(private val repo: UserRepository) {
    suspend fun getUserName(id: Long): String {
        val user = repo.findById(id)
        return user?.name ?: "Unknown"
    }
}

// Тест
class UserServiceTest {
    private val repo = mockk<UserRepository>()
    private val service = UserService(repo)

    @Test
    fun `should return user name when user exists`() = runTest {
        // given
        coEvery { repo.findById(1L) } returns User(1, "Alice")

        // when
        val result = service.getUserName(1L)

        // then
        assertEquals("Alice", result)
        coVerify(exactly = 1) { repo.findById(1L) }
    }

    @Test
    fun `should return Unknown when user not found`() = runTest {
        // given
        coEvery { repo.findById(any()) } returns null

        // when
        val result = service.getUserName(99L)

        // then
        assertEquals("Unknown", result)
    }
}

Ключевые моменты:

  • coEvery и coVerify — для работы с suspend-функциями.
  • mockk() создает пустой мок, spyk() — частичный мок реального объекта.
  • Используйте slots для захвата аргументов.

1.2. Тестирование лямбд и extension-функций

MockK позволяет мокать функции-расширения, что критично для фреймворков вроде Ktor или Spring WebFlux:

class StringValidator {
    fun String.isValidEmail(): Boolean = this.contains("@")
}

// Тест
@Test
fun `test extension mock`() {
    val validator = mockk<StringValidator>()
    with(validator) {
        every { "test@mail.com".isValidEmail() } returns true
        every { "invalid".isValidEmail() } returns false
    }
    assertTrue("test@mail.com".isValidEmail())
    assertFalse("invalid".isValidEmail())
}

2. Интеграционное тестирование: База данных и Testcontainers

Интеграционные тесты проверяют, как ваш код взаимодействует с реальными зависимостями: базами данных, очередями сообщений, внешними API. Здесь на помощь приходит Testcontainers — библиотека, которая поднимает Docker-контейнеры прямо во время тестов.

2.1. Настройка Testcontainers с PostgreSQL

Для Spring Boot приложения это выглядит так:

@SpringBootTest
@Testcontainers
class UserRepositoryIntegrationTest {

    companion object {
        @Container
        val postgres = PostgreSQLContainer<Nothing>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test")

        @DynamicPropertySource
        @JvmStatic
        fun configureProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.datasource.url", postgres::getJdbcUrl)
            registry.add("spring.datasource.username", postgres::getUsername)
            registry.add("spring.datasource.password", postgres::getPassword)
        }
    }

    @Autowired
    private lateinit var repository: UserRepository

    @Test
    fun `should save and find user by email`() {
        val user = User(name = "Bob", email = "bob@test.com")
        val saved = repository.save(user)

        val found = repository.findByEmail("bob@test.com")

        assertNotNull(found)
        assertEquals("Bob", found?.name)
        assertEquals(saved.id, found?.id)
    }
}

Преимущества:

  • Изолированное окружение — тесты не портят реальные данные.
  • Легко переключаться между версиями СУБД.
  • Работает локально и в CI/CD.

2.2. Тестирование с Ktor и H2

Для Ktor-приложений интеграционные тесты можно писать без Spring, используя встроенный тестовый движок:

class UserRoutesTest {
    @Test
    fun `test create user endpoint`() = testApplication {
        application {
            module()
        }

        val response = client.post("/users") {
            contentType(ContentType.Application.Json)
            setBody(UserCreateRequest("Charlie", "charlie@test.com"))
        }

        assertEquals(HttpStatusCode.Created, response.status)
        val user = response.body<UserResponse>()
        assertEquals("Charlie", user.name)
    }
}

3. E2E-тестирование: Полный цикл REST API

End-to-End тесты проверяют работу всего приложения от HTTP-запроса до ответа, включая базу данных, бизнес-логику и внешние интеграции. Для Kotlin-проектов часто используют Ktor Client или RestAssured.

3.1. Пример E2E-теста на Ktor Client

class E2ETest {
    private val client = HttpClient(CIO) {
        install(ContentNegotiation) {
            json()
        }
    }

    @Test
    fun `full user lifecycle`() = runBlocking {
        // 1. Соз                

Блоги

Рекомендаций по книгам