Как измерить test coverage в Go и не сойти с ума
Вы написали тесты для своего Go-приложения. Чувствуете себя супергероем? Правильно делаете. Но есть один нюанс: сколько именно кода покрыто тестами? Без ответа на этот вопрос ваша уверенность — просто гадание на кофейной гуще.
Вопрос "How to measure test coverage in Go" — один из самых популярных на Stack Overflow (десятки тысяч просмотров). И это не случайно. Разработчики хотят не просто писать тесты, а понимать, насколько они эффективны. В этом туториале мы разберем все способы измерения test coverage в Go: от базовых команд до продвинутых техник с профилями и HTML-отчетами.
Что такое test coverage и зачем его измерять?
Test coverage (покрытие кода тестами) — это метрика, показывающая, какой процент вашего исходного кода выполняется во время тестов. Выражается в процентах. 100% coverage — это не всегда Святой Грааль, но стремиться к высоким показателям стоит.
Зачем это нужно?
- Контроль качества: вы видите, какие участки кода остались без тестов
- Уверенность в рефакторинге: высокое покрытие снижает риск что-то сломать
- CI/CD метрики: можно настроить порог coverage и не пускать код с низким покрытием
- Документация: тесты — это живая документация, а coverage показывает, насколько она полная
Подвох: coverage ≠ качество тестов
Помните: 100% coverage не гарантирует отсутствие багов. Можно написать тесты, которые просто вызывают функции, но ничего не проверяют. Coverage — это количественная метрика. Качество тестов — качественная. Используйте оба подхода.
Базовое измерение: go test -cover
Начнем с самого простого. У Go есть встроенная поддержка coverage прямо в инструменте go test. Давайте создадим простой проект и посмотрим, как это работает.
Создайте файл math.go:
package main
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
func Multiply(a, b int) int {
return a * b
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, nil // Ошибка: должно быть return 0, errors.New("division by zero")
}
return a / b, nil
}
А теперь — тест (math_test.go):
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("expected 5, got %d", result)
}
}
func TestSubtract(t *testing.T) {
result := Subtract(10, 4)
if result != 6 {
t.Errorf("expected 6, got %d", result)
}
}
// Намеренно не тестируем Multiply и Divide
Теперь запустим тесты с флагом -cover:
go test -cover
Вывод будет примерно таким:
ok example/math 0.002s coverage: 50.0% of statements
Все верно — покрыто 50% кода, потому что Multiply и Divide не тестируются. Но это лишь общая цифра. А что, если мы хотим увидеть детальный профиль покрытия?
Профиль покрытия: go test -coverprofile
Флаг -coverprofile сохраняет детальную информацию о покрытии в файл:
go test -coverprofile=coverage.out
Теперь в папке появился файл coverage.out. Откройте его — это текстовый формат, где каждая строка показывает, была ли выполнена конкретная инструкция кода. Но читать его вручную — скучно. Давайте перейдем к визуализации.
HTML-отчет: go tool cover -html
Самый удобный способ анализа coverage — HTML-отчет. Он подсвечивает выполненный код зеленым, а невыполненный — красным.
go tool cover -html=coverage.out -o coverage.html
Откройте coverage.html в браузере. Вы увидите:
- Зеленые строки — код, который выполняется хотя бы в одном тесте
- Красные строки — код без тестов
- Серые строки — неисполняемые строки (комментарии, пустые строки)
Для нашего примера Multiply и Divide будут красными. А еще вы заметите, что if b == 0 внутри Divide тоже красный — мы не тестировали случай деления на ноль.
Продвинутые техники: флаги -covermode и -coverpkg
Режимы покрытия: -covermode
Go поддерживает три режима покрытия:
set(по умолчанию) — была ли инструкция выполнена хоть разcount— сколько раз была выполнена каждая инструкцияatomic— как count, но потокобезопасный (для параллельных тестов)
Режим count особенно полезен, если вы хотите убедиться, что код в циклах выполняется несколько раз:
go test -covermode=count -coverprofile=coverage.out
В HTML-отчете для режима count цвет будет меняться в зависимости от количества выполнений — от зеленого (мало) до синего (много).
Покрытие для определенных пакетов: -coverpkg
Иногда нужно измерить покрытие не только для текущего пакета, но и для зависимых. Например, вы тестируете пакет handlers, а хотите увидеть покрытие в services и models:
go test -coverprofile=coverage.out -coverpkg=./services,./models ./handlers
Это особенно полезно в больших проектах с микросервисной архитектурой.
Интеграция в CI/CD: автоматический контроль coverage
Ручной запуск — это хорошо, но в современной разработке coverage должен проверяться автоматически при каждом пуше. Рассмотрим интеграцию с GitHub Actions.
Создайте файл .github/workflows/test.yml:
name: Go tests with coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run tests with coverage
run: go test -coverprofile=co