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

Материал подготовлен на основе выступления разработчика Positive Technologies Ивана Цыганова на конференции Moscow Python Conf (слайды, видео).

Зачем мы пишем тесты


ИБ-эксперты Positive Technologies проводят более 200 аудитов информационной безопасности в год, но мы прежде всего продуктовая компания. Один из наших продуктов — система контроля защищенности и соответствия стандартам MaxPatrol.

Продукт состоит из трех больших подсистем:

  • Pentest отвечает за тестирование на проникновение — система без реквизитов доступа пытается собрать данные о сетевых устройствах. .
  • Audit выполняет системные проверки — имея реквизиты доступа система собирает информацию о устройстве.
  • Compliance — выполняет проверки на соответствие стандартам.

Размер только лишь одной подсистемы Audit составляет более 50000 строк — без тестов, деклараций, только чистый Python-код. Зачем мы пишем тесты для столь значительного объёма кода?

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

Проверка покрытия кода тестами также позволяет решать свои задачи:

  • Становится видно, какой код в действительности протестирован.
  • Она позволяет увидеть все ветви исполнения кода, которые могут быть не всегда очевидны.

Нужно ли 100% покрытие


Здесь есть интересный момент — многие специалисты считают, что проверка покрытия тестами говорит о качестве тестирования. На самом деле это совершенно не так. Да, это хорошая ачивка («у нас 100% coverage!»), но это не означает того, что проект полностью протестирован. Стопроцентное покрытие говорит лишь о стопроцентном покрытии кода тестами, и ни о чем больше.

Для Python де-факто стандартом проверки покрытия является библиотека coverage.py. Она позволяет проверить покрытие кода тестами, у нее есть плагин для pytest. В основном, библиотека работает, но не всегда.

Пример — код ниже покрыт тестами на 100%. И в этом примере претензий к работе coverage.py нет.



Но на более сложной функции один тест дает 100% покрытие, при этом функция остается не протестированной. Мы не проверяем ситуацию, когда единственный ‘if’ функции обернется в False.



У библиотеки есть еще один режим работы, который позволяет отслеживать покрытие ветвей исполнения кода. Если запустить проверку в этом режиме, то будет видно, что не покрыт переход из третей в пятую строку кода. Это означает, что на всех запусках тестов мы никогда не попадали из третьей строки сразу в пятую, а всегда попадали в четвертую, то есть “if” на всех тестовых данных оборачивался в True.



Как считается покрытие


Существует простая формула для расчета покрытия кода тестами:



Coverage.py работает по такой схеме — сначала библиотека берет все исходники и прогоняет через собственный анализатор для получения списка инструкций. Этот анализатор обходит все токены и отмечает «интересные» с его точки зрения факты, затем компилирует код, обходит получившийся code-object и сохраняет номера строк. При обходе токенов он запоминает определения классов, «сворачивает» многострочные выражения и исключает комментарии.

Переходы между строками считаются примерно так же:



Опять берется исходный код и анализируется классом AstArcAnalyzer для получения пары значений — из какой строки в какую возможен переход. AstArcAnalyzer обходит AST-дерево исходников с корневой ноды, при этом каждый тип нод отрабатывается отдельно.
Далее нужно каким-то образом получить информацию о реально выполненных строках — для этого в coverage.py используется функция settrace. Она позволяет нам установить свою функцию трассировки, которая будет вызываться при наступлении некоторых событий.

Например, при наступлении события “call” мы понимаем, что была вызвана функция или мы вошли в генератор… В этом случае библиотека сохраняет данные предыдущего контекста, начинает собирать данные нового контекста, учитывая особенности генераторов. Еще одно интересующее нас событие — событие “line”. В этом случае запоминается выполняемая строка и переход между строками. Событие return отмечает выход из контекста — тут важно помнить, что yield также вызывает наступление события “return”.

После этого строится отчет. К этому моменту у нас есть данные о том, что выполнялось, а также что должно было выполняться — по этим данным можно сделать выводы о покрытии кода тестами.

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

Что может пойти не так


Рассмотрим простой пример — вызов некоторой функции с условием при передаче параметров.



Оператор if будет покрыт всегда. И мы никогда не узнаем, что это условие всегда оборачивалось в false.

Проблема возникнет и при использовании lambda — внутрь этой функции coverage.py не заглядывает и не скажет нам о том, что внутри что-то не покрыто. Не сможет библиотека разобраться и с list, dict, set-comprehensions.

