На доске команды горит зелёная плашка: покрытие 96 процентов. Через месяц тот же продукт ловит дефект в ветке обработки отказа, которую ни один тест ни разу не запускал. Формально всё хорошо, по существу стена покрашена только с парадной стороны. Цифра честная и одновременно бессмысленная, потому что она отвечает на вопрос, какие строки кто-то запускал, и молчит о том, сравнивал ли кто-то полученное с ожидаемым. Вокруг этой разницы и построен весь разговор о пользе метрики.
Метрика измеряется в четырёх видах и хранит свои сюрпризы
Инструменты считают не одну цифру, а четыре. Строчное покрытие показывает, какие строки исполнились. Покрытие выражений почти совпадает с ним. Функциональное фиксирует, что каждая функция вызывалась хотя бы раз. Ветвное самое придирчивое: оно спрашивает, заходил ли кто-нибудь в каждую развилку, включая else, тернарные операторы и короткие логические связки. Именно ветвное честнее всего, и именно его чаще всего не смотрят.
Разницу проще показать, чем описать. Возьмём функцию расчёта доставки:
function shipCost(weight, express) {
if (weight <= 0) throw new RangeError("вес не может быть нулевым");
return express ? 500 + weight * 20 : weight * 10;
}
it("считает экспресс-доставку", () => {
expect(shipCost(2, true)).toBe(540);
});
Единственный тест исполнил все три строки, строчное покрытие стопроцентное. Ветвное покажет половину: защита от нулевого веса сработала лишь в одну сторону, тернарник живёт только в ветке экспресса, а вариант обычной доставки и падение на нуле остались в темноте. Отчёт покажет это рядом цифр, поэтому ветвное покрытие и считают самым говорливым из четырёх.
Под капотом классического стека лежит семейство Istanbul: инструмент вплетает в код счётчики, тесты выполняются, отчёт собирается из ступеней зелёного, жёлтого и красного. В проектах на Node командуют npx jest --coverage либо nyc поверх своего раннера, а в конфигурации Jest с версии 27 доступен поставщик v8, который считает исполнение средствами движка и заметно быстрее.
// jest.config.js
module.exports = {
coverageProvider: "v8",
collectCoverageFrom: ["src/**/*.js", "!src/generated/**"],
coverageThreshold: {
global: { branches: 70, lines: 80 },
},
};
Порог в конфигурации превращает метрику в замок: ниже планки сборка краснеет. Работает ли такой замок на пользу, зависит от того, что за ним стоит, и об этом дальше.
Инструментарий метрики вырос в индустрию отчётов с красивыми картами
Семейство инструментов огромно и старо. Для C и C++ меряет gcov из комплекта компилятора, в Java стандартом де-факто стал JaCoCo, в мире Python командуют pytest с ключом cov, в JavaScript его место делят Istanbul, nyc и встроенный поставщик v8. Отчёты выпускают в форматах lcov и cobertura, панели анализа кода рисуют из них древовидные карты, где каждый файл раскрашен по шкале от красного до зелёного.
Обратная сторона красивых карт известна давно. Фаулер предупреждал о панелях, продвигающих невежество: когда число видно всем и постоянно, оно превращается в самостоятельную цель, а обсуждение качества проверок подменяется слежением за плашкой. Инструмент честен, его отчёты честны, обман начинается на уровне ожиданий, а не вычислений.
Настоящая польза живёт в отчёте, а не в общей цифре
Полезность метрики лежит в детализации. Отчёт показывает файлы и строки, мимо которых тесты прошли стороной, и это готовая карта рисков: обработчик обрыва сети, редкая ветка скидки для юридических лиц, резервный путь при отказе кэша. Мартин Фаулер сформулировал это ещё в апреле 2012 года: покрытие полезно как инструмент поиска непротестированных мест и почти бесполезно как числовое сведение о качестве проверок.
Из той же записки выходит вторая мысль, которую цитируют заметно реже: метрика работает как отрицательный индикатор. Низкий процент надёжно сигнализирует о беде, высокий не гарантирует ничего. Инженеры Google в заметке о практиках покрытия от августа 2020 года повторяют это другими словами: значительная часть ценности сведений о покрытии в том, чтобы подсвечивать не покрытое, а непокрытое.
Третья грань пользы касается изменённого кода. Проверка покрытия ветки с новой функциональностью при ревью отвечает на короткий вопрос, пришли ли к функции проверки, и не требует никаких споров о том, достаточно ли тестов в проекте вообще.
Практично и обратное движение. Старый модуль встраивают в систему, покрытие показывает, какие его функции вообще никем не вызываются, и половину из них удаляют без сожалений. Метрика работает как рентген перед перестановкой мебели: сначала снимок, потом решение. Тот же отчёт экономит время новичка: сразу видно, какие модули у команды в фокусе проверок, а какие никто не открывал годами.
Типовая сценка из практики: разработчик открывает отчёт после полугода разработки и находит модуль начислений, в котором красным залита половина развилок. Разбор показывает, что часть этих ветвей давно мертва, их никто не вызовет, и метрика сама вытащила наружу кладбище кода, которое давно стоило удалить. Это вторая профессия покрытия: поиск мёртвых участков, которые иначе жили бы годами.
Процент легко подделывают даже без злого умысла
Самый короткий путь к красивым цифрам выглядит безобидно:
it("обрабатывает заказ", () => {
buildOrder({ items: [], buyer: "гость" });
});
Строки исполнились, покрытие выросло, ни одно значение не проверено. Такой тест не поймает поломку никогда, он лишь регистрирует факт запуска. Инженеры Google прямо предупреждают про копирование проверок ради числа и про тесты с малой ценностью, которые плодят технический долг: их нужно поддерживать, ждать их выполнения, а пользы ноль.
Диапазон тоньше и дальше. Тест с утверждением, но на тривиальном случае, проверяет очевидное и съедает секунды сборки. Тест, привязанный к внутренним полям класса, ломается при каждом рефакторинге и тормозит разработку. Полнота в сто процентов Фаулеру внушает подозрение: такая цифра обычно означает, что проверки писали под отчёт, а не под уверенность.
Классическая гонка перед релизом выглядит узнаваемо: планка поднята до девяноста, фактический уровень стоит на восьмидесяти шести, и команда за неделю закрывает разницу проверками геттеров, сеттеров и констант перечислений. Число достигнуто, цена выражается в пустых секундах сборки и в иллюзии прибавившегося резерва качества. Хуже того, порог теперь занят, и настоящие дыры в обработке отказов прикрываются свежей, полностью бессодержательной массой проверок.
И главное ограничение, которое не обойти никакой дисциплиной: исполнение строки не равно проверке строки. Код может выполниться по всем правилам и вернуть неверный результат, а метрика останется зелёной, потому что она видит факт запуска, но не факт сравнения с ожиданием.
Два авторитетных вывода сходятся в одной точке
Фаулер в той же записке даёт ориентиры по уровням: меньше половины кодовой базы означает беду, значения в верхних восьмидесятых и девяностых сопутствуют вдумчивому тестированию, а стопроцентная цифра выглядит подозрительно. Его соавтор по теме Брайан Марик сказал это раньше и точнее любых методичек: он ожидает высокого покрытия, и иногда руководители его требуют, разница между этими двумя формулировками и есть вся суть вопроса.
Вторая позиция принадлежит инженерам Google. Их заметка 2020 года отвергает универсальное идеальное число: нужный уровень зависит от критичности кода, частоты его изменения и ожидаемого срока жизни. Там же назван побочный эффект жёстких целей: инженеры начинают воспринимать порог как галочку и останавливаются ровно на нём, даже когда разумно идти дальше. Обе школы согласны в главном: цифра годится для поиска дыр и бесполезна как цель сама по себе.
Сценка из практики поясняет разницу нагляднее методик. В проде ломается расчёт при отрицательной сумме возврата, совещание по разбору, и первый вопрос звучит, почему проверки пропустили. Отчёт отвечает: строки расчёта покрыты, зелёные, претензий нет. Дыра оказалась не в неисполненном коде, а в неиспробованном случае, отрицательная сумма просто не приходила никому в голову. Инженеры Google раскладывают такие истории на два класса: пробел в неисполненном пути покрытие действительно находит, для этого оно и придумано, а неиспробованный случай внутри исполненного кода метрике недоступен в принципе, здесь нужны обзор кода, продуманные граничные значения и фаззинг со своим наглым перебором входов. Лекарство у этих двух классов разное, и путать их значит лечить мигрень гипсом.
Фаулер добавляет пару штрихов о границах. Разработка через тесты полезна, но недостаточна для качества проверок. А признаком избытка он считает не долгий прогон, а дублирование: когда простая правка кода требует чрезмерной правки тестов, проблема в связанности проверок с устройством кода, а не в их количестве.
Порог в конфигурации дисциплинирует ровно до первого обмана
Из этих наблюдений вытекает рабочая дисциплина, при которой метрика приносит пользу, а не делает вид:
- в отчёте смотрят на непокрытые строки и ветви, а не на общую плашку над списком файлов;
- порог ставят на изменённый код, чтобы старый массив не давил на новые модули;
- каждый тест обязан содержать утверждение, вызов функции без проверки в набор не принимают;
- ветвное покрытие держат в фокусе, потому что непокрытые развилки прячут самые дорогие ошибки;
- мёртвый код удаляют, вместо того чтобы подгонять на него проверки ради процента;
- число не выносят руководству как цель, иначе метрика честно превратится в галочку.
Инкрементальный порог заслуживает отдельного слова. Глобальный потолок в восемьдесят пять процентов на крупном проекте превращает каждую мелкую правку в бухгалтерию: старый код лежит ниже планки, новые изменения виноваты по умолчанию. Порог на изменённом коде работает иначе: принёс функцию, принеси и проверки, а десятилетний массив никто не трогает до поры до времени. Это честная сделка между дисциплиной и здравым смыслом, и она держится годами, пока обе стороны её помнят.
Практичная настройка планки: на пару пунктов ниже фактического уровня команды. Порог выше реальности наказывает за каждую мелочь и учит обходить его обманом, порог заметно ниже превращается в украшение. Зазор в два-три процента страхует от случайных просадок и не даёт метрике превратиться в палку.
Мутационное тестирование проверяет качество там, где процент молчит
На вопрос, проверяют ли тесты хоть что-нибудь, отвечает не покрытие, а мутационное тестирование. Инструмент вносит в код мелкие повреждения: меняет знак сравнения, удаляет инкремент, подменяет возвращаемое значение, после чего гоняет набор. Хороший тест на мутанте падает, плохой проходит невозмутимо. Доля убитых мутантов показывает реальную силу проверок, и картина иногда перепахивается целиком: при полном строчном покрытии выживает заметная часть мутантов.
В проектах на JavaScript популярен инструмент Stryker, запуск занимает одну команду:
npx stryker run
Конфигурация минимальна:
{
"testRunner": "jest",
"coverageAnalysis": "perTest",
"reporters": ["html", "clear-text", "progress"]
}
Пример выжившего мутанта выглядит житейски: тест убеждается, что функция вернула список, но не проверяет порядок. Мутант ломает сортировку, набор зеленеет дальше, и у команды появляется конкретное место для нового утверждения вместо абстрактного желания больше покрытия.
У мутационного подхода есть цена: каждая мутация означает прогон хотя бы части набора, на больших проектах это часы. Поэтому мутантов гоняют по ночам, только на изменённых модулях либо с анализом на уровне теста, когда инструмент запускает лишь те проверки, которые касаются повреждённой строки.
Вывод напрашивается сам и совпадает с выводом обеих упомянутых школ. Покрытие остаётся полезной метрикой, пока его читают как карту: где темно, туда и идут с проверками. Как только цифра становится целью, она начинает производить исполненные строки, а не уверенность. Вопрос недели вида какой у нас процент стоит заменить вопросом, какие участки мы не трогали и что об этом думаем. Первый вариант звучит солиднее на совещании, второй реально снижает число дефектов, а между этими вариантами пролегает вся разница между отчётом и инженерией.