> Как написать юнит-тест для функции, которая читает данные из базы и считает среднее? (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Black Wall Group (BWG)

Стек: Go

> Пример ответа

Короткий ответ

Юнит-тест для функции, читающей из базы и считающей среднее, следует писать через интерфейс хранилища и мок-объект, а не через реальную БД. Основная цель - изолировать логику вычисления среднего от конкретной реализации доступа к данным. В Go это делается через интерфейс, который функция принимает как зависимость, и mock-реализацию в тесте. Такой подход обеспечивает детерминированность, скорость и независимость от окружения.

Подробное объяснение

Ключевая проблема: функция, которая одновременно читает из БД и считает среднее, нарушает single responsibility. Правильный подход - разделить на две части: получение данных (через интерфейс) и вычисление среднего (чистая функция). Тогда тестировать нужно обе части отдельно.

Для тестирования функции, которая всё же объединяет эти операции, используется dependency injection. Функция принимает интерфейс, например DataReader, с методом GetValues(ctx context.Context) ([]float64, error). В тесте подставляется mock, возвращающий заранее известные данные. Это позволяет проверить:

  • корректность вычисления среднего на известных входных данных;
  • обработку пустого набора (деление на ноль);
  • обработку ошибки от хранилища;
  • передачу контекста и его отмену.

В Go для моков часто используют библиотеки (gomock, testify/mock) или пишут простые ручные моки. Для senior-уровня важно уметь писать моки вручную без внешних зависимостей - это показывает понимание интерфейсов.

На практике

На практике структура такая:

  1. Определяем интерфейс в пакете, где используется функция.
  2. Функция принимает интерфейс как параметр.
  3. В тесте создаём mock-структуру, реализующую интерфейс.
  4. Тестируем сценарии: нормальные данные, пустой список, ошибка чтения.
  5. Для проверки вызова с правильным контекстом можно использовать замыкание или запись аргументов в mock.

Важно: не использовать реальную БД в юнит-тестах - это интеграционный тест. Для юнит-теста достаточно мока. Если функция написана без интерфейса (жёстко привязана к конкретной БД), это сигнал к рефакторингу перед написанием теста.

Пример кода

GO
// reader.go
type DataReader interface {
GetValues(ctx context.Context) ([]float64, error)
}
func Average(ctx context.Context, r DataReader) (float64, error) {
values, err := r.GetValues(ctx)
if err != nil {
return 0, err
}
if len(values) == 0 {
return 0, nil
}
sum := 0.0
for _, v := range values {
sum += v
}
return sum / float64(len(values)), nil
}
// reader_test.go
type mockReader struct {
values []float64
err error
gotCtx context.Context
}
func (m *mockReader) GetValues(ctx context.Context) ([]float64, error) {
m.gotCtx = ctx
return m.values, m.err
}
func TestAverage(t *testing.T) {
t.Run("normal case", func(t *testing.T) {
r := &mockReader{values: []float64{2, 4, 6}}
ctx := context.Background()
avg, err := Average(ctx, r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if avg != 4.0 {
t.Errorf("expected 4.0, got %v", avg)
}
if r.gotCtx != ctx {
t.Error("context was not passed through")
}
})
t.Run("empty values", func(t *testing.T) {
r := &mockReader{values: []float64{}}
avg, err := Average(context.Background(), r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if avg != 0.0 {
t.Errorf("expected 0.0, got %v", avg)
}
})
t.Run("reader error", func(t *testing.T) {
expectedErr := errors.New("db down")
r := &mockReader{err: expectedErr}
_, err := Average(context.Background(), r)
if !errors.Is(err, expectedErr) {
t.Errorf("expected %v, got %v", expectedErr, err)
}
})
}

Как отвечать на собеседовании

Начните с того, что тестировать функцию с реальной БД - не юнит-тест. Скажите, что сначала нужно проверить, принимает ли функция зависимость через интерфейс. Если нет - предложите рефакторинг. Затем опишите структуру теста: mock, сценарии, проверка контекста. Упомяните edge cases: пустой результат, ошибка чтения, большое количество данных. Покажите, что понимаете разницу между юнит- и интеграционным тестированием. Если спросят про табличные тесты - приведите пример с t.Run и таблицей кейсов. Будьте готовы обсудить trade-off между ручными моками и библиотеками.

Что проверяет интервьюер

Интервьюер оценивает:

  • понимание принципов юнит-тестирования (изоляция, детерминированность);
  • умение проектировать код, тестируемый через интерфейсы;
  • знание идиоматичного Go (интерфейсы, обработка ошибок, контекст);
  • способность покрыть edge cases;
  • умение объяснить, почему мок лучше реальной БД в юнит-тесте;
  • понимание разницы между юнит- и интеграционным тестированием.

Типичные ошибки

  • Использование реальной БД в юнит-тесте (это интеграционный тест).
  • Тестирование только happy path, без пустых данных и ошибок.
  • Игнорирование проверки передачи контекста.
  • Мок с хардкодом внутри теста вместо переиспользуемой структуры.
  • Отсутствие проверки ошибок в тесте (например, не проверяют err).
  • Слишком сложный мок, который дублирует логику реального хранилища.
  • Написание теста для функции, которая не принимает интерфейс - в этом случае тест либо невозможен, либо требует рефакторинга, о чём нужно сказать сразу.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью