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

Однако в определенный момент мы, возможно, захотим начать сбор некоторой информации о состоянии нашего тестового набора. На ум приходят такие вопросы, как «Достаточно ли я протестировал?» или «Действительно ли нужны эти медленные полные стековые тесты?». Обычно в этот момент вы начинаете все чаще и чаще сталкиваться с одним термином: тестовое покрытие.

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

Покрытие тестирования предназначено для ответа на вопросы, связанные с кодом, выполняемым тестами. Как правило, наблюдение за кодом, охватываемым тестами, включает в себя рассмотрение фрагментов кода, которые фактически выполняются во время выполнения набора тестов. Это приводит к различным подклассам тестового покрытия. Покрытие ветвей для
примера показывает, сколько ветвей кода покрыто. Любое
if-clause в вашем коде начинается с другой ветки, той, в которой условие истинно, и присоединенной then-part вашего if- блок выполняется, и тот, в котором условие оценивается как ложное, а блок не выполняется.
См. следующий пример для иллюстрации:

Пока что ветвление происходит довольно просто. Выглядит почти как выполненные строки, не так ли? Предполагая, что мы всегда пишем наш код очень подробным образом, он может быть эквивалентным выполняемым строкам. Но обычно это не так для реальных приложений. Давайте вместо этого посмотрим на этот отрывок:

Да, знаменитый тернарный оператор (?:) может быть довольно проблематичным. Он создает точки ответвления, а также выполнение в одной строке. Точно так же явные выражения return имеют тенденцию преждевременно уничтожать ответвления. Думаю, было бы очень плохой новостью сказать вам сейчас, что большинство инструментов отчетов о покрытии сообщают только о покрытии на основе строк. Есть несколько языков, которые упрощают построение правильных абстрактных синтаксических деревьев для представления потенциальных путей выполнения программы и правильного обхода этого дерева. Большинство языков, которые мы используем в веб-разработке, например Ruby, Javascript, PHP или Python не входят в этот список.

Последствия линейного покрытия

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

Давайте сначала разберемся, почему, прежде чем мы займемся реальной
информацией, которую мы можем почерпнуть из этих отчетов. Как мы видели в предыдущем примере, покрытие на основе строк определяет только, была ли выполнена конкретная строка во время выполнения набора тестов. Это означает, что строки кода, содержащие несколько точек ветвления (например, тернарный оператор или постфиксные if-предложения), не могут быть правильно сообщены. В более широком смысле это приводит к потенциальному отчету о 100% тестовом покрытии от lb-репортера (построчного репортера), не отражающего полное покрытие ветки. Поэтому покрыты не все отрасли. Однако это нормально, если мы знаем об этом и помним о последствиях. Само по себе полное покрытие ветки даже не приводит к мистическому 100% тесту, который может нас заинтересовать. Почему?

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

Таким образом, языки с динамической типизацией, такие как, например, Ruby, сразу же оказываются в невыгодном положении. Но прежде чем вы побежите к своему руководителю или остальной части вашей команды и предложите использовать с этого момента только статически типизированные языки, нужно иметь в виду, что даже для этих языков невозможно 100% тестовое покрытие. Это связано с тем, что мы не можем увидеть то, что не реализовали, если не написали для этого тест. Если, например, мы забыли проверить нулевые значения или предоставили типы подклассов (которые могут быть предоставлены в качестве аргументов для методов, которые ожидают суперкласс - даже в случае языков со статической типизацией, типы не полностью заблокированы), мы не будем убедитесь, что это не покрывается, если на самом деле нет теста, который это покрывает.
Тогда этот тест завершится неудачно, что определенно приведет нас к реализации отсутствующего
фрагмента кода. Но если мы не знаем, что нам нужно протестировать этот крайний случай, мы никогда не увидим его в отчете о покрытии. В отчете о покрытии мы увидим это только как дополнительное выполнение для той же линии или ответвления.