Все эти случаи имеют кое-что общее. Как мы выяснили выше, coverage.py использует парсер и получает список инструкций. В итоге результатом работы библиотеки является покрытие инструкций, а не строк кода.

Делаем мир лучше


Возьмем простой пример непокрываемого кода:



Допустим, мы хотим покрыть его и знать, когда не срабатывало условие “or c”. Ни один режим coverage.py не позволит этого сделать. Что можно попробовать сделать в этом случае?

Можно установить собственную функцию трассировки, посмотреть на результат ее работы и сделать выводы. То есть, фактически, повторить то, что делает coverage.py. Этот вариант не подходит, поскольку мы имеем ограниченное количество событий: call, line, return, exception. Маленькие частички оператора if мы никогда не увидим.

Другой вариант — использовать модуль ast.NodeTransformer. С его помощью мы можем обойти дерево, обернуть в «нечто» каждую ноду, запустить и посмотреть, что выполнялось. Проблема здесь в том, что на уровне AST очень сложно обернуть ноду в “нечто”, не изменив при этом логику исполнения. Да и в целом, далеко не все ноды можно обернуть. Этот метод тоже подходит.

Но можно использовать и другой подход. Что если, во время импорта перехватить контроль, обойти байткод импортируемого модуля, добавить внутрь байткода вызов своей функции трассировки, собрать code-object и посмотрим, что получилось. Именно эта идея реализована в прототипе библиотеки OpTrace.

Как работает OpTrace


Прежде всего нужно установить Import.Hook— здесь все довольно просто. В нем есть Finder, который пропускает неинтересные нам модули, создав для нужных Loader. В свою очередь, этот класс получает байт-код модуля, строки его исходного кода, модифицирует байт-код и возвращает измененный байткод в качестве импортируемого модуля.

Работает все это так. Создается wrapper, внутри которого «пробрасываются» две функции — первая нужна для того, чтобы отметить опкод, как уже посещенный (visitor). Задача второй — просто отметить, что такой опкод существует в исходнике (marker).



В Python есть ряд инструментов для работы с байткодом. Прежде всего, это модуль dis и его одноименный метод позволяет увидеть байткод в красивом виде.



Подобное представление удобно просматривать, но не обрабатывать. Существует и другой метод — get_instructions. Он принимает на вход code-object и возвращает список инструкций.



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

С трассировкой дело обстоит несколько сложнее. Нельзя просто так взять и поместить в байткод вызов каких-то нужных нам методов. У CodeObject есть атрибут consts — это доступные внутри него константы. В них можно поместить lambda-функцию и “замкнуть” в нее текущую инструкцию в качестве параметра по-умолчанию. Таким образом, вызвав эту лямбду из констант без параметров, мы сможем трассировать выполнение конкретных опкодов. Далее нужно лишь сгенерировать код для вызова константы.

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



Болдом на скриншоте подсвечен оригинальный трассируемый байткод. После модификации байткода необходимо запустить тесты. Так мы выясним, какая часть кода выполнялась, а какая нет. Возникает вопрос, а что делать с непокрытыми опкодами? В проекте на 50 000 строк их перечисление может занять несколько страниц.

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



Допустим, что строки у нас всегда вычисляются корректно. Теперь можно попробовать вычислить позицию в строке для каждого пропущенного опкода. Рассмотрим несложный пример с опкодом LOAD_FAST. Его параметры говорят о том, что мы имеем дело с загрузкой некоей переменной. Мы можем попробовать в известной нам строке найти ее имя.



Покрыв примерно 70 типов опкодов удалось получить вменяемый отчет. Но многие опкоды покрыть невозможно. Новый отчет выглядит так:



Удивительно, но это работает. Например, мы четко видим, что не сработал LOAD_FAST для переменной c.

OpTrace: минусы и плюсы


При работе с прототипом имеется ряд проблем.

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

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

Заключение


Одной из целей этого исследования и разработки была демонстрация факта того, что не существует идеальных библиотек. Coverage.py хорош, но не идеален — слепо верить его отчетам нельзя. Поэтому необходимо всегда разбираться с тем, как работает библиотека и изучать как она работает “изнутри”.

