Что такое покрытие кода тестами и сколько его действительно нужно
Основы4 минРедакция Innovialabs
Коротко
Покрытие кода показывает, какие части кода выполнялись во время тестов, а не были ли они проверены; используйте его, чтобы находить непротестированные места, требовать покрытия нового кода в CI и держать умеренный порог вместо погони за 100%.
Что измеряет покрытие кода
Покрытие кода (code coverage) — доля кода, которая выполнилась во время прогона тестов. Инструмент инструментирует программу, запускает тесты и сообщает, какие строки, ветви и функции были достигнуты.
Главное: покрытие показывает, что выполнилось, а не что проверено. Тест, который вызывает функцию без единой проверки (assert), всё равно повышает покрытие.
Основные виды покрытия
| Вид | Что считается | Слабое место |
|---|---|---|
| По строкам (statement) | Каждая выполненная строка | Строка с if a and b считается покрытой, даже если одно из условий ни разу не было ложным |
| По ветвям (branch) | Каждый исход каждого решения: обе стороны if, каждый case, вход в цикл и его пропуск | Не проверяет все комбинации условий |
| По функциям | Каждая функция вызвана хотя бы раз | Один вызов почти ничего не говорит о граничных случаях |
Покрытие по ветвям — самая полезная метрика на каждый день. Она ловит классическую дыру: ветку else, обработчик ошибки, ранний return, до которых тесты не доходят.
def discount(user):
if user.is_vip:
return 0.2
return 0.0
Один тест с VIP-пользователем оставляет последнюю строку невыполненной и покрывает только одну из двух ветвей. Тест для обычного пользователя закрывает обе дыры.
Инструменты по экосистемам
- Python — coverage.py. Запустите
coverage run --branch -m pytest, затемcoverage reportилиcoverage html. С pytest плагинpytest-covдаётpytest --cov=src --cov-branch. - JavaScript и TypeScript — Istanbul и c8. Istanbul (через
nyc) инструментирует код; c8 использует покрытие, встроенное в движок V8. В Jest есть встроенный--coverage, а Vitest поддерживает провайдеры V8 и Istanbul. - Java и Kotlin — JaCoCo. Работает как Java-агент, подключается к Maven и Gradle, считает строки, ветви и другие счётчики.
- Другие. В Go есть
go test -cover, в .NET обычно используют Coverlet, и почти у каждого языка есть стандартный вариант.
Все они умеют выдавать отчёты в распространённых форматах — Cobertura XML или LCOV, которые понимают CI-сервисы и инструменты код-ревью.
Почему 100% покрытия вводит в заблуждение
- Выполнение — не проверка. Тесты без осмысленных assert дают полное покрытие и ничего не ловят.
- Оно не видит отсутствующий код. Покрытие измеряет написанное. У забытой валидации или необработанного случая нет строк, которые можно покрыть.
- Комбинации ускользают. Можно покрыть каждую строку и ветвь, а конкретное сочетание входных данных всё равно упадёт.
- Оно искажает приоритеты. Команда начинает тестировать геттеры, сгенерированный код и тривиальные обёртки ради цифры, а сложная логика получает поверхностные тесты.
- Стоимость резко растёт в конце. Последние проценты — обычно пути ошибок, которые трудно вызвать и которые иногда лучше проверять на другом уровне.
Высокое покрытие — нормальный побочный эффект хороших тестов. Как самоцель оно поощряет неправильное поведение.
Разумные пороги
Универсального числа нет. Вместо одной цифры на весь проект думайте слоями:
- Умеренный общий порог, ниже которого сборка не должна опускаться, выбранный исходя из текущего состояния проекта. Частая отправная точка — где-то в районе 70–80% с поправкой на проект.
- Более высокие ожидания для критичного кода: платежи, права доступа, миграции данных, бизнес-правила.
- Более низкие — для связующего кода: конфигурация, простые адаптеры, вёрстка UI.
- Исключите то, что не должно учитываться: сгенерированные файлы, миграции, вспомогательный код тестов. Исключения должны быть явными и проходить ревью.
- Поднимайте постепенно. Повышайте порог по мере роста покрытия, а не ставьте сразу большую цель.
Покрытие в CI
- Запускайте тесты с покрытием на каждый pull request и сохраняйте отчёт как артефакт сборки.
- Роняйте сборку ниже порога. В coverage.py есть
--fail-under, в Jest —coverageThreshold, в c8 —--check-coverage, в JaCoCo — цельcheckс правилами. - Проверяйте покрытие изменённого кода. Самое практичное правило: новые и изменённые строки должны быть покрыты. Инструменты вроде
diff-coverили интеграции с код-ревью показывают это прямо в pull request. - Смотрите на непокрытые строки, а не на процент. Настоящая ценность отчёта — указать на ветви, которые никто не проверил.
- Сочетайте с другими сигналами. Мутационное тестирование, код-ревью и ошибки в продакшене показывают, ловят ли тесты баги на самом деле.
# pyproject.toml
[tool.coverage.run]
branch = true
source = ["src"]
[tool.coverage.report]
fail_under = 75
show_missing = true