Coverage - это доля кода, которая выполнилась хотя бы раз за время прогона тестов. Инструмент оборачивает исходник счётчиками и отмечает, куда дошло управление. Отсюда несколько чисел, которые стоит различать. Line coverage - сколько строк исполнилось. Function coverage - сколько функций были вызваны. Branch coverage - сколько веток условий реально прошли: и ветка if, и ветка else, обе стороны тернарного оператора, оба исхода короткого замыкания в a || b. Эти числа собираются сами, поэтому их так соблазнительно принять за оценку качества тестов. Это и есть первая ловушка.
Наивный ход мысли выглядит разумно: раз число растёт, когда тестов больше, значит выше число - лучше набор. Отсюда цель '100 процентов' и гейт в CI, который заворачивает пул-реквест ниже порога. Один показатель, понятная планка, ощущение движения - менеджеру удобно. Проблема в том, что метрика измеряет не то, что кажется.
Coverage фиксирует выполнение, а не проверку. Возьмём сервис оформления заказа с функцией applyDiscount, которая считает скидку по сумме корзины. Тест, который просто вызывает applyDiscount(order) и на этом заканчивается, добавит строкам и функции полное покрытие - управление ведь через них прошло. Но ни один expect не сравнил результат с ожиданием. Строка выполнилась; никто не посмотрел, что она вернула. Покрытие зелёное, а контракт не защищён ничем.
Поэтому голый процент строк вводит в заблуждение сильнее, чем branch coverage. Пусть applyDiscount делает ранний возврат для пустой корзины, а дальше ветвится по порогу суммы. Один тест на обычный заказ прогонит почти все строки и покажет высокий line coverage - но ветку с пустой корзиной и границу порога не тронет вовсе. Branch coverage это видит: непройденные ветки прямо названы. Как быстрый ориентир ветки полезнее строк, потому что баги живут именно в непройденных развилках.
Пороги задаются в coverageThreshold и работают как пол, а не как цель. Глобальный минимум держит набор от сползания вниз, а для критичных областей планку поднимают адресно: домен, где живут деньги и правила, заслуживает строже, чем клей вокруг него. coverageProvider: 'v8' берёт данные напрямую из движка, без инструментирования через Babel. Ключевое слово - пол: порог не даёт стать хуже, но сам по себе не делает тесты сильнее.
Силу проверок мерит не coverage, а mutation testing. Инструмент берёт исходник и вносит маленькие поломки - мутанты: меняет > на >=, убирает строку, переворачивает булеву константу. После каждой правки он заново гоняет набор. Если тесты позеленели на сломанном коде, мутант выжил - значит, проверки слишком слабы, чтобы заметить эту поломку. Классический пример: замена > на >= на границе порога скидки должна ронять набор. Если не роняет, пограничный контракт не защищён, сколько бы ни было покрытия.
У всего этого есть цена. Mutation testing перезапускает набор десятки раз и потому медленный - его держат для домена и запускают ночью или на релизной ветке, а не на каждый коммит. Слишком высокий обязательный порог тоже вредит: он подталкивает добивать число тестами без единого assert, лишь бы гейт пропустил. Так и выглядит реальный провал: команда прибивает 100 процентов строк, дописывает вызовы без проверок, отчёт сияет - а в проде заказ на ровно пороговую сумму получает не ту скидку, потому что границу >= никто не проверил. Coverage показал, что код выполнили. Что его проверили - он не показывал никогда.
coverageProvider: 'v8',
coverageReporters: ['text', 'html', 'lcov'],
coverageThreshold: {
global: {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
'./src/domain/': {
branches: 90,
lines: 95,
},
}