вторник, 18 января 2011 г.

VIM + Devel::Cover - оценка степени покрытия Perl-кода тестами в одно касание

Введение

Значение инструментов для оценки степени покрытия кода тестами зачастую недооценивают: есть мнение, что если код и так хорошо покрыт, то отчёт всего лишь подтвердит и без того известный факт, ну а если кодовая база практически не протестирована, то скудные 5% покрытия наврядли добавят оптимизма разработчикам.

На практике же, даже при нулевом изначальном покрытии, подобный инструмент способен стать серьёзным подспорьем в ежедневной работе: повысить эффективность труда и уменьшить количество дефектов.

В этой статье рассказывается об интеграции VIM-а и модуля Devel::Cover. Первоначальная настройка потребует некоторых усилий, однако этот труд многократно окупится в дальнейшей работе: после того как создание отчёта станет вопросом нажатия пары кнопок, тестирование "невслепую" войдёт в привычку.

Devel::Cover на примере

Для начала, построим отчёт вручную. Итак, на входе имеем модуль Quux.pm и тест для него quux.t. Для запуска тестов с измерением степени покрытия, достаточно задать переменную окружения HARNESS_PERL_SWITCHES следующим образом (есть и другие способы, см. perldoc Devel::Cover):
export HARNESS_PERL_SWITCHES="-MDevel::Cover"
Затем запускаем тест (в этом примере предполагается, что и тест и модуль находятся в одной папке, откуда и запускается команда prove):
prove quux.t
В результате, в текущей директории появится папка cover_db/ - база данных с информацией о покрытии кода. Для создания HTML-отчёта на основе этих данных, необходимо запустить следующую команду (из той-же директории):
cover
Вот как выглядит результат cover_db/coverage.html:


Видно, что в отчёт попал и сам тест quux.t. Для того чтобы этого не происходило достаточно передать команде cover опцию -ignore_re "[.]t$". В отчёте также фигурирует показатель степени покрытия кода документацией (pod-coverage). Если эта информация не нужна, то её также можно отключить (см. perldoc Devel::Cover, параметр -coverage).

В отчёте представлены 5 метрик для каждого файла:
  • stmt --- % выполненных строк кода
  • bran --- % выполненных ветвей условных операторов
  • cond --- % сработавших комбинаций в составных логических условиях
  • sub --- % выполненных подпрограмм
  • pod --- % подпрограмм, имеющих POD-документацию
Столбец time показывает сколько времени прошло в каждом из файлов, а total - агрегирует перечисленные выше показатели.

Если навести мышью на ячейку, то появится всплывающая подсказка вида "N/M", где M - это общее количество тестируемых объектов (к примеру, для столбца stmt - это общее количество строк кода в файле), а N - количество протестированных объектов (для stmt - количество выполненных строк кода).

Если перейти по ссылке в ячейке, то будет показан подробный отчёт по данной метрике. Вот, к примеру, как в нашем примере выглядит bran-отчёт:


Красным отмечены невыполнившиеся ветви кода.

В завершение отмечу, что статистику несколько портит столбец sub, в котором помимо подпрограмм почему-то учитываются выражения вида use.

Стратегии тестирования

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

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

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

Ещё один факт, на который следует обратить внимание заключается в том, что показатель bran-покрытия не учитывает контекст. К примеру, пусть в методе имеются два отдельных условных оператора if(). Тест по-честному проверяет каждое из условий в состояниях TRUE и FALSE, что в результате даёт 100% bran-покрытие. Однако, тест не проверяет что будет, если условие в первом if-е вычислилось как TRUE, а во-втором как FALSE в то время как это может иметь решающее значение для логики работы программы.

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

Ещё одно полезное применение Devel::Cover - помощь при ручном тестировании. Представьте себе большую монолитную, сильно-связанную программу, "вклиниться" в которую традиционными способами затруднительно. В такой ситуации построить отчёт можно следующим образом:
perl -MDevel::Cover yourprog args
cover
Автоматизация процесса

Следующим шагом автоматизируем запуск тестов, построение отчёта, открытие браузера и удаление временных файлов (если они больше не требуются). Скрипт test-coverage-report.pl осуществляет все вышеперечисленные операции.

Пример использования:
$ ./test-coverage-report.pl --input-file quux.t --browser-cmd=/usr/bin/google-chrome --browser-args '--new-window'

quux....ok
All tests successful.
Files=1, Tests=3, 2 wallclock secs ( 1.01 cusr + 0.04 csys = 1.05 CPU)
Reading database from /tmp/quux-qbIB


