Покрытие в картинках

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

Покрытие тестами — полезная метрика, если не делать из неё цели1. Дядя Боб на одной из лекций спрашивал аудиторию, к какому проценту покрытия кода тестами стоит стремиться, и, получив в ответ что-нибудь вроде «95%», с картинным удивлением вопрошал: «Выходит, вам всё равно, работают ли остальные пять процентов вашего кода? Так стоило ли их писать?!»

В Go «из коробки» хороший инструментарий для тестирования и анализа покрытия кода тестами. Лично мне нравится ещё визуализировать для наглядности — обычно с помощью Go Cover Treemap:

$ go test -covermode="atomic" -coverprofile cover.out ./...
$ go-cover-treemap -coverprofile cover.out -percent > cover.svg

визуализация покрытия кода тестами, 49,2%

Это мой учебный проект для курса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

визуализация покрытия кода тестами, 71,5%

Другое дело! Бывает ещё полезно добавить учёт покрытия end-to-end-тестами, но в этом проекте я не заморачивался4.

Впрочем, если метрика всё-таки становится целью, возникает желание посрезать углы. Вот, например:

$ go test -covermode="atomic" -coverpkg='./...' -coverprofile cover.out ./...
$ go tool cover -html=cover.out

фрагмент кода, в этом файле 86,4% покрытия

«Если поведение паники надо тестировать, в этом месте должна быть не паника, а возврат ошибки!» — справедливо отмечает автор Courtney. Я с ним не во всём согласен, но тут — да.

$ courtney -t="-covermode=atomic"
$ go tool cover -html=coverage.out

тот же фрагмент кода, теперь в этом файле 97,4% покрытия

$ courtney -t="-covermode=atomic"
$ go-cover-treemap -coverprofile coverage.out -percent > cover_courtney.svg

визуализация покрытия кода тестами по анализу Courtney

Ой, что-то пакетов поубавилось! 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

визуализация покрытия кода тестами, 75,9%

Но всё-таки «Don’t panic!» — девиз правильных гоферов, а подход Courtney кажется мне читерским5, так что для себя я строю диаграммы из отчётов go test.

А вы как развлекаетесь вечерами?


  1. Закон Гудхарта никто не отменял: когда метрика становится целью, она перестаёт быть хорошей метрикой. ↩︎

  2. Ссылка реферальная. Если будете покупать — не проходите мимо, вам несложно, мне — приятно. ↩︎

  3. Курс мне проспонсировал работодатель. Неплохой курс, толковый, нравится. Сам себе купил бы вряд ли, а так — получаю удовольствие. ↩︎

  4. С end-to-end чуть больше возни, надо собрать бинарник, фиксирующий выполнение кода, но это тоже несложно↩︎

  5. Courtney игнорирует не только паники, но и, например, передачу ненулевых ошибок без обработки, а это уже нечестно. ↩︎