Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Почему нельзя тестировать конкурентный код через time.Sleep?
Sleep делает тест одновременно медленным и ненадёжным: на медленной CI-машине сон мал — тест флакает красным, на быстрой — сон велик, тест минутами ждёт того, что происходит за миллисекунды. Правильная замена — синхронизация результата: канал, WaitGroup, sync.Cond. Если функция сигнализирует о завершении сама (канал результата, колбэк), тест читает сигнал; если нет — это дефект дизайна функции, и чинить надо её, а не тест. Формулировка для интервьюера: «sleep проверяет время, а тест должен проверять поведение».
// ПЛОХО: time.Sleep(500 * time.Millisecond); assert(result)
// ХОРОШО:
select {
case res := <-resultCh:
assert.Equal(t, want, res)
case <-time.After(2 * time.Second):
t.Fatal("операция не завершилась за таймаут")
}2. Как сделать конкурентную функцию тестируемой? Инъекция синхронизации.
Приём: функция получает «канал завершения» или сигнальный интерфейс параметром, и тест подставляет детерминированную синхронизацию вместо реальной асинхронности. Варианты: колбэк onDone как параметр; метод принимает канал, в который пишет результат; зависимость-интерфейс (Clock/Scheduler) инъекцией — в тесте fake-часы, тикающие вручную. Идея одна: тест управляет моментами «когда», код — «что». Противопоказание: не добавлять параметры только ради теста, если поведение уже детерминировано — сначала убрать саму недетерминированность.
// Функция тестируема: момент завершения управляется каналом
func worker(jobs <-chan Job, done chan<- struct{}) {
for range jobs {}
close(done)
}
// Тест: 1 задача → worker завершится детерминированно
jobs := make(chan Job); done := make(chan struct{})
go worker(jobs, done)
close(jobs)
<-done // детерминированное ожидание3. Как гонять тесты с детектором гонок в CI?
Флаг -race у go test (и go build -race для staging-бинарника): тесты компилируются с инструментацией обращений к памяти, детектор строит happens-before граф и роняет тест при гонке. Практики: -race в CI на каждый PR (цена — замедление ×2-20, память ×5-10 — приемлемо для тестов); гонять под -race тесты, реально выполняющие параллельные ветки (t.Parallel, конкурентные сценарии), иначе детектору нечего анализировать; флейки под -race — не «перезапустить», а чинить. Плюс: под -race паника от гонки даёт точные трассы обеих сторон — источник правды при разборе.
go test -race -count=2 ./... # count=2: два прогона ловят гонки, проявляющиеся не с первого раза
4. Что такое goleak и как он ловит утечки горутин в тестах?
goleak (uber-go/goleak) — библиотека, которая сравнивает список живых горутин до и после теста: если после теста появились новые горутины, которых не было до — тест падает со стеком «виновника». Использование: defer goleak.VerifyNone(t) в начале теста (или TestMain для всех). Идеально ловит забытые воркеры, не закрытые подписки, зависшие на каналах горутины — то, что в проде было бы медленной утечкой. Нюанс: фоновые горутины самого приложения (пулы, поллеры) надо либо останавливать в тесте, либо исключить через goleak.IgnoreTopFunction.
func TestWorkerStops(t *testing.T) {
defer goleak.VerifyNone(t) // упадёт, если воркер не остановился
stop := make(chan struct{})
go worker(stop)
close(stop)
}5. Как тестировать функцию, которая должна НЕ завершиться (бесконечный цикл обслуживания)?
Переворот вопроса: тестировать нужно не «не завершилась», а «реагирует на сигналы». Функцию обслуживания делают управляемой (done-канал, контекст, инъекция), и тест проверяет: (1) по сигналу она завершается — детерминированно через <-done с таймаутом; (2) во время работы обрабатывает входы — через каналы теста; (3) после завершения не оставляет горутин — goleak. Прямой тест «бесконечный цикл не завершился» невозможен по определению — это маркер, что функцию надо рефакторить в управляемую форму. На собеседовании этот переворот — ключевой ответ.
func TestServiceStopsOnSignal(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { svc.Run(ctx); close(done) }()
cancel()
select {
case <-done: // штатная остановка
case <-time.After(2 * time.Second):
t.Fatal("сервис не остановился по отмене контекста")
}
}6. Почему -count=2 и -shuffle находят больше багов?
-count=2 (и больше) прогоняет каждый тест несколько раз: ловит зависимость от порядка, неочищенное состояние между прогонами и гонки, проявляющиеся не с первого раза. -shuffle=on перемешивает порядок тестов: тесты, падающие при другом порядке, зависят от глобального состояния (пакетные переменные, кэш) — зависимость от порядка это дефект. Тесты должны быть изолированы: любой тест обязан проходить в одиночку, в любом порядке и много раз подряд. В CI: go test -race -count=2 -shuffle=on ./... — минимальный набор, ловящий целый класс «работает на моей машине».
go test -race -count=2 -shuffle=on ./...
7. Как тестировать, что операция действительно конкурентна (а не последовательна под капотом)?
Приём «барьер синхронизации»: запустить N операций параллельно (WaitGroup), и внутри каждой блокироваться на общем барьер-канале, который тест открывает после того, как все дошли до него. Если реализация «параллельна» лишь формально (последовательный цикл под капотом), тест зависнет на барьере и упадёт по таймауту. Альтернатива для API-уровня: замер времени — N операций за время, сопоставимое с одной (с запасом), но временные тесты флакают, поэтому барьер — основной приём, время — вспомогательная метрика.
barrier := make(chan struct{})
var wg sync.WaitGroup
results := make([]Result, 5)
for i := range results {
wg.Add(1)
go func(i int) {
defer wg.Done()
<-barrier // все дошли — значит, работали параллельно
results[i] = fetch(i)
}(i)
}
close(barrier) // открыли барьер
wg.Wait()Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Testify: тестирование на Go с assert, require и mock — Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.