В Go 1.20 сделали возможность сбилдить приложение с флагом cover
go build -cover
после чего, если запустить такое приложение, то будет собираться статистика, показывающая, какие части кода были выполнены, а какие нет, и складываться в папочку, указанную в переменной окружения.
Это, конечно, было сделано для интеграционных тестов, когда приложение запускается целиком в каких-то сценариях (а не через go test
), но, вероятно, это можно попробовать использовать и по-другому:
запустить такой бинарник прямо на проде, подержать какое-то время и посмотреть, какие участки кода в реальности никогда не запускаются.
Так можно найти недовыпиленный легаси-код, старые эндпоинты API, которые давно никому не нужны, малозначимые проверки if err != nil
и прочее. Как минимум, на это интересно посмотреть, можно найти что-нибудь удивительное.
Disclaimer: разумеется, сбор статистики создает какой-то оверхед, поэтому подойдёт точно не всем. Как вариант, можно пустить туда небольшую часть трафика.
Дальше — больше
Давайте в целом поговорим о покрытии кода. Оставим пока что за скобками, надо ли его вообще измерять (это холиварный вопрос для отдельной статьи). В любом случае, держу пари, что оно у вас не 100%, ведь стопроцентное покрытие — это очень дорого и не окупает усилий. Например, бывают такие условия if err != nil
, которые выстреливают раз в год. Протестировать их очень сложно и не всегда нужно.
Но при этом хочется понимать, насколько хорошо покрыта основная логика, без таких редких ошибок. Т.е. та, которая реально работает на проде и может поломаться при изменениях. И это можно сделать примерно тем же способом, что и выше.
Допустим, у нас есть какие-то тесты, и мы можем получить стандартный файлик-отчёт тестового покрытия. Если мы сматчим это со знанием, какой код реально работает на проде, а какой запускается слишком редко или никогда, то мы можем понять, а сколько процентов реально работающего на проде кода покрыто тестами, и можем увидеть, какие строки кода стоит покрыть в первую очередь, т.е. строки точно живого кода.
Другими словами, мы получим реальное покрытие, или ещё его можно назвать "живое покрытие".
Disclaimer: разумеется, бывают сценарии, когда что-то серьёзное происходит раз в год, и мы это пропустим (начисление годовых бонусов). Это надо учитывать. Но ведь бывают и микросервисы без отложенной логики, которые молотят одно и то же каждый день — тогда такая схема подойдет. У нас в Каруне таких полно.
Как конкретно? Как посмотреть наглядно?
1. Ищем мертвечину
Итак, мы сбилдили приложение с флагом -cover
, запускаем его на проде.
Запускать надо с переменной окружения GOCOVERDIR
GOCOVERDIR=somedata ./myapp
После завершения приложения в этой папке появятся бинарные файлы, которые можно сконвертировать в нормальный вид
go tool covdata textfmt -i=somedata -o prodcoverage.txt
и посмотреть, что там, стандартными средствами, например так:
go tool cover -html=prodcoverage.txt
Кстати, если приложение было внезапно принудительно завершено, например, из-за паники, то статистика не соберётся.
2. Считаем покрытие живого кода и смотрим наглядно
Мне пришлось потратить пару часов на выходных, чтобы написать для этого небольшую утилиту.
Как ей пользоваться?
Считаем обычное покрытие и собираем в файл
go test -coverprofile testcoverage.txt
Затем берем файл из предыдущего шага (сбор данных с прода), и берем его за основу, чтобы пересчитать testcoverage относительно реально сработавших строк кода.
go install github.com/anton-okolelov/live-coverage@latest
live-coverage prodcoverage.txt testcoverage.txt > result.txt
Смотрим, что получилось:
go tool cover -html=result.txt
Например, для такого проекта:
func main() {
fmt.Println(sumValues(2, 3))
fmt.Println(subValues(4, 1))
}
// executed and tested
func sumValues(a int, b int) int {
return a + b
}
// executed and not tested
func subValues(a int, b int) int {
return a - b
}
// dead code
func mulValues(a int, b int) int {
return a * b
}
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSumValues(t *testing.T) {
assert.Equal(t, 4, sumValues(2, 2))
}
Живое покрытие отобразится так:
Т.е. без учета функции mulValues, которая на проде никогда не запускалась. И покрытие получилось 25%, а не 20%, как если бы мы просто посмотрели с помощью go test -cover.