Прямой ответ
Покрытие кода (code coverage) показывает, какая часть исходного кода выполнялась при прогоне тестов. Это метрика исполнения, а не проверки: она говорит, какие строки запускались, но не говорит, проверялся ли результат.
Виды покрытия:
| Вид | Что измеряет | Строгость |
|---|---|---|
| Line / statement | Какие строки выполнены | Низкая |
| Branch | Какие ветвления пройдены в обе стороны | Средняя |
| Condition | Какие условия в сложных выражениях принимали оба значения | Высокая |
| Path | Какие комбинации путей через функцию пройдены | Очень высокая |
| Function | Какие функции вызывались хотя бы раз | Очень низкая |
На практике используют два первых. Строковое покрытие проще читать, ветвлевое ловит непроверенные else.
pip install pytest-cov
pytest --cov=src --cov-branch --cov-report=term-missing
Вывод term-missing показывает номера строк, которые не выполнялись, — это самая полезная часть отчёта.
Главное ограничение: тест без утверждений даёт полное покрытие.
def test_discount():
apply_discount(100, 0.5) # строка выполнена, покрытие 100 %
# результат не проверяется: любое изменение расчёта пройдёт незамеченным
Именно поэтому метрика покрытия сама по себе не характеризует качество тестов. Её разумное использование — находить непроверенные участки, а не ставить цели по процентам.
Что ещё не видно в покрытии:
- Качество утверждений. Строка выполнена, но результат не сверен.
- Граничные значения. Ветвление пройдено в одну сторону; значение на границе не проверялось.
- Корректность данных. Тест мог пройти по всем строкам с нереалистичным входом.
- Поведение при ошибках. Обработчики исключений часто остаются непокрытыми, потому что их трудно вызвать.
Практика: как пользоваться метрикой с пользой
1. Смотрите на непокрытое, а не на процент. Список строк, которые не выполнялись, — готовый план работы. Обычно там оказываются ветки обработки ошибок и редкие условия — именно они ломаются в продакшене.
2. Включайте ветвлевое покрытие. Строковое покрытие показывает if как выполненный, даже если else никогда не срабатывал. --cov-branch это выявляет.
3. Порог в CI — на diff, а не на весь проект. Требование «покрытие не ниже 85 % по всему проекту» стимулирует писать пустые тесты ради цифры. Требование «новые и изменённые строки покрыты» работает лучше: оно адресное и не наказывает за старый код.
# pyproject.toml
[tool.coverage.report]
fail_under = 80
show_missing = true
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
4. Исключайте неинтересное осознанно. Сгенерированный код, точки входа, ветки для отладки. Каждое исключение должно быть объяснимо, иначе отчёт теряет смысл.
5. Используйте покрытие для ускорения других инструментов. Мутационное тестирование запускает для каждого мутанта только те тесты, которые покрывают изменённую строку; без данных покрытия прогон дороже в разы. Методика — в статье про мутационное тестирование.
6. Различайте уровни. Покрытие, собранное только unit-тестами, и покрытие полным прогоном — разные числа. Для оценки защищённости бизнес-логики интереснее первое — о распределении уровней в статье про пирамиду тестирования.
Рекомендации:
- Не превращайте покрытие в KPI команды. Как только за цифру спрашивают, появляются тесты без утверждений. Метрика полезна как индикатор, а не как цель.
- Сочетайте с другими сигналами. Покрытие отвечает «выполнялось ли», мутационный анализ — «заметили бы ошибку», flaky-статистика — «можно ли доверять прогону».
- Проверяйте обработчики ошибок. Отдельный тест на каждую ветку
except— дешёвый способ поднять реальную защищённость там, где она важнее всего. - Пересматривайте порог. Порог, который команда обходит, хуже отсутствия порога: он создаёт иллюзию контроля.
- Смотрите отчёт в код-ревью. Строки без покрытия в новом коде — вопрос, который стоит задать до слияния, а не после инцидента.
Практический сценарий: 92 % покрытия и пропущенная ошибка в возврате средств
Контекст: модуль возвратов имел покрытие 92 % и считался благополучным. В проде обнаружили, что повторный возврат по тому же заказу проходит успешно.
- Диагностика. Проверка показала: ветка
if order.refund_exists: raise ...была покрыта — строка выполнялась в тесте. Но утверждение проверяло не отказ, а сам факт вызова функции. - Что ещё нашли. Разбор отчёта
term-missingпо модулю показал 14 непокрытых строк, из них 9 — в обработчиках ошибок платёжного провайдера. - Действия. (а) Исправили тест на повторный возврат: теперь проверяется исключение и отсутствие второго списания; (б) добавили тесты на все ветки обработки ошибок провайдера; (в) включили ветвлевое покрытие в CI с порогом на изменённые строки; (г) запустили мутационный анализ по модулю — выжило 31 % мутантов, что подтвердило слабость утверждений.
- Результат. Покрытие выросло незначительно — с 92 до 94 %, но мутационный показатель поднялся с 69 до 88 %. Именно второй показатель связан с числом дефектов, доходящих до продакшена.
Вывод: разница в два процентных пункта оказалась важнее, чем казавшееся благополучным высокое покрытие. Метрику стоит читать как карту непроверенных мест, а не как оценку качества.
FAQ
Какой процент покрытия считается хорошим? Универсального числа нет. Для бизнес-логики разумно 80–90 % с обязательным покрытием веток ошибок; для тонких слоёв адаптеров и сгенерированного кода высокий процент недостижим и не нужен. Важнее отсутствие непокрытых критичных веток.
Почему покрытие может быть 100 %, а ошибки remain? Потому что покрытие измеряет исполнение строк, а не проверку результатов. Тест, который вызывает функцию без утверждений, даёт полное покрытие и нулевую защиту.
Стоит ли блокировать слияние при падении покрытия? Разумно блокировать при снижении покрытия изменённых строк. Глобальный порог на весь проект часто блокирует по не связанным с изменением причинам и провоцирует обходные приёмы.
Как измерять покрытие в асинхронном и многопоточном коде? Инструменты поддерживают такой сбор, но требуется включить параллельный режим и объединить результаты прогонов. Иначе покрытие оказывается заниженным, потому что часть процессов не учитывается.
Источники: coverage.py docs, pytest docs, Wikipedia (code coverage) — ссылки в шапке.