JavaRush /Курсы /Go SELF /Coverage в Go test — -cover, -coverprofile, -covermode

Coverage в Go test — -cover, -coverprofile, -covermode

Go SELF
31 уровень , 1 лекция
Открыта

1. Что такое coverage и почему это не “процент счастья”

Coverage часто воспринимают как школьную оценку: «у меня 92% — я молодец», «у меня 37% — я плохой». На практике это вредная привычка, потому что coverage в Go отвечает на более узкий и честный вопрос: выполнялись ли операторы вашего кода во время выполнения тестов. Не выполнялись — значит тесты туда не ходили; выполнялись — значит ходили. Это не говорит, что проверки в тестах были умными, и уж точно не гарантирует отсутствие багов.

В Go, когда мы включаем coverage, go test инструментирует код и считает покрытие по statements (операторам). Поэтому ветвления (if, switch) почти всегда дают типичную картину: вы протестировали «зелёную дорожку», а «красная» (ошибки, edge cases, ранние return) осталась неисполненной — и coverage это честно подсветит. В этом смысле coverage — отличный способ напомнить: «Эй, а ты точно проверял ошибочные случаи, или только надеялся на доброту вселенной?»

Перед тем как выбирать режимы coverage, полезно понимать философию измерения. Coverage в Go по умолчанию считает statements, то есть операторы. Это не совсем то же самое, что «покрытие веток» (branch coverage) в некоторых других экосистемах. Ветвления влияют на coverage опосредованно: если у вас есть if, то операторы внутри одной ветки будут считаться покрытыми только если вы туда реально заходили.

Для новичка это отличная модель, потому что она совпадает с интуицией: «я писал код — тест должен туда зайти». Особенно хорошо coverage подсвечивает те места, которые чаще всего забывают тестировать: ранние возвраты, обработку ошибок, default в switch, проверки входных данных. Если вы пишете Go в его обычном стиле (где много if err != nil { return ... }), coverage очень быстро показывает, какие return по ошибке вы ни разу не проходили тестами.

2. go test -cover: быстрый “термометр” покрытия

Когда вы добавляете к запуску тестов флаг -cover, go test начинает считать, сколько операторов (statements) было выполнено во время тестов, и печатает процент в консоль. Это самый быстрый способ получить первичный сигнал: тесты вообще куда‑то ходят или только компилируются и делают вид, что заняты. Такой запуск особенно полезен, когда вы только что добавили новую функцию и хотите понять, не забыли ли написать тесты совсем.

Покажу на маленьком кусочке нашего учебного приложения (пусть это будет пакет todo, где мы парсим статус задачи из строки). Код нарочно сделаем с ветвлением: это удобно для демонстрации coverage.

package todo

import (
	"fmt"
	"strings"
)

type Status int

const (
	StatusTodo Status = iota
	StatusDone
)

func ParseStatus(s string) (Status, error) {
	switch strings.TrimSpace(strings.ToLower(s)) {
	case "todo":
		return StatusTodo, nil
	case "done":
		return StatusDone, nil
	default:
		return 0, fmt.Errorf("unknown status: %q", s)
	}
}

Теперь тест, который специально покрывает не всё (мы начнём с частичного покрытия, чтобы увидеть смысл процентов):

package todo

import "testing"

func TestParseStatus_Todo(t *testing.T) {
	st, err := ParseStatus("todo")
	if err != nil || st != StatusTodo {
		t.Fatalf("expected todo, got %v, err=%v", st, err)
	}
}

Запуск:

go test -cover

В выводе вы увидите строку в стиле «coverage: N% of statements». Это именно “N% операторов выполнились” — не “N% правильности”. Такой процент — хороший термометр: показывает температуру, но не ставит диагноз.

3. -coverprofile: “процент в консоли” vs “данные в файл”

Процент в консоли — это приятно, но он почти бесполезен, если вы хотите понять, что именно не покрыто. Для этого Go умеет сохранять подробные данные покрытия в файл — профиль покрытия (coverage profile). Делается это флагом -coverprofile.

Ключевой момент: если вы используете -coverprofile=..., то go test автоматически включает coverage‑анализ. То есть вам не нужно писать и -cover, и -coverprofile вместе — достаточно -coverprofile.