И хотя это может показаться потенциально надуманным крайним сценарием, я бы сказал, что для любого данного приложения с достаточной сложностью, например когда невозможно объяснить детали каждого сценария своей пресловутой бабушке, это будет обычным случаем, когда приложение демонстрирует 100% (или даже 95%) покрытие тестом на основе строк.
Это потому, что строк нет. покрыты, но, как правило, относятся к тривиальным случаям, например,
редко вызываемым геттерам и сеттерам. Следовательно, вся остающаяся
сложность заключается в коде, который уже был выполнен хотя бы один раз.

Помня все это, для чего можно на самом деле использовать отчеты о покрытии тестами?

Преимущества отчета о тестовом покрытии

В конечном итоге отчет о тестовом покрытии может быть очень полезным инструментом, если
его правильно использовать. Он обеспечивает интересную перспективу с точки зрения высокого уровня проекта. Он четко разделяет код на два
класса: на классы покрытого и непокрытого кода. Таким образом,
он довольно легко используется в качестве инструмента для поиска задач на более поздних этапах проекта,
когда вы очень уверены, что его приложение работает так, как задумано, но хотите сохранить его таким, доказать другие или просто хотят создать надежный код.

1. найти непроверенный код

Подробный отчет о покрытии может указывать прямо на модули / классы и методы / функции, которые не были выполнены в рамках тестов. Таким образом, он может выявить потенциально устаревший код, потому что он вообще не выполняется или еще не протестирован должным образом. Особенно на более поздних стадиях проекта это также может указывать (безвредно) на тривиальные строки кода, например простые определения геттеров, которые в основном используются разработчиком для интроспекции, а не во время выполнения программы. Однако чаще всего это указывает на то, что мы упустили. В нашем примере показан основной путь выполнения, не охваченный никакими тестами. Предполагается, что метод добавляет элемент в наш Set, если он еще не существует. В любом случае предполагается вернуть соответствующий объект. Однако сценарий, в котором ничего не добавляется в Set, не рассматривается.

2. обнаруживать ранее неизвестные крайние случаи

На более поздних стадиях разработки обычно не охватываются полные классы или методы - при условии, что кто-то уже отслеживает качество их набора тестов - а вместо этого отдельные строки или небольшие группы строк. Чаще всего вы можете обнаружить, что они относятся к else случаю if-предложения. Мы склонны писать наш код таким образом, чтобы часть then представляла положительный результат, так называемый счастливый путь. Для большинства разделов нашего кода это также общий случай. Поэтому необычные печальные пути часто можно найти внутри else case и определенно в _7 _ / _ 8_ блоках.

3. проверить путь выполнения отдельного теста

Необязательно запускать инструмент покрытия тестами на своем полном наборе тестов. Во время разработки новой функции может быть очень полезно запустить новый тест изолированно, чтобы увидеть, как он на самом деле движется по коду. Мы склонны гордиться тем, что можем выполнить программу в своей голове, и думать о путях, по которым инструкции пользователя проходят через нашу базу кода. Но с любой сложной системой количество возможностей может показаться бесконечным, даже если мы уменьшим объем до пресловутой единицы. А инструменты тестового покрытия могут помочь нам в нашем пути к лучшему пониманию кода, который мы только что написали всего несколько минут назад.

Заключение

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

В этой короткой статье я использовал Ruby, чтобы проиллюстрировать примеры. Ruby, как и большинство интерпретируемых языков с динамической типизацией, традиционно поддерживает только отчеты о покрытии на основе строк, поскольку его внутренние утилиты отслеживания допускают только гранулярность на основе строк. Однако выпуск Ruby 2.5, выпущенный в конце года, изменил это. Команда Ruby-Core интегрировала отслеживание покрытия на основе ветвей. Хотя фактическим инструментам тестового покрытия потребуется время, чтобы интегрировать и использовать это в своем представлении; Я думаю, что это определенно повод для энтузиазма, это позволяет нам еще больше отодвинуть занавес, хотя есть еще некоторые вещи, которые останутся в тени.