Главная / Статьи / За пределами процента покрытия: тестирование ситуаций сбоев, с которыми сталкиваются пользователи на самом деле

За пределами процента покрытия: тестирование ситуаций сбоев, с которыми сталкиваются пользователи на самом деле

Почему высокий показатель охвата кода может скрывать неотработанные сценарии сбоев, три вида показушных тестов, к которым он приводит, и как писать тесты, обеспечивающие защиту реального поведения.

920 слов

Запрос к пул-репозиторию с результатами всех тестов в состоянии «зеленое» и отчетом о покрытии кода выше 98% кажется надежным. Однако API возвращает значение null, тогда как интерфейс ожидал объект; запрос обрабатывается в неправильном порядке из-за медленного мобильного соединения; или кто-то дважды нажимает кнопку отправки — в результате фронтенд превращается в пустой экран. Вопрос после анализа причин всегда один и тот же: как это могло сломаться, если код был полностью протестирован? В этой статье объясняется, что на самом деле измеряет покрытие кода, какие шаблоны тестирования завышают его показатели без добавления реальной защиты, и как сосредоточить тестирование на тех сценариях, которые действительно важны.

Что измеряет покрытие кода и что нет

Инструмент оценки покрытия фиксирует, какие строки кода, ветки и функции выполнялись во время тестирования. Это всё. Он не знает, проверяла ли какая-либо асертация полученный результат, соответствуют ли входные данные реальному трафику или правильно ли ведёт себя код при сбоях зависимостей. Выполнение и проверка — это два разных показателя, причём инструмент оценки покрытия сообщает только о первом.

Полезной аналогией может служить проверка здания, при которой кто-то обходит каждую комнату с фонариком. Каждая комната была осмотрена, но никто не проверял крышу во время шторма. Высокое покрытие говорит лишь о том, что ваши тесты обработали код, но не о том, что они его проверили на прочность.

Три способа завышения показателей покрытия без улучшения надёжности

Когда целью становится процентное покрытие, люди начинают оптимизировать тесты исключительно под этот показатель. В результате получаются тесты, которые с минимальными усилиями удовлетворяют требованиям инструмента. Постоянно встречаются три одинаковых подхода.

Мираж идеального сценария

Представьте помощник, который парсит ввод пользователя и обновляет состояние. Его тест проходит при использовании чистой, корректно сформированной строки, проверяет ожидаемый результат и обеспечивает полное покрытие веток. Однако он никогда не пробует обработать пустую строку, необычные символы, аргумент undefined, медленный ответ или некорректный JSON. Все строки кода выполнялись, но не тестировались те входные данные, которые могут вызвать проблемы в реальных условиях.

Слишком часто мокируемый компонент

Здесь каждый вызов API, поставщик контекста и вложенные элементы заменяются на имитацию. Тестирование завершается за несколько миллисекунд и охватывает все варианты отрисовки. Однако в реальных условиях настоящий конечный пункт возвращает данные немного иным форматом, чем предполагало моделирование, или библиотека меняет способ генерации событий после обновления. Тесты продолжают проходить, поскольку они работают только с собственными предположениями, а первая реальная интеграция происходит в браузере пользователя.

Тесты без значимых утверждений

Самая слабая форма проявляется при строгих требованиях, таких как обязательный порог в 90% среди всей команды. В таких тестах функции вызываются исключительно для фиксации выполнения строк кода, при этом делается мало или совсем нет утверждений. Отчет становится зеленым, хотя между кодом и будущими изменениями логики нет никакой защиты.

Как тестировать поведение вместо количества строк

Тесты, которые выявляют ошибки до того, как их заметят пользователи, сосредотачиваются на том, что делает система, а не на том, какие именно участки кода она использует.

  • Тестируются состояния и переходы системы, а не отдельные функции. Пользователей не волнует, запускалась ли какая-то вспомогательная функция. Их интересует то, что происходит, когда сеть прерывается на полпути отправки формы или когда они пытаются покинуть страницу, пока происходит загрузка файла. Необходимо протестировать индикаторы загрузки, состояния ошибок, возможности повторной попытки и механизмы обработки ошибок.
  • Предпочтительнее использовать интеграционные тесты там, где это практично возможно. Имитация реальных внешних компонентов, таких как платежные шлюзы сторонних поставщиков, является разумным подходом. Однако имитация собственных внутренних вспомогательных функций или слоев обработки данных часто скрывает ошибки, существующие между ними. Позвольте компонентам и модулям работать вместе в той мере, в которой это допускают условия.
  • Позвольте багам в продакшене расширять набор тестов. Когда обнаруживается дефект, напишите тест, который его воспроизводит и даёт ошибку, прежде чем приступать к его устранению, затем изменяйте код до тех пор, пока тест не пройдёт. Со временем набор тестов будет включать крайние случаи, с которыми действительно сталкивается ваша система, а не те, которые кто-то себе представил.
  • Связанной и хорошо известной техникой является тестирование мутаций, при котором намеренно изменяют код (меняют условие, удаляют строку) и проверяют, сбоит ли какой-либо тест. Выжившие мутации указывают непосредственно на код, который выполняется, но не проверяется, что именно и не может показать тестирование покрытия. Чтобы узнать подробности на уровне компонентов, ознакомьтесь с этими антипаттернами тестирования React, создающими ложное чувство уверенности.

    Использование покрытия в качестве сигнала, а не цели

    Уровень покрытия тестами не бесполезен. Низкое значение в критически важном модуле является настоящим предупреждением, а отчеты могут выявить пути выполнения кода, которые вообще никто не тестирует. Проблемы начинаются тогда, когда целью становится именно процент покрытия, поскольку это стимулирует выполнение большого количества тестов в ущерб их качеству.

    Система тестов с уровнем покрытия около 65%, сосредоточенная на рискованных рабочих процессах, сложных бизнес-правилах и механизмах восстановления после сбоев, предотвратит гораздо больше инцидентов, чем хрупкая система с уровнем покрытия 95%, построенная на тестах «идеальных» сценариев и имитациях. Прежде чем добавлять тест, лучше задать вопрос «Какой реальный сбой этот тест сможет обнаружить?», чем «Какие строки кода остались без тестирования?»

    Основные выводы

    • Уровень покрытия показывает, какой код был запущен, но не указывает на то, проверялось ли его поведение.
    • Вводные данные для «идеальных» сценариев, интенсивное использование внутренних имитаций и тесты без проверок утверждений всё это повышает показатель покрытия, не снижая при этом рисков.
  • Переходы между целевыми состояниями, обработка ошибок и интеграция собственных модулей.
  • Сначала превращайте каждую обнаруженную ошибку в неудачный тест, а затем уже исправляйте её.
  • Отслеживайте способы сбоев, от которых защищает ваш набор тестов, и рассматривайте показатель охвата как вспомогательную информацию.
  • Связанные материалы

    • Beyond Bundle Size: Finding What Actually Makes Your Web App Slow — Почему сокращение количества килобайт редко помогает ускорить медленное приложение, и как отслеживать реальное время ожидания на серверах, в процессах обработки данных, при использовании скриптов и изображений от сторонних поставщиков.
    • Beyond P95: Оценка задержки, которую действительно испытывают ваши пользователи — почему хороший показатель P95 может сосуществовать с медленным продуктом, как время ожидания в очереди и распространение запросов скрываются от панелей управления, и как измерение времени на каждом этапе прекращает споры о причинах задержки.