- Что такое покрытие
- Первый пример: простой модуль
- Как увидеть подробнее
- Визуальный отчёт
- Добавим тест
- Как это выглядит в больших проектах
- Практический сценарий
Когда разработчик пишет тесты, естественный вопрос — «а действительно ли мой код проверен полностью?». Может оказаться, что часть функций никогда не вызывается из тестов. В таком случае ошибки в этих местах останутся незамеченными.
Чтобы оценить полноту тестов, используют покрытие кода (coverage). В Go этот инструмент встроен прямо в стандартную систему тестирования — достаточно запустить go test
с нужными флагами.
Что такое покрытие
Покрытие показывает, сколько строк программы было выполнено во время запуска тестов. Если функция была вызвана, значит её строки засчитаны в покрытие. Если нет — они остаются «красными» в отчёте.
Важно понимать: покрытие отвечает на вопрос «был ли этот код выполнен во время тестов», но не говорит «проверялся ли результат правильно». То есть можно вызвать функцию и ничего не проверить — покрытие вырастет, но пользы от такого теста мало.
Первый пример: простой модуль
Допустим, есть пакет calc
с двумя функциями:
package calc
func Add(a, b int) int {
return a + b
}
func Sub(a, b int) int {
return a - b
}
И есть тест, который проверяет только сложение:
package calc_test
import (
"calc"
"testing"
)
func TestAdd(t *testing.T) {
got := calc.Add(2, 2)
want := 4
if got != want {
t.Errorf("Add(2, 2) = %d, хотели %d", got, want)
}
}
Теперь запускаем тест с покрытием:
go test -cover
Вывод:
ok calc 0.003s coverage: 50.0% of statements
Go говорит: покрытие 50%. Это значит, что половина строк кода была выполнена тестами, а вторая половина (функция Sub
) вообще не вызывалась.
Как увидеть подробнее
Флаг -cover
показывает только общий процент. Но часто важно понять, какая именно функция осталась без теста. Для этого используют флаг -coverprofile
.
go test -coverprofile=coverage.out
Теперь результаты записаны в файл coverage.out
. Внутри хранится карта: какие строки выполнялись, а какие нет. Этот файл можно разобрать с помощью утилиты go tool cover
.
Например:
go tool cover -func=coverage.out
Результат:
calc/calc.go:3: Add 100.0%
calc/calc.go:7: Sub 0.0%
total: (statements) 50.0%
Теперь ясно: функция Add
покрыта полностью, а Sub
— нет.
Визуальный отчёт
Чтобы было нагляднее, можно открыть отчёт в браузере. Для этого используется команда:
go tool cover -html=coverage.out
Go сгенерирует HTML-страницу: строки, которые выполнялись, будут подсвечены зелёным, а строки, до которых тесты не добрались, — красным. Это самый удобный способ увидеть, где именно остаются пробелы.
Добавим тест
Напишем проверку для Sub
:
func TestSub(t *testing.T) {
got := calc.Sub(5, 3)
want := 2
if got != want {
t.Errorf("Sub(5, 3) = %d, хотели %d", got, want)
}
}
Запустим снова:
go test -cover
Теперь:
ok calc 0.004s coverage: 100.0% of statements
А если открыть go tool cover -html=coverage.out
, все строки будут зелёными.
Как это выглядит в больших проектах
В реальном коде тесты редко покрывают всё на 100%. Всегда остаются места, которые проверять сложно: ветки с ошибками, защитные условия, редко используемые сценарии.
Обычно нормальным считается уровень от 70 до 90 процентов. Стремиться к 100% имеет смысл только в библиотеке с критичной логикой (например, криптография).
Практический сценарий
Например, есть функция:
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
Если тесты проверяют только корректное деление, то ветка с ошибкой остаётся красной.
func TestDivide_OK(t *testing.T) {
got, err := Divide(10, 2)
if err != nil {
t.Fatal(err)
}
if got != 5 {
t.Errorf("ожидали 5, получили %d", got)
}
}
Запускаем с покрытием — и видим, что часть кода не проверена. Нужно добавить тест на ошибочный сценарий:
func TestDivide_Error(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Fatal("ожидали ошибку, но получили nil")
}
}
Теперь покрытие станет 100%, и в HTML-отчёте обе ветки будут зелёными.
Покрытие кода тестами показывает, какие строки программы реально выполняются при прогоне тестов. В Go для этого встроен инструмент: go test -cover
выводит общий процент, -coverprofile
сохраняет подробности в файл, а go tool cover -html=coverage.out
позволяет открыть отчёт в браузере.
Это удобный способ видеть, где тесты отсутствуют совсем. Но покрытие не заменяет здравый смысл: можно достичь 100%, но при этом тесты будут проверять только «что функция вызвалась», а не правильность результата. Полезность тестов определяется не только числом зелёных строк, но и тем, насколько они действительно проверяют логику программы.
Самостоятельная работа
Мы написали маленький «анализатор» текста — он посчитает, сколько раз встречается каждое слово. Cлова — это последовательности букв и цифр, регистр игнорируется.
Что сделать
- Напишите тесты с 100% покрытием: разные кейсы, пустые строки, смешанные символы.
- Проверьте покрытие командами ниже.
- Подключите генерацию отчета в SonarQube. Добавьте бейдж с информацией о покрытии в README в репозиторий.
package textstat
import (
"strings"
"unicode"
)
// WordCount считает частоты слов (буквы/цифры), регистр игнорируется.
func WordCount(s string) map[string]int {
toKey := func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsDigit(r) }
tokens := strings.FieldsFunc(s, toKey)
freq := make(map[string]int, len(tokens))
for _, t := range tokens {
k := strings.ToLower(t)
if k == "" {
continue
}
freq[k]++
}
return freq
}
Что покрыть в тестах
- Пустая строка и строки из одних разделителей.
- Повторы слов разного регистра → одна запись.
- Пунктуация/символы как разделители.
Подсказка
- Используйте табличные тесты и под‑тесты.
- Для отчёта по покрытию выполните команды:
go test -cover
go test -coverprofile=coverage.out
go tool cover -func=coverage.out
Показать решение
package textstat
import "testing"
func TestWordCount(t *testing.T) {
tests := []struct {
name string
in string
want map[string]int
}{
{"empty", "", map[string]int{}},
{"delims_only", " , ! ", map[string]int{}},
{"simple", "go go GO", map[string]int{"go": 3}},
{"mixed", "Hello, go-dev! Go.", map[string]int{"hello": 1, "go": 2, "dev": 1}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := WordCount(tc.in)
if len(got) != len(tc.want) {
t.Fatalf("len: got %d, want %d", len(got), len(tc.want))
}
for k, v := range tc.want {
if got[k] != v {
t.Fatalf("%s: got %d, want %d", k, got[k], v)
}
}
})
}
}
Дополнительные материалы
- go tool cover — документация
- go test — опции покрытия
- The cover story (статья)
- Sonarcloud — Go test coverage
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.