Еще один ключевой тезис — coverage в 100% расслабляет команду. Раз результатам работы библиотек нельзя полностью доверять, то полное покрытие — это просто ачивка, за которой могут скрываться реальные проблемы.
Поделиться с друзьями
-->

Комментарии (38)


  1. saroff
    06.03.2017 17:30
    +10

    Так а почему 100% покрытие это плохо-то?


    1. ptsecurity
      06.03.2017 17:31
      -5

      > слепо верить отчетам нельзя
      > coverage в 100% расслабляет команду

      В общем, достаточно прочитать статью :)


      1. greendimka
        06.03.2017 18:14
        +6

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


        1. Lain_13
          06.03.2017 18:26
          +1

          Я думаю имеется в виду тот факт, что если система тестирования кода сообщает о 100% покрытии кода тестами это не значит, что они действительно покрыты на 100%. Т.е. даже если вы думаете, что покрыли код тестами на 100% и система вам говорит об этом в нём всё ещё могут быть ошибки.
          Впрочем, плохо не само покрытие кода тестами на 100%, а слепая уверенность в том, что это так.


          1. greendimka
            06.03.2017 18:35

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


            1. ConstantineDrozdov
              06.03.2017 23:48

              На сколько я понял, речь шла о модульном тестировании, а не об интеграционном, так что мне кажется, ты немного не в тему (могу ошибаться)


              1. greendimka
                06.03.2017 23:59
                -3

                Если бы ConstantineDrozdov не хамил, то получил бы разъяснение, почему в данной конкретной ситуации он ошибается.


      1. saroff
        06.03.2017 19:11
        +3

        Так в том и дело, что статью я прочитал, а про 100% покрытие увидел только эти две фразы :)
        Почему coverage.py работает плохо понятно, почему никаким тулзам нельзя верить на 100% думаю все и так знают, так что нам теперь, специально уменьшать покрытие тестов? Ну чтоб это, не расслаблялись.


        1. phoenixweiss
          07.03.2017 01:22
          +1

          Объективно просто статья не соответствует заголовку.
          Да, статья вполне вменяемая, в ней есть адекватные мысли, но по существу заголовок ее реально дизориентирует.
          Тут суть на самом деле в том что качество тестов не равно показателю покрытия кода.
          При выборе между сотней «assert true» без параметров и всего парой тестов, грамотно покрывающих критический функционал всегда для продукта будет лучше второе.
          Кроме того, остальные выводы также крайне очевидны. В других комментариях уже обращали внимание на конкретику, скажу лишь что многие утверждения тут действительно уровня «воздух — прозрачный, водя — мокрая, а огонь — горячий».
          Если честно, я ожидал действительно грамотного и интригующего ответа на поставленный в заголовке вопрос с примером реального кода в котором творится какая-то магия языка, не поддающаяся нормальным тестам или еще какою-то загадку.


          1. saroff
            07.03.2017 10:01

            Если честно, я ожидал действительно грамотного и интригующего ответа на поставленный в заголовке вопрос

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


      1. ezj
        07.03.2017 10:47

        В статье, по сути, 99% воды. Достаточно просто прочитать капитанское «заключение»…


    1. khim
      06.03.2017 20:28

      100% покрытие это ни плохо и ни хорошо. Это просто число. Но если вы хотите использовать его как цель — вспомните про Закон Гудхарта. О нём, в общем-то, никогда забывать не стоит…


  1. magic4x
    06.03.2017 17:49
    +1

    Код картиночками. В джепеге. Серьезно?


    1. magic4x
      06.03.2017 17:53
      +1

      if result['Total'] ? 1000
      Я даже попробовал это запустить.


      1. kana-desu
        06.03.2017 19:20
        -3

        Шрифты с гигатурами для повышения читаемости кода (считать и спарсить один символ "?" намного проще, чем два символа, особено в какой-нибудь строке с js-лямбдой), например — https://github.com/tonsky/FiraCode


    1. madkite
      06.03.2017 21:23

      Похоже это был доклад на каком-то митапе с вот этими слайдами, который трансформировался в хабростатью. Не специально ж они так, надеюсь...


  1. abyrkov
    06.03.2017 20:41

    ИМХО, но статья совсем не о том, что 100% покрытие кода это плохо. Скорее «Почему ваш код не покрывается на 100% и как это исправить».
    P.S. Картинки, чего-то, полетели


  1. Juralis
    06.03.2017 21:14
    +1

    У меня немного философский вопрос. А как вообще физически можно получить 100% покрытие тестами? Всегда ведь есть некая "серая зона", которая зачастую возникает довольно неожиданно, зависит от окружения, поступающих некорректных данных и так далее. То есть, 100% покрытие тестами — это вообще всегда ложь, каким бы методом не достигалась эта цифра. Но если 100% — всегда ложь, то и любой другой процент — тоже автоматически ложь. 100% — это эталон. Лишившись эталона, мы лишаемся всего, приближающегося к эталону. Я не говорю, что тесты не нужны, но когда кто-то говорит о сколько-то процентном покрытии тестами, это звучит как попытка пустить пыль в глаза. я ещё понимаю, когда есть какое-то жёсткое ТЗ, в котором прописано, условно, какая функция при каких условиях какое значение должна возвращать за какое время. Или что-то подобное. Тогда понятно, что такое 100%. Это когда напротив каждого такого пункта в ТЗ стоит галочка — покрыто тестами. Но и не более. То есть, в этом случае, нельзя говорить о полной корректности программы на любых данных в любых условиях. Любой выход за границы, прописанные в ТЗ — швах. А в условиях, когда тестами покрывается нечто, у чего и ТЗ никогда не было — что они вообще тестируют? Понятно, что часто можно примерно прикинуть, какой результат будет верным. Но часто бывает и так, что формально правильно, а по существу — издевательство.


    1. hlogeon
      06.03.2017 21:44

      мммм… Вы никогда не видели OpenSource проектов? 100% coverage сплошь и рядом. И некоторые как-то ведь умудряются и на изменения реагировать и релизиться регулярно.


      1. Juralis
        06.03.2017 22:19

        Наличие плашки ещё не говорит о полном покрытии. Достаточно просто спросить: 100% от какой суммы случаев? В основном, это всё конечно полезно, но я просто не вижу никаких доказательных способов заявить, что покрыто реально 100%. Чтобы это заявить, нужно сначала доказать, что успешное прохождение этих тестов полностью гарантирует работоспособность кода в 100% случаев. Но зачастую, вместо доказательства используется тот или иной автоматический анализ. Как они работают — отдельный вопрос. В принципе, статья довольно наглядно иллюстрирует некоторые аспекты. И ни о какой доказательности тут речи не идёт. Можно сказать, что такие средства выявляют некоторые проблемные места, но никто не даст гарантии, что они выявляют 100%. Иными словами, они доказывают, что в конкретном месте — возможна ошибка и её нужно покрыть тестом. Но они не доказывают, что кроме найденного — ничего больше нет. То есть, эти 100% — это 100% от найденного, а не от существующего. В принципе, это уже что-то. Но с другой стороны, это реально не правильная формулировка. Это нельзя использовать как доказательство корректности программы. Посмотрите на различные версии json-парсеров. Они все в теории должны делать одно и тоже. А на практике, они отличаются и выходной результат работы одного парсека может не совпасть с результатом работы другого. При этом, оба могут быть покрыты тестами на 100%, поскольку формально оба алгоритма могут быть корректными. Но при попытке обменяться результатом между двумя системами использующими разные партеры — будет ошибка. Кто виноват? Виноват плохой стандарт, который допускает неопределённое поведение. Соответственно, во вселенной просто физически не может быть ни одного парсера, который не содержит ошибок относительно другого парсера. И не имеет значения, какие у них там покрытия.


        1. VolCh
          07.03.2017 09:14
          +1

          100% от какой суммы случаев?

          В идеале 100% покрытия означает, что при прохождении всего набора тестов каждая ветвь кода исполняется хотя бы один раз. Статья показывает, что плашка "100%" может это не гарантировать, что некоторые ветви кода могут быть не покрыты, то есть популярные анализаторы не могут правильно выявить все разветвления в коде и считают две ветви за одну даже в довольно простых случаях. Соответственно ставят 100% когда тест только на одну ветвь.


          Чтобы это заявить, нужно сначала доказать, что успешное прохождение этих тестов полностью гарантирует работоспособность кода в 100% случаев.

          Вы, по-моему, неправильно понимаете суть тестов. Они даже не пытаются доказать, что программа безошибочна, что проблемных мест в ней нет. Цифры 100%, 99%, 50% показывают, в идеале, что при некоторых наборах данных программа выдаёт нужные результаты и при выполнении тестов затронуто столько процентов кода. Основная функция тестов — фиксация поведения программы в строго заданных случаях, обычно качественно меньших чем все возможные.


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


          Объективной пользы от автотестов по сути две:


          • инструмент, позволяющий разработчику легко проверять исправил ли он баг, возникающий при конкретном сочетании параметров
          • инструмент, позволяющий разработчику легко проверять не сломал ли он исправления багов, возникавших ранее при конкретном сочетании параметров

          Автотесты не про выявление новых багов, а про исправление известных, причём только для конкретных случаев. Можно делать индуктивные умозаключения, что если тест показывает, что мы исправили баг, при котором для 2+2 возвращалось 5, и теперь возвращается 4, то и для 3+3 вернется 6, но нужно понимать что тесты этого не гарантируют и, например, для 32767 + 1 код может вернуть -1, и даже для 3+3 может вернуться 9, если в реализации случайно стоит a*b, а не a+b.


          1. Juralis
            15.03.2017 20:48

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

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

            Наверное, описанный вами подход вполне оправдан в каких-то условиях, но это выглядит как способ решения довольно узких задач. В моём представлении, более применимы тесты в стиле «может ли пользователь зарегистрироваться». Тест получается более комплексным. Его можно крутить несколько раз, на разных наборах данных, чтобы проверять пограничные условия или ранее известные баги. А просто проверять, что а+б == б+а — это как-то мне не понятно.


            1. VolCh
              16.03.2017 05:52

              Автотесты — это инструмент проверки того, что тикет не надо повторно открывать, если он уже закрыт. Грубый процесс:


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

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


    1. VolCh
      06.03.2017 21:58

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


      1. Juralis
        06.03.2017 22:43

        Я бы вместо слова "являются" скорее применил бы "должны являться". Но на практике, это далеко не всегда так. В основном, по той простой причине, что в большинстве случаев естественный человеческий язык перевести на строгий однозначный и формализованный язык просто невозможно даже в теории по причине его изначально метафорической природе. Кроме того, зачастую тесты пишутся даже для кода, который написан вообще без ТЗ, просто интуитивно. И эти тесты, как бы они хорошо не покрывали бы код — просто культ карго и дань моде.
        Я не являюсь большим специалистом в области автоматического тестирования, но сама идея такого подхода, как описана в статье — мне кажется просто каким-то маркетинговым трюком для коммерческого продукта и не более того. Описанный там подход наверное полезен, для статического анализа кода и выявления в нём потенциальных проблемных мест. Но писать тесты на основе этого анализа — довольно странное занятие, на мой взгляд. От фактических ошибок такие тесты не избавят. Выше я упомянул пример с json-парсерами, которые как ни покрывай, а они всё равно будут в ряде случаев просто не совместимы друг с другом.


  1. kicum
    07.03.2017 11:15

    А тут у меня сразу по первому примеру вопрос, где сравниваются длины массивов — где там 100% покрытие? Не учтены случаи с пустым массивом, null (я совсем не питонист, не знаю какая верная формулировка), с массивами большой длинны(что-то около Long.MAX_VALUE), вызов функции без аргументов, аргументы не являются массивами.

    Тест — не только проверка функциональности, но и контракт между разработчиками.


    1. VolCh
      07.03.2017 12:08
      +1

      Не путайте покрытие кода тестами с покрытием кодом кейсов использования.


      1. kicum
        07.03.2017 15:42

        Эмм… даже растерялся немного. А зачем тогда такой код, который не покрывает тесты?


        1. VolCh
          07.03.2017 16:05
          +1

          Код покрывает кейсы, а не тесты. Грубо, задача сложить два числа, код типа sum(a, b) { return a + b; } тест assert(4, sum(2,2)); 100% покрытия кода тестами, но код не покрывает все возможные кейсы использования, например, приводящие к переполнению целых чисел или передаче вообще не чисел.


          1. kicum
            07.03.2017 16:17

            Ай, а вот и нет и это распространенная ошибка.

            Да с формальной точки зрения покрытие — 100%. Но вы все же кое-что забыли. Какого типа переменные a,b? В сигнатуре не указано.
            А значит покрытие нужно увеличить до нескольких вариантов входных типов:

            assert(?, sum(«2», «2»));
            assert(?, sum('2', '2'));
            assert(?, sum(2.00, 2.00));

            И это минимум того что нужно сделать.

            Покрытие именно строк кода тестами — метрика достаточно бесполезная, если ориентироваться только на нее. И не забываем, что тест — часть документации и контрактов между разработчиками


            1. VolCh
              07.03.2017 16:56
              +1

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


  1. Kavaru
    07.03.2017 18:01
    +1

    Зачем было лить столько воды, чтобы донести мысль: «100% покрытия кода» не равно «100% работоспособности кода»?
    Вот, например, 100% покрытый код:

    def foo (a, b):
        return a+b
    
    assert foo(2,3) = 5
    

    Который 100% не до конца протестирован.

    И тут плохо не то, что код покрыт на 100%, а то, что кто-то считает эту метрику панацеей. Т.е. статью стоило назвать «Почему делать покрытие кода ключевым KPI это плохо» или «Почему менеджер требующий 100% покрытия кода это плохо».


    1. VolCh
      07.03.2017 18:42

      До вас мысль не донесли. 100% покрытия кода от какой-то конкретной тулзы вполне может не означать, что тесты реально покрывают каждую ветвь кода. Работоспособность дело десятое, пост не про неё :)


  1. Tsyganov_Ivan
    07.03.2017 20:41

    На правах автора доклада добавлю свои 5 копеек)

    Основная мысль доклада, как уже говорили в комментариях — «Не надо верить тулзам, которые говорят, что тесты хорошие».
    coverage.py по-дефолту покажет непокрытые строки кода и вы увидите заветные 100% покрытия, но стоит указать параметр --branch и покрытие падает, потому что вряд ли покрыты все возможные переходы. Покрыв переходы между инструкциями (этот режим считает именно переходы между statements, а не lines) получаем снова 100%.
    Но получается и этим 100% верить нельзя. Вот отсюда и второстепенная мысль доклада — «А что можно сделать, что бы еще лучше оценить покрытие кода». И во второй половине доклада представлена идея покрытия кода на уровне байткода.

    Что касается названия — реакция в комментах показала, что название выбрано как нельзя лучше :-)


  1. Lucyfer
    08.03.2017 06:52

    Для Java есть библиотека мутационного тестирования http://pitest.org/
    Суть в том что с помощью инструментирования байткода по определенным правилам (например инвертирование условия в операторе if, замена тела метода на return null и т.д.) изменяется тестируемая система и если при этом не падает ни один тест, то их явно недостаточно. После прогона тесткейса на всех мутациях будет собран настоящий честный code coverage


    1. Tsyganov_Ivan
      08.03.2017 09:45
      +1

      Да, мутационное тестирование это очень интересная идея для проверки качества тестов.
      Для Python есть несколько библиотек, которые я смотрел:
      cosmic-ray
      mutpy
      Они работают, но когда у вас много разветвленного кода — количество мутантов растет и прогон тестов начинает занимать непростительно много времени.
      О мутационном тестировании я рассказывал на Pycon Siberia 2016. К сожалению, видео доклада пока нет, но организаторы обещали.

      После прогона тесткейса на всех мутациях будет собран настоящий честный code coverage

      С этим можно поспорить) Безусловно, после прогона тестов на мутантах и исправления тестов — ваши тесты станут лучше. Но, как мне кажется, говорить о честном coverage, все еще рано.


  1. woooody
    08.03.2017 13:03

    Помимо покрытия по стркам (SC) и веткам (DC) есть еще полное покрытие условий в ветке (MC/DC).
    Однако 100% покрытие кода говорит только о том, что команда тестировщиков добивалась 100% покрытия кода. О качесте тестирования это не говорит вообще:
    1. Создание тестовых ситуаций не гарантирует что какие-либо выходые значения проверялись.
    2. Если в функции есть две ветки которые тестировали независимо, то результаты их покрытия будет 100% (это покажет любой сборщик). Но при этом элементарно создать комбинацию, которая всё повалит.

    int func(int a, int b)
    {
        int div = 1;
        if (a) div = 0;
        if (b) return (100/div);
        return div;
    }
    


  1. 0xFE
    13.03.2017 03:51

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

    woooody прав. Для того, что бы говорить о покрытии кода тестами, нужно обязательно уточнять какая именно метрика используется. Если интересно — вот достойное описание различных видов метрик покрытия кода. Но… их тоже нужно применять в зависимости от того, что вы собственно разрабатываете. Вот рекомендации.