Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Как устроено тестирование в Go? Что должен знать файл теста?
Тесты живут в файлах *_test.go рядом с тестируемым кодом (тот же пакет или *_test для внешних), запускаются go test ./... Тест — функция вида TestSum(t *testing.T): имя обязано начинаться с префикса Test, после которого идёт заглавная буква. Внутри — вызовы тестируемого кода и проверки; ошибки сигналятся через t.Errorf/t.Fatalf (Fatalf останавливает тест сразу, Errorf продолжает). Хелперы помечают t.Helper(), чтобы трейсы указывали на строку вызова, а не на хелпер. Никаких аннотаций и фреймворков не требуется — тест это обычный код.
func TestSum(t *testing.T) {
got := Sum(2, 3)
if got != 5 {
t.Errorf("Sum(2,3) = %d; want 5", got)
}
}2. Что такое table-driven tests и почему это стандарт Go?
Табличный тест — одна тестовая функция и таблица кейсов (входы + ожидания), которая прогоняется циклом. Плюсы: новая ветка поведения — новая строка, а не новая функция; сообщения об ошибке содержат имя кейса; локальные копии переменных цикла обязательны до Go 1.22 (v := v или параметры range) — иначе все кейсы видят последнее значение. Сабтесты через t.Run(name, func(t *testing.T)) дают отдельно запускаемые кейсы (go test -run TestSum/имя_кейса) и читаемый вывод. На собеседовании просят написать такой тест руками — тренировка обязательна.
tests := []struct {
name string
a, b, want int
}{
{"обычные", 2, 3, 5},
{"отрицательные", -1, 1, 0},
{"нули", 0, 0, 0},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
if got := Sum(tt.a, tt.b); got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}3. Чем testify отличается от стандартной библиотеки? Когда что брать?
Стандарт: сравнение через if и t.Errorf — ноль зависимостей, но длинно на сложных структурах. testify/assert — готовые ассерты (Equal, Len, Contains, ErrorIs) с читаемыми диффами; assert продолжает тест, require останавливает на первом провале (логично для подготовительных шагов: require.NoError(t, err) перед использованием результата). На собеседовании правильно: знать оба, сказать «в учебном коде — стандарт, в проекте — testify ради диффов и меньше boilerplate», и не путать: testify НЕ фреймворк запуска — тесты всё равно go test.
assert.Equal(t, want, got) // провал — тест продолжится require.NoError(t, err) // провал — тест остановится assert.ErrorIs(t, err, ErrNotFound) // сравнение ошибок
4. Как делают моки в Go? Зачем нужен интерфейс?
Мок в Go строят на интерфейсе: зависимость объявляют маленьким интерфейсом на стороне потребителя (IdFinder, Clock), в тесте подставляют реализацию-заглушку с заранее заданным поведением. Пути: (1) ручные фейки — простая структура с полями-настройками, самый читаемый вариант; (2) кодогенерация gomock/mockgen — строгие моки с проверкой вызовов (EXPECT().Get(id).Return(user)) для крупных интерфейсов; (3) testify/mock — аналогично в стиле testify. Красная тряпка на интервью: мокать всё подряд. Мокают границы (база, сеть, время, случайность); чистые функции и структуры тестируют как есть — иначе тесты проверяют моки, а не код.
type UserFinder interface { Find(id int64) (User, error) }
type fakeFinder struct{ user User; err error }
func (f fakeFinder) Find(id int64) (User, error) { return f.user, f.err }
svc := Service{Finder: fakeFinder{err: ErrNotFound}}5. Что делает t.Parallel() и где его осторожность?
Вызов t.Parallel() помечает сабтест параллельным: он приостановится, пока не выполнятся все непараллельные тесты, затем параллельные пойдут одновременно (пул по GOMAXPROCS). Ускоряет интеграционные тесты с ожиданиями. Осторожность трижды: общие ресурсы (база, файлы, порты) ломаются при параллельности; переменные цикла до Go 1.22 нужно копировать; и захваченные в замыкание «внешние» переменные дают классическую гонку — детектор -race это ловит, поэтому параллельные тесты гоняют именно с ним.
6. Как тестировать HTTP-хендлер без поднятого сервера?
Пакетом net/http/httptest: httptest.NewRequest(method, target, body) создаёт запрос, httptest.NewRecorder() — «ответ-протокол», хендлер вызывается напрямую http.HandlerFunc(h).ServeHTTP(rr, req), результат читается rr.Code и rr.Body. Это быстрее и детерминированнее настоящего сервера; поднимаемый httptest.NewServer нужен, когда тестируется клиент или middleware с реальной сетью. Вопрос-развитие: как проверить, что хендлер вернул нужный JSON — декодировать rr.Body в структуру и сравнить assert'ом, а не строкой.
rr := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/users/42", nil) Handler(w).ServeHTTP(rr, req) require.Equal(t, http.StatusOK, rr.Code)
7. Что такое TestMain и покрытие? Чем опасен гон за процентом?
TestMain(m *testing.M) — точка входа всего тестового бинарника: поднять общую тестовую базу, прогнать m.Run(), подчистить. Покрытие — go test -cover (процент инструкций, выполненных тестами). Опасность: 100% покрытия ≠ отсутствие багов — метрика отвечает «какой код исполнялся», а не «правильно ли проверен результат». Тесты без ассертов поднимают покрытие до единицы и не ловят ничего. Здоровая цель — покрытие критичной бизнес-логики близко к полному, инфраструктурного клея — по остаточному принципу.
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Testify: тестирование на Go с assert, require и mock — Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- JWT-аутентификация в Go: полный курс по токенам — Разбираем JWT в Go: устройство токена, golang-jwt v5, подписи HMAC и RSA, refresh-токены, bcrypt, middleware авторизации и лучшие практики.