Вступление: Почему «зелёные» тесты — это иллюзия?
Вы запускаете тесты, видите заветную зелёную полосу и чувствуете себя супергероем. Но что, если я скажу, что ваши тесты могут быть бесполезны? Что они проверяют только одну строчку из десяти, а остальные девять — это «мёртвая зона», где живут баги? Именно здесь на сцену выходит покрытие кода тестами (code coverage). Это не просто цифры для отчёта, а ваш личный радар, показывающий, насколько хорошо тесты «покрывают» ваш Kotlin-код.
В этой статье мы разберём, как превратить покрытие кода в Kotlin из скучной метрики в мощный инструмент. Вы узнаете, какие инструменты реально работают, что означают проценты и как не загнать себя в ловушку «100% покрытия». Поехали!
Что такое покрытие кода и почему это важно для Kotlin?
Представьте, что ваш код — это поле для гольфа, а тесты — игроки. Покрытие кода — это карта, показывающая, по каким участкам поля прошли игроки. Если игроки обходят стороной опасный бункер (сложный алгоритм), вы об этом узнаете.
В мире Kotlin, где мы часто используем корутины, sealed классы и extension-функции, наивные тесты могут легко пропустить целые ветки логики. Метрики покрытия помогают ответить на три вопроса:
- Какой процент строк кода был выполнен во время тестов?
- Какие условия (if/else, when) были проверены?
- Какие функции остались без внимания?
Без этих метрик вы рискуете выпустить в прод код, который работает «только по понедельникам».
Инструменты для измерения покрытия в Kotlin: Топ-3
В экосистеме Kotlin есть три главных героя, которые помогут вам измерить code coverage. Давайте познакомимся с каждым.
1. JaCoCo (Java Code Coverage) — Ветеран
Самый популярный инструмент. Работает на уровне байт-кода, поэтому подходит и для Java, и для Kotlin. Его главный плюс — богатая статистика и интеграция с CI/CD.
Как подключить в Gradle (Kotlin DSL):
plugins {
id("jacoco")
}
tasks.jacocoTestReport {
dependsOn(tasks.test) // Запускаем тесты перед отчётом
reports {
xml.required.set(true)
html.required.set(true) // Генерируем красивый HTML-отчёт
}
}
После запуска ./gradlew jacocoTestReport вы получите HTML-файл, где красным подсвечены непокрытые строки. Удобно, правда?
2. Kover — Современный игрок
Специально разработан для Kotlin. В отличие от JaCoCo, Kover работает на уровне AST (абстрактного синтаксического дерева), что делает его более точным для Kotlin-специфичных конструкций (например, inline-функций).
Подключение Kover:
plugins {
id("org.jetbrains.kotlinx.kover") version "0.7.3"
}
kover {
excludeSourceSets {
names("test") // Исключаем сами тесты из отчёта
}
}
Kover генерирует отчёты в форматах HTML, XML и даже в виде бейджей для GitHub. Это идеальный выбор для чистого Kotlin-проекта.
3. IntelliJ IDEA Coverage — Для быстрой проверки
Встроенный инструмент. Запускаете тесты с покрытием (зелёный значок «Run with Coverage») и видите раскрашенный код прямо в IDE. Удобно для локальной разработки, но не для CI.
Совет: Используйте IntelliJ для быстрой итерации, а JaCoCo или Kover — для финального отчёта.
Метрики покрытия кода: Что означают эти проценты?
Когда вы видите «80% покрытия», это звучит круто. Но 80% чего? Давайте разберём основные метрики, которые выдают все инструменты.
Line Coverage (Покрытие строк)
Самая простая метрика. Сколько строк исходного кода было затронуто тестами. Если строка не выполнилась ни разу — она считается непокрытой.
Пример:
fun process(value: Int): String {
if (value > 0) { // Строка 1
return "Positive" // Строка 2
} else {
return "Negative" // Строка 3
}
}
Если вы протестировали только value > 0, то строка 3 останется красной. Line coverage покажет 66%.
Branch Coverage (Покрытие ветвей)
Более умная метрика. Она проверяет, были ли пройдены все возможные пути выполнения (true/false для if, каждый case для when).
В примере выше branch coverage будет 50%, так как проверена только одна ветка (true), а другая (false) — нет.
Method Coverage (Покрытие методов)
Процент функций, которые были вызваны хотя бы один раз. Полезно для поиска «мёртвого кода».
Примеры реального кода на Kotlin: Играем с покрытием
Давайте напишем простой класс и посмотрим, как ведут себя метрики.
Код, который нужно протестировать
class Calculator {
fun calculate(a: Int, b: Int, operation: String): Int {
return when (operation) {
"add" -> a + b
"subtract" -> a - b
else -> throw IllegalArgumentException("Unknown operation")
}
}
fun isEven(number: Int): Boolean {
return number % 2 == 0
}
}
Плохой тест (низкое покрытие)
class CalculatorTest {
@Test
fun `test add`() {
val calc = Calculator()
assertEquals(5, calc.calculate(2, 3, "add"))
}
}
Результат: Line coverage ~30%. Мы не проверили subtract, else и isEven. Branch coverage по when — 33% (проверен только один case).
Хороший тест (высокое покрытие)
class CalculatorTest {
@Test
fun `test add`() {
val calc = Calculator()
assertEquals(5, calc.calculate(2, 3, "add"))
}
@Test
fun `test subtract`() {
val calc = Calculator()
assertEquals(1, calc.calculate(3, 2, "subtract"))
}
@Test(expected = IllegalArgumentException::class)
fun `test unknown operation`() {
val calc = Calculator()
calc.calculate(1, 1, "multiply")
}
@Test
fun `test isEven`() {
val calc = Calculator()
assertTrue(calc.isEven(4))
assertFalse(calc.isEven(3))
}
}
Результат: Line coverage