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

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

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

Junior ~18 мин 7 вопросов

Тестирование на собеседовании Go: table-driven, моки, httptest

Тесты на собеседовании спрашивают дважды: «как вы тестируете» (культура) и «напишите тест» (навык руками). Ниже — вопросы с ответами, покрывающие стандартный набор: стиль table-driven, границы стандартной библиотеки и testify, моки без магии, httptest для хендлеров. Практика на реальных уроках — в курсе testify, тесты auth-кода разобраны в jwt-go.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

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% покрытия ≠ отсутствие багов — метрика отвечает «какой код исполнялся», а не «правильно ли проверен результат». Тесты без ассертов поднимают покрытие до единицы и не ловят ничего. Здоровая цель — покрытие критичной бизнес-логики близко к полному, инфраструктурного клея — по остаточному принципу.

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: