Golang: как найти мёртвый код в проекте, а заодно оценить покрытие тестами живого кода

В 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))
}

Живое покрытие отобразится так:
s8an188as2jmn55_wuni0utmafa.png

Т.е. без учета функции mulValues, которая на проде никогда не запускалась. И покрытие получилось 25%, а не 20%, как если бы мы просто посмотрели с помощью go test -cover.

© Habrahabr.ru