Пример команды:

go test -coverprofile=coverage.out

В результате вы получаете два полезных эффекта одновременно: в консоль печатается процент, а в файл coverage.out записывается профиль покрытия, который потом можно анализировать инструментами (в том числе визуально). Детальный разбор чтения профиля — отдельная тема, но важно уже сейчас привыкнуть к дисциплине: “процент посмотрел → профиль сохранил”.

И ещё одна практическая деталь из реальной жизни. Когда вы начинаете чинить покрытие, вы часто делаете такой цикл: дописал тест → прогнал тесты → снова прогнал. Если вы видите, что прогон “мгновенный”, и у вас есть подозрение, что сработал кеш go test, то вы уже знаете лекарство: -count=1. Coverage‑профиль, конечно, тоже можно пересобирать принудительно этим способом, если сомневаетесь, что изменения реально исполнились.

4. -covermode: set, count, atomic

Флаг -covermode отвечает за то, как именно считать покрытие: просто факт выполнения, количество выполнений или количество выполнений с корректным подсчётом в параллельных сценариях. В Go есть три режима: set, count, atomic. Если говорить совсем коротко: set отвечает на вопрос “выполнялся ли оператор”, count — “сколько раз выполнялся”, atomic — “как count, но с точным подсчётом в параллельных программах”.

Вот компактная табличка (её реально полезно держать в голове):

-covermode Что хранит Когда выбирать Цена/нагрузка
set (по умолчанию) Выполнялся/не выполнялся Почти всегда по умолчанию, особенно для обучения и обычных unit‑тестов Дешево
count Сколько раз выполнялся оператор Когда хотите “тепловую карту”: какие места бегают часто, какие редко Дешево (обычно достаточно)
atomic Сколько раз, но корректно при параллельном выполнении Когда важны точные счётчики при конкурентном/параллельном выполнении Дороже: использует атомарные операции, может быть заметно “тяжелее”

Режим set: стандартный и “не раздражает”

set — режим по умолчанию. Он отвечает на вопрос: “этот оператор выполнялся хотя бы раз?”. Для большинства учебных и прикладных ситуаций этого хватает за глаза. Вы, как разработчик, чаще всего хотите именно это: быстро увидеть “красные” места, то есть те куски, куда тесты вообще не заходили.

Включить set явно можно так (хотя это и не обязательно, потому что он и так дефолтный):

go test -covermode=set -coverprofile=coverage.out

Это удобно, когда вы пишете скрипты/команды одинакового вида и хотите всегда явно указывать режим.

Режим count: когда вы хотите понимать “как часто”

count вместо “да/нет” хранит, сколько раз выполнялся оператор. Это уже не только про “покрыто/не покрыто”, но и про “как распределяется активность”. Важно не путать это с профилированием производительности: coverage не измеряет время. Но как “тепловая карта” частоты выполнения count бывает полезен: например, вы видите, что какая-то часть кода исполняется тысячу раз в одном тесте, и понимаете, что это очень горячее место логики.

Команда:

go test -covermode=count -coverprofile=coverage.out

И важная деталь: процент покрытия при set и count не меняется, потому что “покрыт ли оператор” — это отдельная метрика от “сколько раз покрыт”.

Режим atomic: точные счётчики при параллельности

atomic — это тот же count, но с корректным подсчётом, когда один и тот же код может выполняться одновременно. Он использует атомарные операции (sync/atomic) и поэтому может быть ощутимо дороже.

На текущем этапе вам достаточно запомнить “зачем он существует” и “почему не надо включать его просто потому что звучит круто”. Это как надеть каску, бронежилет и взять огнетушитель, чтобы сходить за хлебом: формально безопаснее, но продавец начнёт задавать вопросы.

Команда выглядит так:

go test -covermode=atomic -coverprofile=coverage.out

5. Мини‑сценарий: как coverage помогает найти забытый тест

Теперь сделаем то, что обычно происходит в реальном проекте: добавим ещё один тест и посмотрим, как меняется покрытие смыслово.

