Зарегистрируйтесь, чтобы продолжить обучение

Покрытие кода и go test -cover Go: Автоматическое тестирование

Когда разработчик пишет тесты, естественный вопрос — «а действительно ли мой код проверен полностью?». Может оказаться, что часть функций никогда не вызывается из тестов. В таком случае ошибки в этих местах останутся незамеченными.

Чтобы оценить полноту тестов, используют покрытие кода (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)
                }
            }
        })
    }
}

Дополнительные материалы

  1. go tool cover — документация
  2. go test — опции покрытия
  3. The cover story (статья)
  4. Sonarcloud — Go test coverage

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff