Покрытие (coverage) отвечает ровно на один вопрос: какие строки кода исполнились во время прогона тестов. Это важно понять с самого начала, потому что вокруг метрики выросло опасное недоразумение. Покрытие показывает, что строка была выполнена, - и ничего не говорит о том, была ли проверена её правильность. Строка может отработать внутри теста без единого assert на её результат и всё равно посчитается покрытой.
В Vitest покрытие включают провайдером. По умолчанию это v8 - он берёт данные прямо у движка V8, быстрый и не требует инструментирования кода. Репортеры выбирают под задачу: text печатает таблицу в терминал, html даёт кликабельный отчёт с подсветкой непокрытых строк, lcov - машинный формат для CI и внешних сервисов. Обычно включают сразу несколько, каждый под своего потребителя.
Пороги (thresholds) задают минимум, ниже которого сборка падает. Их роль - именно пол, страховка от сползания вниз, а не цель, к которой тянутся. Разница принципиальна: порог в 80% не значит, что 80 - хорошая оценка; он значит, что опускаться ниже 80 нельзя без явного решения. Как цель процент покрытия вреден - он толкает писать тесты ради строк, а не ради поведения.
Куда честнее строк ветвления. Строчное покрытие легко обмануть: один тест на счастливый путь прогонит функцию сверху вниз и даст высокий процент, ни разу не зайдя в else, в ветку ошибки, в обработку пустого входа. Branch coverage показывает, все ли развилки пройдены, и именно оно вскрывает непроверенные ветки - те самые места, где обычно и прячутся баги.
Но и стопроцентное ветвление не гарантирует, что тесты что-то ловят. Это доказывает мутационное тестирование: инструмент вносит в код мелкую поломку - меняет > на >=, + на -, выкидывает вызов - и смотрит, покраснеет ли хоть один тест. Если после порчи все тесты зелены, значит, строка исполняется, но её результат никто не проверяет, и покрытие по ней - пустая цифра.
Пример с границей нагляден. Условие if (age > 18) и условие if (age >= 18) дают разное поведение ровно в одной точке - при age равном 18. Тест, который бьёт только по age 25 и age 10, покроет строку и обе ветки на сто процентов, но замену > на >= не заметит: граница осталась незащищённой. Мутант выживает - и честно сообщает, что теста на границу нет.
Отсюда трезвый взгляд на метрику. Покрытие полезно как индикатор непройденного: красные строки в html-отчёте прямо показывают, куда тест ещё не заглянул, и это ценно. Но высокий процент - не доказательство качества; он лишь верхняя граница того, что тесты в принципе могли проверить. Настоящую силу набора измеряют тем, ловит ли он внесённые поломки, а не тем, сколько строк подсветилось зелёным.
Типичный провал - гнаться за цифрой: дописать тесты без assert-ов, лишь бы строки покрасились, и получить высокий процент при бессильном наборе. Второй провал - смотреть только на строки, игнорируя ветки: отчёт рисует 90%, а половина условий ни разу не прошла обеими сторонами. Держите пороги полом, читайте ветвление, а качество проверок сверяйте мутационным прогоном, а не процентом.
// vitest.config.ts -> test:
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
thresholds: { // пол, не цель
lines: 80,
branches: 80, // ветки честнее строк
functions: 80,
},
}// Оба теста дают 100% строк и веток...
expect(canVote(25)).toBe(true)
expect(canVote(10)).toBe(false)
// ...но замену age > 18 на age >= 18 не ловят: нет теста на границу 18.
// Мутант выживает -> граница не защищена.