У нас была функция ParseStatus с тремя логическими путями: "todo", "done" и ошибка (default). Сейчас тест покрывает только "todo". Давайте добавим тест на "done" (и уже заметим, что ошибки всё ещё не покрыты).

package todo

import "testing"

func TestParseStatus_Done(t *testing.T) {
	st, err := ParseStatus("done")
	if err != nil || st != StatusDone {
		t.Fatalf("expected done, got %v, err=%v", st, err)
	}
}

Запускаем:

go test -cover

Процент вырастет. Но что важно: даже если процент стал “красивее”, вы всё ещё не проверили путь с ошибкой, а именно он чаще всего ломается в проде первым. Поэтому добавим тест на неправильный ввод, чтобы покрыть default:

package todo

import "testing"

func TestParseStatus_Unknown(t *testing.T) {
	_, err := ParseStatus("i am not a status")
	if err == nil {
		t.Fatalf("expected error, got nil")
	}
}

Теперь вы увидите, что coverage приближается к 100% по этой функции. И это тот редкий случай, когда высокий coverage действительно совпадает со здравым смыслом: функция маленькая, путей мало, тесты проверили все пути.

При этом важно держать в голове взрослую мысль: даже если coverage 100% на функции, тест всё ещё может быть слабым. Например, можно написать тест, который вызывает функцию, но ничего не проверяет. Coverage будет счастлив, а баги — тоже.

6. Рабочие команды, которые удобно держать под рукой

Покрытие удобно использовать как повторяемый ритуал, а не как “раз в год перед релизом”. Поэтому полезно иметь пару стандартных команд.

Вот базовый набор, который чаще всего закрывает 90% потребностей:

go test -cover
go test -coverprofile=coverage.out
go test -covermode=count -coverprofile=coverage.out

Первая команда даёт быстрый сигнал “вообще что-то покрывается?”. Вторая сохраняет профиль. Третья сохраняет профиль с подсчётом “сколько раз”, если вам нужно не только “красное/зелёное”, но и “насколько зелёное”.

7. Типичные ошибки при работе с coverage

Ошибка №1: воспринимать coverage как доказательство корректности тестов.
Самая популярная ловушка — думать, что “80% coverage” означает “программа на 80% правильная”. Coverage измеряет факт исполнения операторов, а не качество утверждений в тестах. Можно добиться высокого coverage тестами, которые вообще ничего не проверяют, и получить идеально “покрытый” мусор.

Ошибка №2: забывать про -coverprofile и жить только с процентом.
Процент в консоли — это полезный сигнал, но он не отвечает на главный вопрос: что именно не покрыто. Без -coverprofile вы быстро упираетесь в ситуацию “процент низкий, но где именно — непонятно”. Профиль — это способ превратить покрытие из абстрактного числа в диагностические данные.

Ошибка №3: думать, что -coverprofile нужно использовать вместе с -cover, иначе coverage не включится.
Это мелочь, но она постоянно всплывает у новичков. На самом деле -coverprofile сам включает coverage‑анализ, то есть -cover становится избыточным.

Ошибка №4: включать -covermode=atomic “на всякий случай”.
atomic нужен для точного подсчёта в параллельных сценариях и может быть дорогим. Если вы пишете обычные unit‑тесты без параллельного выполнения, count почти всегда достаточно, а set — тем более.

Ошибка №5: пытаться “добить процент” вместо того, чтобы покрывать смысловые ветки.
Иногда люди начинают писать тесты ради чисел, особенно когда в команде есть KPI “не меньше 85%”. Это приводит к странным тестам, которые вызывают код без проверки результата, лишь бы строчки стали зелёными. Гораздо полезнее идти от логики: покрывайте ветки с ошибками, edge cases и инварианты функций, а процент пусть будет следствием, а не целью.

1
Задача
Go SELF, 31 уровень, 1 лекция
Недоступна
Термометр покрытия
Термометр покрытия
1
Задача
Go SELF, 31 уровень, 1 лекция
Недоступна
Профиль в файл
Профиль в файл
1
Задача
Go SELF, 31 уровень, 1 лекция
Недоступна
Статусы задач
Статусы задач
1
Задача
Go SELF, 31 уровень, 1 лекция
Недоступна
Три covermode
Три covermode
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