---------------------------- ------ ------ ------ ------ ------ ------ ------
File stmt bran cond sub pod time total
---------------------------- ------ ------ ------ ------ ------ ------ ------
Quux.pm 94.3 87.5 80.0 87.5 0.0 46.0 86.4
quux.t 100.0 n/a n/a 100.0 n/a 54.0 100.0
Total 97.3 87.5 80.0 94.7 0.0 100.0 92.7
---------------------------- ------ ------ ------ ------ ------ ------ ------


Writing HTML output to /tmp/quux-qbIB/coverage.html ...
done.
В текущем сеансе браузера создано новое окно.
Coverage report is generated in '/tmp/quux-qbIB'. Press 'Y' (default) to cleanup this directory or 'N' if you want to keep it. [Y]
Y
удален `/tmp/quux-qbIB/Quux-pm.html'
удален `/tmp/quux-qbIB/cover.12'
удален `/tmp/quux-qbIB/Quux-pm--condition.html'
удален `/tmp/quux-qbIB/cover.css'
удален `/tmp/quux-qbIB/structure/159a56006bd3bae11c68f2dfb7609a8d'
удален `/tmp/quux-qbIB/structure/7c2bd0b808c91b847c598f3960c48eee'
удален каталог: `/tmp/quux-qbIB/structure'
удален каталог: `/tmp/quux-qbIB/runs'
удален `/tmp/quux-qbIB/Quux-pm--branch.html'
удален `/tmp/quux-qbIB/Quux-pm--subroutine.html'
удален `/tmp/quux-qbIB/coverage.html'
удален каталог: `/tmp/quux-qbIB'
Для того, чтобы этот скрипт заработал, потребуется установить следующие Perl-модули:
Остальные зависимости являются built-in модулями.

Интеграция с VIM-ом

И, наконец, последний штрих: добавим в vimrc заклинание, вызывающие этот скрипт для текущего файла. Вот оно:
map ,c    <Esc>:!/path/to/test-coverage-report.pl --input-file % --browser-cmd=/usr/bin/google-chrome --browser-args='--new-window'<CR>
map ,C <Esc>:!/path/to/test-coverage-report.pl --input-file % --browser-cmd=/usr/bin/google-chrome --browser-args='--new-window' --prove-args='--verbose'<CR>
Комбинация ,c запустит тест, построит отчёт, откроет заглавную страницу в браузере, а затем спросит удалять сгенерированные файлы или нет. По умолчанию (просто ENTER) файлы будут удалены. Вариант ,C делает ровным счётом тоже самое, но запускает prove в verbose режиме. Таким образом, для построения отчёта достаточно открыть vim-ом файл quux.t и нажать ,c.

Для ещё большей гибкости, можно написать свою обёртку для команды prove, которая, к примеру, может по имени Perl-модуля автоматически находить тест для него в определённой папке. Таким образом, ,c можно будет сказать как на самом модуле Quux.pm так и на тесте для него quux.t даже не переключая буфер!

Выводы

Интеграция модуля Devel::Cover с VIM-ом выводит к кончикам пальцев очень мощный и полезный инструмент, который способен стать серьёзным подспорьем в каждодневной работе, а благодаря простоте и удобству, тестирование невслепую очень быстро войдёт в привычку.

Ссылки
Приложение

Архив с тестовыми файлами, скриптом и отчётом: vim_plus_devel_cover_files.tar.gz

Статья опубликована на портале CITForum

2 комментария:

  1. http://habrahabr.ru/blogs/perl/111655/
    можно оформить в виде традиционного теста и запускать при изменениях, в том тесте что я предлагал оценивается суммарное покрытие, но думаю можно и детальнее анализировать если есть желание

    ОтветитьУдалить
  2. Да, такой подход довольно часто встречается в CPAN-овских модулях, однако:

    1. Успешный all_cover_ok( 80 ) это своего рода средняя температура по больнице. Если проект большой, то тестировать всё подряд вплоть до достижения 80% не очень целесообразно, да и есть риск наступить на классические грабли оптимизации - когда оптимизируют не то что реально тормозит программу, а то что проще оптимизировать. Тоже самое с тестами. Для того чтобы этого не происходило и нужен детальный отчёт.

    2. Нужен способ для оценки степени покрытия, которую дает конкретный тест. В противном случае придётся на каждое изменение файла (.pm) или теста (.t) запускать все тесты (в нашем случае это занимает порядка 15 минут).

    ОтветитьУдалить