4.2 Тестовое покрытие и эффективность тестов
Обзор и мотивация
Тестовое покрытие измеряет процент кода, выполненного набором тестов: покрытие строк, покрытие ветвлений или более строгое покрытие путей. Это одна из самых широко отслеживаемых метрик во всей этой книге, дёшево вычислять, легко визуализировать как единственный процент и, следовательно, одна из самых часто подделываемых, именно так, как тема 1.2 предсказывает для любой метрики, становящейся целью. Набор тестов может достичь высокого покрытия, не проверяя почти ничего значимого, потому что покрытие измеряет, выполнился ли код во время прогона теста, а не проверил ли тест на самом деле, что код вёл себя правильно.
Этот разрыв между покрытием и подлинной эффективностью тестов не малозначительная сноска; это центральная забота этой темы. Тест, вызывающий функцию и ничего не утверждающий о её результате, увеличивает покрытие идентично тесту, тщательно проверяющему поведение функции по граничным случаям. Исправление, которое рекомендует эта тема, мутационное тестирование, намеренно вводит небольшие искусственные дефекты в код и проверяет, действительно ли набор тестов их ловит, прямой ответ на этот разрыв, и эта тема относится к нему как к необходимому дополнению покрытия, а не опциональной добавке.
Для крупных команд цели покрытия часто принимаются по всей организации как ворота качества, именно тот вид стимулируемой высоковидимой метрики, против которой предупреждает тема 1.2 как наиболее подверженной манипулированию. Корпоративные и государственные организации, устанавливающие общее требование процента покрытия без парной проверки эффективности, фактически стимулируют именно тот паттерн манипулирования порогом, который описывает эта книга: тривиальные тесты, написанные исключительно для достижения числа, без соответствующего улучшения фактического предотвращения дефектов.
Ключевые принципы
- Покрытие измеряет выполнение, а не проверку. То, что строка выполняется тестом, ничего не говорит о том, проверил ли тест что-либо значимое о ней.
- Цель покрытия без проверки эффективности это учебниковая установка закона Гудхарта (тема 1.2): число улучшается, пока подлинное качество нет.
- Мутационное тестирование необходимое дополнение покрытия, а не замена; используйте оба вместе.
- Покрытие более полезно как пол, чем как цель для максимизации. Низкое число раскрывает подлинно непротестированный код; погоня за 100% часто производит убывающую или отрицательную отдачу.
- Покрытие критических путей важнее равномерного общего покрытия. Не весь код несёт одинаковый риск при сбое.
Рекомендации
Используйте покрытие, чтобы находить непротестированный код, а не как цель для максимизации
Относитесь к отчёту о покрытии в первую очередь как к карте того, что вообще не имеет теста, что подлинно полезная информация, а не как к оценке, которую нужно толкать к 100%. Код с нулевым покрытием это реальный пробел, достойный закрытия; предельная ценность толкания покрытия с 85% до 95% обычно гораздо ниже и часто не стоит усилий, которые это требует, особенно если эти усилия производят низкоценные тесты только для достижения более высокого числа.
Сочетайте каждую цель покрытия с мутационным тестированием
Инструменты мутационного тестирования автоматически вводят небольшие дефекты в ваш код, переворачивая оператор сравнения, меняя граничное условие, а затем запускают ваш набор тестов против каждой мутированной версии. Набор тестов, «убивающий» (проваливающийся против) большинство мутантов, подлинно проверяет поведение; набор тестов с высоким покрытием строк, но низкой долей убитых мутантов, выполняет код, значимо его не проверяя. Это сочетание единственная самая эффективная страховочная метрика против манипулирования целью покрытия, и эта книга рекомендует его как стандартную практику, а не продвинутую или опциональную технику.
Приоритизируйте покрытие и мутационное тестирование на критических путях в первую очередь
Не весь код несёт одинаковый риск. Путь обработки платежей, проверка аутентификации или скрипт миграции данных заслуживают гораздо более строгого тестирования, чем редко используемый административный отчёт. Вместо того чтобы преследовать равномерное покрытие по всей кодовой базе, определите ваши пути кода с наивысшим риском, наивысшими последствиями и сконцентрируйте там усилия и покрытия, и мутационного тестирования в первую очередь, принимая более низкое покрытие на подлинно низкорисковом коде как сознательный информированный компромисс, а не недосмотр.
Следите за конкретными паттернами манипулирования покрытием
Самые распространённые способы манипулирования покрытием, как только оно становится целью, включают: тесты, вызывающие функцию, но ничего значимого не утверждающие о результате (манипулирование порогом из темы 1.2, применённое к этой метрике), отключение или удаление проваливающихся тестов вместо исправления лежащей в основе проблемы и полное исключение трудно тестируемого кода из вычисления покрытия вместо обращения к тому, почему его трудно тестировать. Периодически проверяйте выборку тестов напрямую, читая их фактические утверждения, а не доверяя одному проценту покрытия.
Устанавливайте пол покрытия, а не потолок покрытия, в вашем конвейере CI
Настройте ваш конвейер сборки на сбой, если покрытие падает ниже согласованного пола для нового кода, предотвращая регрессию, а не требуя, чтобы каждое изменение толкало общее число выше. Это различие важно: пол защищает от отката назад, не создавая то же неумолимое восходящее давление, производящее низкоценные тесты, написанные исключительно для того, чтобы постепенно поднять число дальше.
Компромиссы: плюсы и минусы
| Подход | Плюсы | Минусы |
|---|---|---|
| Только процент покрытия | Дёшево, просто, широко поддерживается инструментарием | Легко подделывается; измеряет выполнение, а не проверку |
| Покрытие плюс мутационное тестирование | Проверяет, что тесты на самом деле проверяют поведение, сопротивляется манипулированию | Более вычислительно затратно; требует инвестиции в инструментарий |
| Единая цель покрытия по всей кодовой базе | Просто сформулировать и применить | Тратит усилия на низкорисковый код; недоинвестирует относительно риска в другом месте |
| Покрытие на основе риска, с критическими путями в первую очередь | Концентрирует усилия там, где важнее всего | Требует суждения для правильного определения подлинно критических путей |
Центральное напряжение: простота против честности. Единственный процент покрытия легко сообщать и легко устанавливать как цель, но эта простота именно то, что делает его так легко подделываемым, как только он становится стимулируемым числом. Разрешайте это напряжение, принимая добавленную сложность мутационного тестирования и приоритизации на основе риска как цену честного сигнала, и явно сообщая вашей команде, почему более низкое общее число покрытия, правильно сконцентрированное на критических путях и подкреплённое сильной долей убитых мутантов, более ценно, чем более высокое более равномерно распределённое, но менее эффективно проверенное.
Вопросы для обсуждения в команде
Какова наша доля убитых мутантов на наших путях кода с наивысшим риском, и как она сравнивается с нашим процентом покрытия на том же коде? Большой разрыв между высоким числом покрытия и низкой долей убитых мутантов самый ясный возможный признак того, что одно покрытие не говорит вам того, что вы думаете, оно вам говорит.
Писали ли мы когда-либо тест в первую очередь для увеличения числа покрытия, с малой реальной мыслью о том, что он должен проверять? Будьте здесь честны; это происходит чаще, чем командам нравится признавать, особенно под давлением сроков, когда ворота покрытия блокируют слияние.
Сконцентрированы ли наши усилия покрытия на наших путях кода с наивысшим риском, или распределены равномерно независимо от последствий сбоя этого кода? Сопоставьте ваше текущее распределение покрытия с честной оценкой риска вашей кодовой базы и поищите несоответствие.
Отключали или удаляли ли мы когда-либо проваливающийся тест вместо исправления лежащей в основе проблемы, которую он раскрыл? Это одна из самых разрушительных форм манипулирования покрытием, потому что она активно удаляет реальную защиту, пока сообщаемое число покрытия может едва двинуться.
Обеспечивает ли наш конвейер CI пол покрытия для нового кода, или он толкает к всё более высокому потолку независимо от убывающей отдачи? Обсудите, создаёт ли ваш текущий дизайн ворот правильный стимул, защиту от регрессии, или неправильный, неумолимое восходящее давление, вознаграждающее низкоценное наполнение тестов.
Какой код в нашей кодовой базе исключён из вычисления покрытия, и оправдано ли это исключение или оно скрывает реальный пробел тестирования? Просмотрите вашу фактическую конфигурацию исключений; этот список обычно тихо растёт со временем без того, чтобы кто-либо пересматривал, оправдано ли каждое исключение всё ещё.
Отраслевой взгляд
Стартап. Формальные цели покрытия часто не нужны так рано; сосредоточьте усилия написания тестов напрямую на ваших самых рискованных самых критически важных для бизнеса путях кода (обычно платежи или логика основного рабочего процесса), а не преследуйте общий процент по кодовой базе, всё ещё быстро меняющейся и, возможно, скоро существенно переписываемой в любом случае.
Малый бизнес. Большинство платформ CI сообщают покрытие автоматически с минимальной стоимостью настройки; используйте его в первую очередь, чтобы заметить полностью непротестированный критический код, а не гонитесь за конкретным целевым процентом, и рассмотрите мутационное тестирование только тогда, когда у вас будет инженерная мощность для действий на основе того, что оно раскрывает.
Корпорация. Общие общеорганизационные цели покрытия распространённая и значимая ошибка в этом масштабе, поскольку они стимулируют именно то манипулирование, которое описывает эта тема, по десяткам команд одновременно. Установите ожидания покрытия на основе риска, варьирующиеся по критичности сервиса, и инвестируйте в инфраструктуру мутационного тестирования конкретно для ваших систем с наивысшим риском.
Государство. Требования покрытия иногда появляются в документации закупок или соответствия требованиям как грубый легко определяемый представитель обеспечения качества. Где возможно, сочетайте любой контрактно требуемый процент покрытия с требованием мутационного тестирования или эффективности на основе дефектов, чтобы контрактный стимул непреднамеренно не вознаграждал именно то низкоценное наполнение тестов, против которого предупреждает эта тема.
Примеры
Корпорация. Руководство платформы электронной коммерции установило общекомпанейское требование покрытия 95% для всего нового кода, применяемое как жёсткие ворота CI. Аудит два года спустя, вызванный волной производственных дефектов в предположительно хорошо протестированном коде, нашёл долю убитых мутантов ниже 40% по большей части кодовой базы: команды писали тесты, выполнявшие пути кода, значимо не утверждая об их поведении, исключительно для удовлетворения ворот под давлением сроков. Компания заменила общее требование покрытия политикой, уровневой по риску: строгое покрытие плюс обязательное мутационное тестирование выше порога доли убитых мутантов 80% для кода платежей и аутентификации, и гораздо более лёгкий пол покрытия для низкорискового внутреннего инструментария, что одновременно сократило потраченные впустую усилия тестирования и измеримо улучшило частоту дефектов в подлинно критических путях.
Государство. Система права на пособия агентства общественного здравоохранения была контрактно обязана поддерживать 90% тестового покрытия по соглашению с вендором разработки. Обзор после инцидента, последовавший за значительным дефектом расчёта права на участие, отгруженным несмотря на соблюдение требования покрытия, нашёл, что конкретная ответственная функция достигла своего покрытия полностью через тесты, вызывавшие функцию с валидными входами, но никогда не тестировавшие граничные условия или невалидные входы, именно там, где произошёл дефект. Пересмотренный контракт с вендором агентства теперь требует документированную оценку мутационного тестирования наряду с покрытием для любого кода расчёта права на участие, закрывая конкретный пробел, позволявший соответствующему, но неэффективному тестированию удовлетворять контракт.
Бизнес-кейс: мотивация, ROI и TCO
Отдача от сочетания покрытия с мутационным тестированием: поимка разрыва между видимым и фактическим качеством тестов до того, как он будет стоить производственного дефекта. Пример электронной коммерции выше ясно показывает паттерн: одно требование покрытия произвело ложное чувство безопасности, которое в итоге раскрыла волна дефектов по гораздо большей цене, чем стоила бы инвестиция в мутационное тестирование, которая поймала бы разрыв раньше.
Полная стоимость владения включает вычислительную стоимость мутационного тестирования, более дорогого для запуска, чем простое инструментирование покрытия, и поэтому обычно резервируемого для кода критического пути, а не целой кодовой базы, плюс инженерное время на интерпретацию результатов и действие на их основе. Эта стоимость оправдана конкретно для кода наивысшего риска, где стоимость необнаруженного пробела в эффективности тестирования наивысшая.
Антипаттерны и ловушки
- Отношение к проценту покрытия как к прямому приговору качества: он измеряет выполнение, а не проверку.
- Написание тестов в первую очередь для удовлетворения ворот покрытия: производит именно тот низкоценный паттерн манипулирования порогом, против которого предупреждает тема 1.2.
- Отключение или удаление проваливающихся тестов вместо исправления лежащей в основе проблемы: удаляет реальную защиту, едва влияя на сообщаемое число.
- Применение единой цели покрытия независимо от риска кода: тратит усилия на низкорисковый код и недоинвестирует в подлинно критические пути.
- Тихий рост списка исключений со временем: скрывает реальные пробелы тестирования за технически точной, но вводящей в заблуждение цифрой покрытия.
- Преследование потолка покрытия вместо пола покрытия: создаёт неумолимое восходящее давление, вознаграждающее наполнение тестов важнее подлинной проверки.
Модель зрелости
- Уровень 1, Инициация: Покрытие не измеряется, или измеряется непоследовательно без пола, цели или проверки эффективности.
- Уровень 2, Развитие: Цель покрытия существует и отслеживается, но никакое мутационное тестирование или приоритизация на основе риска не информирует распределение усилий.
- Уровень 3, Стандартизация: Полы покрытия последовательно применяются в CI, с приоритизацией на основе риска, направляющей, где концентрируются усилия покрытия.
- Уровень 4, Управление: Мутационное тестирование запускается на коде критического пути, с отслеживаемым порогом доли убитых мутантов, который должен выполняться наряду с покрытием, а списки исключений периодически проверяются.
- Уровень 5, Оркестрация: Организация может указать на конкретные сокращения дефектов, прослеженные до приоритизации, информированной мутационным тестированием, а данные покрытия и эффективности вместе напрямую информируют решения об инвестициях в тестирование.
Идеи для обсуждения
- Какова наша доля убитых мутантов на нашем единственном самом критическом пути кода, и знаем ли мы её вообще?
- Писали ли мы когда-либо низкоценный тест исключительно для удовлетворения ворот покрытия?
- Сконцентрированы ли наши текущие усилия покрытия там, где риск наивысший, или распределены равномерно?
- Какой код сейчас исключён из вычисления покрытия, и оправдано ли это исключение всё ещё?
- Стоила бы инвестиция в мутационное тестирование на нашей системе с наивысшим риском своей вычислительной стоимости?
Основные выводы
- Тестовое покрытие измеряет выполнение, а не проверку; покрытая строка ничего не говорит о том, была ли она значимо проверена.
- Сочетайте покрытие с мутационным тестированием, чтобы проверить, что тесты на самом деле ловят реальные дефекты, а не просто выполняют код.
- Концентрируйте усилия тестирования на критических путях высокого риска, а не преследуйте равномерное покрытие по всей кодовой базе.
- Используйте покрытие как пол для защиты от регрессии, а не потолок для неумолимой максимизации.
- Следите за конкретными паттернами манипулирования покрытием: низкоценными тестами, отключёнными проваливающимися тестами и тихо растущими списками исключений.
Источники и дальнейшее чтение
- Working Effectively with Legacy Code, Майкл Физерс.
- Jia, Yue, and Mark Harman, “An Analysis and Survey of the Development of Mutation Testing,” IEEE Transactions on Software Engineering (2011).
- xUnit Test Patterns, Джерард Месарош.
- Accelerate: The Science of Lean Software and DevOps, Николь Форсгрен, Джез Хамбл и Джин Ким.