Покрытие в картинках
Тесты — это хорошо. Приятно работать с кодом, который хорошо покрыт качественными тестами. Тесты дают свободу. Можно экспериментировать, не боясь что-нибудь сломать; можно рефакторить код до идеала, не опасаясь, что он перестанет работать. Люблю тесты.
Покрытие тестами — полезная метрика, если не делать из неё цели1. Дядя Боб на одной из лекций спрашивал аудиторию, к какому проценту покрытия кода тестами стоит стремиться, и, получив в ответ что-нибудь вроде «95%», с картинным удивлением вопрошал: «Выходит, вам всё равно, работают ли остальные пять процентов вашего кода? Так стоило ли их писать?!»
В Go «из коробки» хороший инструментарий для тестирования и анализа покрытия кода тестами. Лично мне нравится ещё визуализировать для наглядности — обычно с помощью Go Cover Treemap:
$ go test -covermode="atomic" -coverprofile cover.out ./...
$ go-cover-treemap -coverprofile cover.out -percent > cover.svg
Это мой учебный проект для курса2 с Яндекс.Практикума3. Есть проблема: реальный процент покрытия в этом проекте сильно больше, чем показывает эта диаграмма.
Дело в том, что я не очень люблю юнит-тесты в internal-пакетах: писать их муторно, а рефакторить код они скорее мешают, особенно при активной разработке, когда контракты функций меняются, а логика время от времени переезжает из пакета в пакет. Зато люблю писать интеграционнные тесты на задачу в целом; они и рефакторить не мешают, и соблюдение ТЗ проверяют, и TDD с ними получается куда веселее. Вот и в этом проекте код хендлеров по большей части тестируется опосредованно интеграционными тестами из пакета router
. По умолчанию go test
считает покрытие только для текущего пакета, и покрытие участков кода пакета handlers
тестами, относящимися к пакету router
, игнорирует.
К счастью, это легко исправить:
$ go test -covermode="atomic" -coverpkg='./...' -coverprofile cover.out ./...
$ go-cover-treemap -coverprofile cover.out -percent > cover_full.svg
Другое дело! Бывает ещё полезно добавить учёт покрытия end-to-end-тестами, но в этом проекте я не заморачивался4.
Впрочем, если метрика всё-таки становится целью, возникает желание посрезать углы. Вот, например:
$ go test -covermode="atomic" -coverpkg='./...' -coverprofile cover.out ./...
$ go tool cover -html=cover.out
«Если поведение паники надо тестировать, в этом месте должна быть не паника, а возврат ошибки!» — справедливо отмечает автор Courtney. Я с ним не во всём согласен, но тут — да.
$ courtney -t="-covermode=atomic"
$ go tool cover -html=coverage.out
$ courtney -t="-covermode=atomic"
$ go-cover-treemap -coverprofile coverage.out -percent > cover_courtney.svg
Ой, что-то пакетов поубавилось! Courtney, если я правильно понимаю, игнорирует код, который не компилируется при тестировании. Значит, надо добавить тесты на main
, можно даже без самих тестов — весь остальной код в проекте так или иначе вызывается оттуда:
$ echo "package main" > cmd/agent/main_test.go
$ echo "package main" > cmd/server/main_test.go
$ courtney -t="-covermode=atomic"
$ go-cover-treemap -coverprofile coverage.out -percent > cover_courtney_full.svg
Но всё-таки «Don’t panic!» — девиз правильных гоферов, а подход Courtney кажется мне читерским5, так что для себя я строю диаграммы из отчётов go test
.
А вы как развлекаетесь вечерами?
-
Закон Гудхарта никто не отменял: когда метрика становится целью, она перестаёт быть хорошей метрикой. ↩︎
-
Ссылка реферальная. Если будете покупать — не проходите мимо, вам несложно, мне — приятно. ↩︎
-
Курс мне проспонсировал работодатель. Неплохой курс, толковый, нравится. Сам себе купил бы вряд ли, а так — получаю удовольствие. ↩︎
-
С end-to-end чуть больше возни, надо собрать бинарник, фиксирующий выполнение кода, но это тоже несложно. ↩︎
-
Courtney игнорирует не только паники, но и, например, передачу ненулевых ошибок без обработки, а это уже нечестно. ↩︎