Содержание урока · 10 разделов
- Команда прогона: go test -race и coverprofile
- Покрытие кода go: -func, -html и честный процент
- Testify в отчётах: вывод уезжает в t.Error
- Кэш go test: -count=1 и testing.Short
- Флаг — что делает
- Flaky: retries против честного ожидания
- Правила прогона в CI
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пятница, вечер: пайплайн красный, а тот же коммит у всех зелёный. Открываете лог — DATA RACE в тесте, который никто не трогал месяц. Это не заговор CI: раннер нагружен соседними джобами, планировщик крутится иначе, и гонка, которую у вас пронесло сотню прогонов, в пайплайне выстрелила с первого раза. Хорошая новость: -race поймал её до продакшена. Плохая: если гонки в CI-прогоне нет, вы всё равно летите вслепую — то, что зелёный, ещё не значит, что код корректен. Ниже — минимальный честный прогон тестов для пайплайна: команда, флаги, покрытие, кэш, flaky и то, как во всём этом выглядит вывод testify.
Команда прогона: go test -race и coverprofile#
Ядро go тесты ci — одна команда: go test -race -coverprofile=coverage.out ./... Троеточие берёт все пакеты модуля, а не только тот, что меняли, -race включает детектор гонок, -coverprofile пишет профиль покрытия в файл. Про -race честно: детектор не бесплатный — по документации память может расти в пять-десять раз, время исполнения в два-двадцать. Для юнит-тестов это копейки, и они дешевле ночной гонки в проде: детектор ловит конкурентный доступ к памяти, который горутины устраивают молча, а механика этих горутин — отдельный урок в go-advanced. Провал любого теста — ненулевой код выхода: именно на него смотрит CI, всё остальное — раскраска лога.
Шаг тестов в пайплайне
GOgo test -race -count=1 -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out -o coverage.htmlОдна команда делает три дела: гоняет все пакеты с детектором гонок, отключает кэш флагом -count=1 и пишет профиль покрытия в файл. Затем -func печатает процент по каждой функции и итог, а -html собирает интерактивную карту строк в отдельный файл артефактом. Точно так же это выглядит и в локальном прогоне: песочница пайплайны не исполняет.
Проверка: зачем -race, если он медленный
Зачем тащить -race в CI, если он замедляет прогон?
Покрытие кода go: -func, -html и честный процент#
Профиль покрытия считает операторы: какая доля строк исполнилась под тестами. go tool cover -func=coverage.out печатает процент по каждой функции и итог, -html рисует карту файла, где зелёное исполнялось, а серое — нет. Теперь честно о цифрах: сто процентов не цель. Покрытие говорит «строка исполнилась», но не «результат проверен»: можно вызвать функцию и не сравнить ничего — процент вырастет, качество нет. Осмысленная цель — держать высоким критичный путь: деньги, доступы, парсинг внешних данных, ветки ошибок. Вспомогательная обвязка и типовые геттеры в процентах не нуждаются. Смотреть на total и радоваться — самообман; смотреть, какие именно строки остались серыми, — работа на десять минут, которая находит мёртвые ветки быстрее любого ревью.
Testify в отчётах: вывод уезжает в t.Error#
Testify в CI не нужен отдельный мост. Провал ассерта — это t.Errorf внутри: тест помечается проваленным, go test печатает FAIL: TestXxx с вашим diff, а на выходе процесс уходит с ненулевым кодом. assert после провала тест не прерывает: остальные проверки кейса отработают, и в отчёте соберутся все провалы разом — иногда это экономит итерацию отладки. require рвёт тест на первом провале — правило выбора разобрано в уроке про assert и require. А будет ли в логе CI понятная строка или загадочное not equal, решают сообщения — стиль msgAndArgs отрабатывали в уроке про сообщения. CI видит всё это без спецобработки: FAIL и код выхода — стандартная реакция testing, testify просто наполняет её содержимым.
Гонка, которую ловит -race
GOfunc TestCollector(t *testing.T) {
var got []string
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func(id string) {
defer wg.Done()
got = append(got, id) // data race
}(task)
}
wg.Wait()
assert.Len(t, got, len(tasks))
}Несколько горутин делают append в один срез: растут и заголовок, и массив — одновременная запись это гонка. wg.Wait синхронизирует завершение горутин, но не доступ внутри цикла. Под -race прогон падает с WARNING: DATA RACE и указанием обеих точек; без него тест может проходить месяцами. Такой пакет видит и локальный go test — песочница горутины не исполняет.
Что вернёт прогон под -race?
Прочитайте код и выберите, что он выведет:
func TestCounter(t *testing.T) {
var n int
var wg sync.WaitGroup
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n++ // без синхронизации
}()
}
wg.Wait()
fmt.Println(n)
}Кэш go test: -count=1 и testing.Short#
go test кэширует результаты: если пакет и его зависимости не менялись, повторный прогон печатает ok (cached) и не исполняет ничего. У себя на машине это скорость; в пайплайне с прогретым кэшем сборщика — риск взять старое зелёное. Флаг -count=1 отключает кэш: каждая попытка честно исполняется, а -count=5 запустит тест пять раз подряд — быстрый способ вылова flaky без всего пайплайна. Для длинных тестов есть вилка testing.Short: внутри теста if testing.Short() { t.Skip(...) }, а короткий прогон с -short идёт на каждый коммит, полный — по расписанию или перед релизом. И тонкость про testify: медленный тест со sleep кэш не волнует — он останется медленным в любом профиле, и лечить его надо не флагами, а ожиданием факта.
Флаг — что делает#
| Флаг | Что делает | Когда нужен |
|---|---|---|
| -race | Включает детектор гонок | Каждый прогон с горутинами — то есть почти любой |
| -coverprofile=file | Пишет профиль покрытия | Шаг тестов в CI, дальше -func или -html |
| -count=1 | Отключает кэш результатов | Пайплайн с прогретым кэшем сборщика |
| -short | Включает testing.Short | Быстрый прогон на каждый коммит |
| -run REGEX | Фильтр тестов по имени | Точечная отладка одного кейса |
| -timeout 5m | Таймаут на пакет тестов | Страховка от зависшей горутины: по умолчанию 10 минут |
| -v | Подробный вывод | Разбор, что именно исполнилось |
Flaky: retries против честного ожидания#
Flaky-тест краснеет без причины и зеленеет при повторе. Первое желание — retries: перезапускать красный до зелёного и наслаждаться тишиной. Это затычка: ретрай не чинит причину, а прячет — под шум проползает настоящий баг, у которого тоже «иногда краснеет». Честный путь — убрать недетерминизм: sleep перед проверкой горутины заменить на ожидание факта через Eventually — механика в уроке про Eventually, — общий срез в параллельных сабтестах на канал или мьютекс, как в уроке про мьютексы, зависимость от сети — на заглушку. Ретрай допустим как временная мера с тикетом и датой сноса. Правило простое: зелёный со второго раза — это красный, который вам пока повезло не увидеть.
Кейс: тест гонялся у разработчика сотню раз — зелёный; в пайплайне -race поймал DATA RACE на append в горутине из первого дня. Объяснение машин: у ноутбучного прогона другое расписание — ядра, загрузка, тайминги, и пересечение записей не совпало ни разу. Детектор гонок не «стал строже в CI» — он одинаковый; совпасть записям на другой машине стало вероятнее. Мораль: красный с -race в CI — не флейк и не каприз пайплайна, это настоящий баг, который у вас просто ещё не случился. Чинится синхронизацией, а не ретраями.
Одна школа ставит гейт: покрытие ниже восьмидесяти процентов — пайплайн красный. Другая против: процент набирается мусорными тестами без проверок, а честная непокрытая ветка ошибки наказывает. Моя граница: порог вреден как абсолют, полезен как храповик — не даёт покрытию упасть ниже достигнутого и ловит большие куски кода без единого теста. На критичные пакеты ставлю требование жёстче и смотрю -html глазами: какие именно строки серые. Процент отвечает «сколько исполнилось», но не «насколько можно верить отчёту» — это разные вопросы, и второй важнее.
Вилка testing.Short в тесте
GOfunc TestSMTPRoundtrip(t *testing.T) {
if testing.Short() {
t.Skip("полный прогон SMTP-заглушки — вне -short")
}
assert.Eventually(t, func() bool {
return outbox.HasMail("welcome")
}, 3*time.Second, 20*time.Millisecond,
"письмо не ушло за три секунды")
}Быстрый прогон с -short скипает тяжёлый тест честно: в отчёте стоит SKIP, а не молчаливое исчезновение. Сама проверка — Eventually с тиком и дедлайном: письмо может дойти не мгновенно, и тест ждёт факт, а не sleep. Сообщение в msgAndArgs уезжает в лог CI, если дедлайн истёк. Это тестовый пакет: песочница заглушку SMTP не поднимает, прогоны даны как ориентир.
Проверка: что увидит CI
Тест с assert.Equal провалился на третьей из десяти проверок. Что увидит CI?
Правила прогона в CI#
- Базовая команда: go test -race -count=1 -coverprofile=coverage.out ./...
- -race на каждый прогон: гонка зависит от расписания, зелёный без детектора ничего не доказывает.
- Покрытие — по критичному пути: 100% не цель, непокрытые ветки ошибок дороже геттеров.
- Кэш отключён -count=1; длинные тесты — вилка testing.Short и отдельный полный прогон.
- assert в отчёте CI выглядит штатно: FAIL, diff и ненулевой код выхода.
- Retries — временная мера с тикетом; честный фикс — убрать недетерминизм из теста.
Соберите шаг тестов
Допишите флаги: детектор гонок, отчёт покрытия по функциям и прогон без кэша.
Почините гонку в сабтесте
Расставьте строки фикса: мьютекс вокруг записи и проверки после ожидания.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: тесты в CI
Прогон в CI — та же testing-машина, что и у вас на машине: go test с флагами, код выхода и отчёт. Testify встраивается в неё без моста — ассерты наполняют FAIL содержимым, а моки и suite живут в тех же тест-функциях. Полезная связка на выходе: -race ловит то, что планировщик обходит, покрытие показывает серые зоны, кэш не подсовывает старое. Дальше по курсу — последние темы: чем закрыть тестирование HTTP-заглушек и что забрать с собой в собственный проект. Карта всех уроков — на странице testify-курса. Привычка на неделю: каждый красный прогон в CI разбирайте без ретрая — сначала причина, потом кнопка перезапуска.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Шаг тестов: go test -race -count=1 -coverprofile=coverage.out ./... и ненулевой код выхода при провале.
- -race ловит конкурентный доступ; цена — память и время в разы, для юнит-тестов это норм.
- Покрытие считает исполненные операторы: строка исполнилась не значит, что результат проверен.
- Критичный путь важнее процента: 100% не цель, серые строки ошибок — сигнал.
- Кэш go test отключает -count=1; длинные тесты — вилка testing.Short и отдельный полный прогон.
- assert уезжает в отчёт штатно: FAIL, diff, все провалы кейса разом.
- Retries — затычка с тикетом; честный фикс — убрать недетерминизм: Eventually вместо sleep, синхронизация вместо вольных горутин.
Домашнее задание
Соберите шаг тестов из урока: go test -race -count=1 -coverprofile=coverage.out ./... плюс отчёт через go tool cover -func. Откройте -html на самом непокрытом пакете и выпишите две серые строки, которые важнее геттеров. Найдите flaky-тест с ретраями в своём пайплайне и замените его причину: Eventually вместо sleep или мьютекс вокруг записи.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему тест зелёный на моей машине и красный в CI?
Чаще всего — расписание горутин и -race: у вас пересечение записей ни разу не совпало, на загруженном агенте CI совпало, и детектор честно краснит. Вторая причина — кэш: у вас прогон печатает ok (cached) без исполнения. Начните диагностику с -race и -count=1 — обычно вопрос закрывается.
Какой процент покрытия считать нормальным?
Универсального числа нет: сто процентов достижимы и бессмысленны, если проверки пустые. Я смотрю на критичный путь — платёжные и авторизационные ветки, парсинг внешних данных: там держу высокий процент и читаю -html, какие строки серые. Обвязке и типовым геттерам хватает минимума. Полезнее порога-абсолюта — храповик: не падать ниже достигнутого.
Testify-тесты как-то особенно настраивать в CI?
Нет: провал ассерта — это обычный t.Errorf, go test печатает FAIL и уходит с ненулевым кодом, любой пайплайн это понимает. Единственная настройка — культура сообщений: msgAndArgs с контекстом делают лог читаемым. Отдельно следите, чтобы тесты не зависели от сети и времени: они ломаются раньше, чем testify успевает помочь.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Viper: конфигурация приложений на Go
Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.
- Собирать конфиг из файлов, окружения и флагов в один реестр
- Читать значения типизированно: Get, GetString, Unmarshal в структуры
- Задавать приоритеты источников: флаг > env > файл > default
- Перечитывать конфиг на лету через WatchConfig и OnConfigChange