TDD в Kotlin: пишем код без страха и упрека

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

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

Начать курс

Почему TDD — это не очередная модная методология, а ваш новый супергеройский плащ

Представьте: вы пишете код, как художник — мазок за мазком, строчка за строчкой. Вы уверены, что всё гениально. Но проходит час, вы запускаете приложение, и оно падает с ошибкой, которую вы искали бы полдня. Знакомо?

А теперь представьте другой сценарий. Вы сначала пишете тест, который гарантированно упадёт. Потом — минимальный код, чтобы тест прошёл. И только потом — рефакторите, улучшаете, но уже с чистой совестью. Это и есть разработка через тестирование (TDD), и в Kotlin она превращается в настоящее удовольствие.

В этой статье мы не будем читать скучные лекции. Мы возьмём реальный пример, пройдём все три стадии TDD (Red — Green — Refactor) и покажем, как писать надёжный код без страха и упрёка. Готовы? Поехали!

TDD: три буквы, которые изменят ваш код

TDD (Test-Driven Development) — это не просто техника, это философия. Суть проста: сначала тест, потом код. Звучит безумно? На первый взгляд — да. На практике — экономит часы отладки.

Цикл разработки через тесты

  • Red — пишем тест, который не проходит (он должен упасть).
  • Green — пишем минимальный код, чтобы тест прошёл.
  • Refactor — улучшаем код, не меняя его поведения.

И так — по кругу, маленькими шагами. Каждый шаг — это уверенность, что вы ничего не сломали. В Kotlin с его лаконичным синтаксисом и нулевой безопасностью TDD становится ещё приятнее.

Инструментарий: что нужно для TDD в Kotlin

Прежде чем погружаться в код, давайте подготовим арсенал. Для юнит-тестирования Kotlin вам понадобятся:

  • JUnit 5 — стандарт де-факто для модульных тестов.
  • MockK — библиотека для моков, созданная специально для Kotlin (никаких больше проблем с null и companion object).
  • Gradle или Maven — для управления зависимостями.

Добавьте в build.gradle.kts:

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testImplementation("io.mockk:mockk:1.13.8")
}

Готово. Теперь можно писать код, который не стыдно показать коллегам.

Пишем первый TDD-цикл: калькулятор с амбициями

Начнём с классики — калькулятора. Но не простого, а с поддержкой строковых операций. Задача: функция calculate принимает строку вида "2 + 3" и возвращает 5.

Шаг 1: Red — пишем падающий тест

Создаём тестовый класс:

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals

class CalculatorTest {

    @Test
    fun `should add two numbers`() {
        val calculator = StringCalculator()
        val result = calculator.calculate("2 + 3")
        assertEquals(5, result)
    }
}

Запускаем — тест падает, потому что класса StringCalculator ещё не существует. Красный сигнал — всё идёт по плану.

Шаг 2: Green — минимальный код для прохождения

Пишем ровно столько, чтобы тест прошёл. Никаких архитектурных изысков:

class StringCalculator {
    fun calculate(expression: String): Int {
        val parts = expression.split(" + ")
        return parts[0].toInt() + parts[1].toInt()
    }
}

Теперь тест зелёный. Ура! Но код пока что уродлив — он заточен только под сложение двух чисел.

Шаг 3: Refactor — делаем код красивым

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

class StringCalculator {
    fun calculate(expression: String): Int {
        val (a, op, b) = expression.split(" ")
        return when (op) {
            "+" -> a.toInt() + b.toInt()
            "-" -> a.toInt() - b.toInt()
            "*" -> a.toInt() * b.toInt()
            "/" -> a.toInt() / b.toInt()
            else -> throw IllegalArgumentException("Unknown operation: $op")
        }
    }
}

Запускаем тесты снова — они зелёные. Отлично! Но мы ещё не закончили — следующий тест добавит новые требования.

Второй цикл: добавляем обработку ошибок

Теперь представим, что пользователь ввёл "10 / 0". Наш код упадёт с ArithmeticException. Хотим возвращать null при делении на ноль.

Red: пишем тест на деление на ноль

@Test
fun `should return null when divide by zero`() {
    val calculator = StringCalculator()
    val result = calculator.calculate("10 / 0")
    assertEquals(null, result)
}

Тест падает — метод возвращает Int, а не Int?. Меняем сигнатуру.

Green: минимальные изменения

fun calculate(expression: String): Int? {
    val (a, op, b) = expression.split(" ")
    return when (op) {
        "+" -> a.toInt() + b.toInt()
        "-" -> a.toInt() - b.toInt()
        "*" -> a.toInt() * b.toInt()
        "/" -> {
            val divisor = b.toInt()
            if (divisor == 0) null else a.toInt() / divisor
        }
        else -> null
    }
}

Refactor: улучшаем читаемость

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

fun calculate(expression: String): Int? {
    return try {
        val (a, op, b) = expression.split(" ")
        performOperation(a.toInt(), op, b.toInt())
    } catch (e: Exception) {
        null
    }
}

private fun performOperation(a: Int, op: String, b: Int): Int? {
    return when (op) {
        "+" -> a + b
        "-" -> a - b
        "*" -> a * b
        "/" -> if (b == 0) null else a / b
        else -> null
    }
}

Тесты проходят. Код стал чище, а главное — мы уверены, что он работает.

Продвинутый TDD: работа с моками в Kotlin

В реальных проектах код редко бывает изолирован. Часто приходится работать с базой данных, API или файловой системой. Здесь на помощь приходит MockK — библиотека для мокирования в Kotlin.

Пример: сервис отправки у

Блоги

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