> Как написать юнит-тест для функции, которая читает данные из базы и считает среднее? (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-уровня важно уметь писать моки вручную без внешних зависимостей - это показывает понимание интерфейсов.
На практике
На практике структура такая:
- Определяем интерфейс в пакете, где используется функция.
- Функция принимает интерфейс как параметр.
- В тесте создаём mock-структуру, реализующую интерфейс.
- Тестируем сценарии: нормальные данные, пустой список, ошибка чтения.
- Для проверки вызова с правильным контекстом можно использовать замыкание или запись аргументов в mock.
Важно: не использовать реальную БД в юнит-тестах - это интеграционный тест. Для юнит-теста достаточно мока. Если функция написана без интерфейса (жёстко привязана к конкретной БД), это сигнал к рефакторингу перед написанием теста.
Пример кода
GO// reader.gotype 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.0for _, v := range values {sum += v}return sum / float64(len(values)), nil}// reader_test.gotype mockReader struct {values []float64err errorgotCtx context.Context}func (m *mockReader) GetValues(ctx context.Context) ([]float64, error) {m.gotCtx = ctxreturn 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). - Слишком сложный мок, который дублирует логику реального хранилища.
- Написание теста для функции, которая не принимает интерфейс - в этом случае тест либо невозможен, либо требует рефакторинга, о чём нужно сказать сразу.
> Похожие задачи по Go
Что происходит, если фича не успела быть сделана в спринт и как переносится задача
Почему важно валидировать входные данные
С какими паттернами проектирования приходилось работать в Go?
Готов ли ты писать код сейчас?
> Похожие задачи по backend
Что происходит, если фича не успела быть сделана в спринт и как переносится задача
Почему важно валидировать входные данные
С какими паттернами проектирования приходилось работать в Go?
Готов ли ты писать код сейчас?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью