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