В 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

Например, для такого проекта:


main.go
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
}

main_test.go

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.

Комментарии (0)