Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~18 мин +20 XP

Тесты в CI: гонки, покрытие и Testify

Пайплайн для go тесты ci собирается из трёх вещей: прогон go test с -race, профиль покрытия и вменяемая реакция на красный тест. Testify здесь без отдельного моста: его вывод уезжает в t.Error, и CI честно видит FAIL с diff. Ниже — команда, флаги, кэш, flaky и спор о пороге покрытия.

Содержание урока · 10 разделов

Пятница, вечер: пайплайн красный, а тот же коммит у всех зелёный. Открываете лог — 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, всё остальное — раскраска лога.

Шаг тестов в пайплайне

GO
go 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

GO
func 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, — общий срез в параллельных сабтестах на канал или мьютекс, как в уроке про мьютексы, зависимость от сети — на заглушку. Ретрай допустим как временная мера с тикетом и датой сноса. Правило простое: зелёный со второго раза — это красный, который вам пока повезло не увидеть.

Вилка testing.Short в тесте

GO
func 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 — временная мера с тикетом; честный фикс — убрать недетерминизм из теста.

Соберите шаг тестов

Допишите флаги: детектор гонок, отчёт покрытия по функциям и прогон без кэша.

go test -coverprofile=coverage.out ./... go tool cover -=coverage.out go test -short -=1 ./...

Почините гонку в сабтесте

Расставьте строки фикса: мьютекс вокруг записи и проверки после ожидания.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: тесты в CI

Термин

-race

нажмите, чтобы перевернуть
Определение

Детектор гонок: ловит конкурентный несинхронизированный доступ к памяти. Цена — рост памяти и времени в разы; гонка зависит от расписания горутин.

1 / 6

Прогон в 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 или мьютекс вокруг записи.

Личные заметкиПусто
Хранятся только в вашем браузере (Testify · Тесты в CI: гонки, покрытие и Testify)

Вопросы и ответы по уроку

Почему тест зелёный на моей машине и красный в CI?

Чаще всего — расписание горутин и -race: у вас пересечение записей ни разу не совпало, на загруженном агенте CI совпало, и детектор честно краснит. Вторая причина — кэш: у вас прогон печатает ok (cached) без исполнения. Начните диагностику с -race и -count=1 — обычно вопрос закрывается.

Какой процент покрытия считать нормальным?

Универсального числа нет: сто процентов достижимы и бессмысленны, если проверки пустые. Я смотрю на критичный путь — платёжные и авторизационные ветки, парсинг внешних данных: там держу высокий процент и читаю -html, какие строки серые. Обвязке и типовым геттерам хватает минимума. Полезнее порога-абсолюта — храповик: не падать ниже достигнутого.

Testify-тесты как-то особенно настраивать в CI?

Нет: провал ассерта — это обычный t.Errorf, go test печатает FAIL и уходит с ненулевым кодом, любой пайплайн это понимает. Единственная настройка — культура сообщений: msgAndArgs с контекстом делают лог читаемым. Отдельно следите, чтобы тесты не зависели от сети и времени: они ломаются раньше, чем testify успевает помочь.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Viper25 уроков~9 чпрактика25 уроков уже доступны

Viper: конфигурация приложений на Go

Интерактивный курс Viper для Go: YAML и env-переменные, приоритеты источников, Unmarshal, WatchConfig, профили окружений и секреты. 25 уроков с примерами.

  • Собирать конфиг из файлов, окружения и флагов в один реестр
  • Читать значения типизированно: Get, GetString, Unmarshal в структуры
  • Задавать приоритеты источников: флаг > env > файл > default
  • Перечитывать конфиг на лету через WatchConfig и OnConfigChange