Перейти к основному содержимому

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Ручной тестировщик IT One / Газпромбанк - Middle 200+ тыс.

· 64 мин. чтения

Сегодня мы разберем собеседование на позицию тестировщика в крупном банковском проекте, где кандидат демонстрирует уверенное владение ручным тестированием, участием в распиле монолита на микросервисы, подготовкой тест-кейсов под автоматизацию и практический опыт с API, нагрузочным тестированием и очередями. Беседа показывает, что он вдумчиво подходит к требованиям, умеет выстраивать процесс тестирования и коммуникацию в команде, но при этом испытывает затруднения в продвинутых SQL-задачах и местами излишне растекается в ответах, что важно учитывать при оценке его уровня.

Вопрос 1. Есть ли вопросы по описанию проекта, команды и условиям работы или можно переходить к обсуждению вашего опыта?

Таймкод: 00:05:29

Ответ собеседника: правильный. Кандидат подтверждает, что всё понятно, проявляет интерес к проекту и предлагает сразу перейти к обсуждению релевантного опыта.

Правильный ответ:

В данном случае "правильный" ответ — это не про знания технологий, а про поведение и зрелость кандидата.

Хорошая стратегия ответа:

  • кратко подтвердить, что общее понимание есть;
  • показать заинтересованность в продукте и домене;
  • плавно перейти к релевантному опыту, связывая его с задачами команды;
  • при необходимости оставить уточняющие вопросы на конец интервью или обозначить 1–2 действительно важных аспекта (без уводов в сторону бытовых условий).

Пример уместного ответа:

«Спасибо за описание, у меня в целом сложилось хорошее понимание. Проект выглядит интересным, особенно часть, связанная с [ключевой аспект: высоконагруженный backend, микросервисы, event-driven архитектура, интеграции и т.п.]. Мой опыт хорошо ложится на эти задачи: я занимался [кратко: разработкой высоконагруженных сервисов на Go, оптимизацией производительности, построением REST/gRPC API, работой с очередями, кэшем, распределёнными системами]. Давайте перейдём к обсуждению моего опыта, и по ходу или в конце я задам несколько уточняющих вопросов по архитектуре и процессам.»

Такой ответ показывает:

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

Вопрос 2. В каком направлении вы хотите профессионально развиваться?

Таймкод: 00:10:25

Ответ собеседника: правильный. Кандидат говорит, что ему нравится тестирование; рассматривает развитие в сторону лидирования в области качества и автоматизации, при этом больше склоняется к автоматизации из-за её глубины и потенциала роста.

Правильный ответ:

Сильный ответ на этот вопрос должен показать осознанную траекторию развития, связанную с задачами компании, технологическим стеком и реальной ценностью для продукта. В контексте backend/Go-стека логично обозначить фокус на:

  • углубление технической экспертизы;
  • владение архитектурой и экосистемой вокруг Go;
  • качество, надёжность и наблюдаемость сервисов;
  • способность системно влиять на команду и продукт.

Пример развёрнутого ответа:

  1. Техническая глубина в Go и backend-инженерии:

    • Углубление в:
      • конкурентность и параллелизм (goroutines, каналы, worker-пулы, sync-примитивы, context);
      • высоконагруженные и низколатентные сервисы;
      • проектирование стабильных и версионируемых API (REST/gRPC);
      • эффективную работу с памятью и профилирование.
    • Цель — уметь проектировать и реализовывать сервисы так, чтобы они были:
      • предсказуемы под нагрузкой,
      • легко сопровождаемы,
      • безопасны и расширяемы.

    Пример (фрагмент worker pool для обработки задач):

    type Task func()

    func worker(id int, tasks <-chan Task) {
    for t := range tasks {
    t()
    }
    }

    func RunWorkers(workerCount int, input []Task) {
    tasks := make(chan Task, len(input))

    for i := 0; i < workerCount; i++ {
    go worker(i, tasks)
    }

    for _, t := range input {
    tasks <- t
    }
    close(tasks)
    }

    Такой код можно развивать: контекст, graceful shutdown, backpressure, метрики — это уже выход на уровень продакшен-готовых решений.

  2. Архитектура и качество систем:

    • Развитие в сторону проектирования:
      • микросервисной архитектуры с чёткими контрактами;
      • устойчивых к сбоям систем (ретраи, idempotency, дедупликация, circuit breaker);
      • технологического ландшафта: очереди (Kafka/RabbitMQ/NATS), кэш (Redis), балансировка, конфигурация, секреты.
    • Фокус на:
      • code review как инструмент повышения качества;
      • внедрение best practices: clean architecture, SOLID-принципы в контексте Go, четкое разделение домена и инфраструктуры;
      • документирование решений и контрактов.
  3. Автоматизация качества и инженерные практики:

    • Стремление не просто "писать тесты", а строить систему, где качество встроено:
      • юнит-тесты, интеграционные, контрактные, нагрузочные;
      • тестируемая архитектура (DI, интерфейсы, изоляция сторонних интеграций).
    • Пример простого, но показательного теста в Go:
    func Sum(a, b int) int {
    return a + b
    }

    func TestSum(t *testing.T) {
    tests := []struct {
    name string
    a, b, wn int
    }{
    {"positive", 2, 3, 5},
    {"negative", -2, -3, -5},
    {"mixed", -2, 3, 1},
    }

    for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
    if got := Sum(tt.a, tt.b); got != tt.wn {
    t.Fatalf("want %d, got %d", tt.wn, got)
    }
    })
    }
    }
    • Важно: автоматизация не ради покрытия, а ради снижения риска, скорости поставки и уверенности в изменениях (CI/CD, quality gates, метрики дефектов).
  4. Наблюдаемость, надёжность и работа с данными:

    • Развитие в сторону:
      • логирования, трассировки (OpenTelemetry), метрик (Prometheus);
      • SLA/SLO/SLI, понимания отказоустойчивости и инцидент-менеджмента.
    • Знание реляционных БД и умение писать эффективные запросы:
      • индексы, планы выполнения, транзакции, блокировки.

    Пример SQL-запроса с акцентом на производительность:

    SELECT u.id,
    u.email,
    COUNT(o.id) AS orders_count
    FROM users u
    LEFT JOIN orders o
    ON o.user_id = u.id
    AND o.created_at >= NOW() - INTERVAL '30 days'
    WHERE u.status = 'active'
    GROUP BY u.id, u.email
    HAVING COUNT(o.id) > 0
    ORDER BY orders_count DESC
    LIMIT 50;

    Здесь важно уметь:

    • понять, какие индексы нужны (users(status), orders(user_id, created_at)),
    • оценить нагрузку и оптимизировать запрос.
  5. Влияние на команду и процессы:

    • Участие в:
      • формировании технических стандартов;
      • менторстве и ревью кода;
      • выборе инструментов и архитектурных подходов.
    • Цель — уметь не только делать самому, но и масштабировать практики качества и инженерной культуры на команду.

Хороший ответ соединяет всё это в одну логичную линию:

  • "Я хочу развиваться в сторону глубокой технической экспертизы в Go и построения надёжных, тестируемых, наблюдаемых backend-систем. Мне важно не только писать код, но и выстраивать архитектуру, автоматизировать качество, влиять на инженерные практики команды и обеспечивать предсказуемость и стабильность продукта под реальной нагрузкой."

Вопрос 3. Каковы ваши ожидания от нового места работы и какие условия для вас критичны?

Таймкод: 00:11:16

Ответ собеседника: правильный. Кандидат ценит понятные процессы и ожидания, нормальную организацию работы, здоровую атмосферу в команде; спокойно относится к рабочим задачам и нагрузке, не предъявляет завышенных требований.

Правильный ответ:

Сильный ответ на этот вопрос должен показать зрелость, адекватность ожиданий и ориентацию на долгосрочное сотрудничество и результат. Важно связать ожидания с эффективной работой и качеством продукта, а не с бытовыми мелочами.

Ключевые моменты, которые стоит обозначить:

  1. Прозрачность целей и ожидаемого результата

    • Ожидание:
      • четко сформулированных задач и зон ответственности;
      • понятных критериев успеха: что считается хорошо сделанной задачей или успешным релизом;
      • адекватного приоритезирования (не когда все задачи «горят» всегда).
    • Зачем:
      • это позволяет принимать самостоятельные решения, оптимизировать архитектуру и не тратить ресурсы на хаотичные переделки.
  2. Зрелые процессы разработки

    • Ожидание базового инженерного фундамента:
      • система контроля версий с понятным workflow (git-flow, trunk-based);
      • code review как реальный инструмент качества, а не формальность;
      • CI/CD для автоматической сборки, тестирования и деплоя;
      • наличие базовой автоматизации: линтеры, тесты, статический анализ.
    • Это снижает количество инцидентов, ускоряет поставку и позволяет сосредоточиться на сложных задачах, а не на ручной рутине.
  3. Техническая адекватность и возможность делать хорошо

    • Ожидание:
      • возможность влиять на архитектурные решения и технический долг;
      • готовность команды и компании обсуждать и внедрять best practices (наблюдаемость, логирование, тестирование, отказоустойчивость).
    • Критично:
      • отсутствие культуры “костыль ради костыля” и постоянного игнорирования последствий.
  4. Коммуникация и командная культура

    • Ожидание:
      • уважительная, конструктивная коммуникация без токсичности и личных наездов;
      • нормальная реакция на ошибки: разбор и улучшение процессов вместо поиска виноватых;
      • готовность делиться знаниями, помогать коллегам, проводить технические обсуждения.
    • Это важно, потому что сложные системы требуют сотрудничества, а не политических игр.
  5. Отношение к нагрузке и срочности

    • Адекватная позиция:
      • нормальное отношение к пиковой нагрузке, дедлайнам и инцидентам, если это исключения, а не образ жизни;
      • готовность включаться в сложные задачи и продакшн-инциденты, если за этим следует анализ причин и улучшение системы.
    • Критично:
      • отсутствие постоянного "вечного продакшен-пожара" и 24/7 хаоса без попыток системно исправить ситуацию.
  6. Возможности развития

    • Не как "приятный бонус", а как инструмент роста ценности для компании:
      • доступ к сложным техническим задачам;
      • возможность участвовать в архитектурных решениях;
      • обмен опытом внутри команды (митапы, ревью, парное программирование).

Пример уместного ответа:

«Для меня важно, чтобы в компании были понятные ожидания: зона ответственности, цели команды, критерии качества. Нравится работать там, где есть базовые инженерные практики: code review, CI/CD, тестирование, прозрачные процессы релизов. Очень ценю здоровую, откровенную, нетоксичную коммуникацию и готовность решать проблемы системно, а не искать виноватых.

К нагрузке и сложным задачам отношусь спокойно, это часть нормальной работы. Критично другое: чтобы была возможность делать вещи качественно, влиять на технические решения и развивать систему, а не постоянно поддерживать её в режиме аварии.»

Вопрос 4. Есть ли у вас личные ограничения или предпочтения по графику и режиму работы?

Таймкод: 00:12:37

Ответ собеседника: правильный. Кандидат показывает гибкость, готовность подстраиваться под ритм распределённой команды, уточняет часовой пояс и планы по переезду, ориентируясь на работу по московскому времени.

Правильный ответ:

На этот вопрос важно ответить честно, конкретно и при этом показать адекватность и готовность к командной работе. Здесь не требуется техническая глубина, но важно продемонстрировать:

  • предсказуемость;
  • уважение к процессам команды;
  • понимание особенностей распределённой работы (созвоны, митинги, релизы, инциденты).

Сильный пример ответа:

«Я достаточно гибок по графику и могу подстраиваться под команду, особенно если она распределённая. Комфортно работать по московскому времени или с небольшими сдвигами, участвовать в стояниях, планированиях и ключевых встречах в общем слоте. Для меня важно, чтобы был понятный базовый коровый интервал пересечения с командой (например, 11:00–17:00 по МСК), а вне этого я могу организовать свой день самостоятельно. Экстренные ситуации и релизы в разумных пределах для меня приемлемы, если это не превращается в постоянную практику.»

Ключевые моменты, которые хорошо подсвечивают зрелый подход:

  • готовность согласовать core-hours, чтобы не страдали коммуникации;
  • осознанное отношение к on-call/дежурствам (если есть) — можно уточнить, что это приемлемо при прозрачных правилах и компенсации;
  • отсутствие лишней категоричности, если нет реальных ограничений (дети, совмещения, жёсткий второй проект и т.п.).

Такой ответ показывает, что с человеком будет удобно работать в командном и продуктовом контексте, а не только техническом.

Вопрос 5. Проходите ли вы сейчас какие-либо курсы или обучение?

Таймкод: 00:13:59

Ответ собеседника: правильный. Кандидат говорит, что сейчас сам курсы не проходит, а обучает новых специалистов на текущем проекте.

Правильный ответ:

Это поведенческий вопрос, здесь важно показать:

  • непрерывное развитие;
  • умение учиться самостоятельно;
  • способность делиться знаниями и систематизировать опыт.

Сильный, зрелый ответ может выглядеть так:

  • Если формальных курсов нет:

    • «Сейчас не прохожу курсы в классическом формате, фокусируюсь на практическом развитии в рамках проекта и самообразовании. Постоянно поддерживаю актуальность навыков за счёт:
      • чтения технической документации и RFC;
      • отслеживания изменений в Go (release notes, proposal-ы);
      • изучения статей и докладов по архитектуре, concurrency, observability, работе с БД;
      • экспериментов с инструментами (профилировщики, линтеры, фреймворки для тестирования, CI/CD).»
  • Важный акцент — обучение других:

    • «Я также занимаюсь обучением коллег: помогаю онбордить новых разработчиков/тестировщиков, объясняю стандарты кода и подходы к тестированию, участвую в ревью. Обучение других заставляет глубже понимать материал, формализовать практики и улучшать внутренние процессы.»

Такой ответ показывает:

  • самостоятельность в обучении;
  • умение работать с первоисточниками, а не только с курсами;
  • готовность передавать знания — это напрямую влияет на качество команды и скорость роста продукта.

Вопрос 6. Каковы основные цели тестирования с вашей точки зрения?

Таймкод: 00:14:17

Ответ собеседника: правильный. Кандидат выделяет: проверить соответствие бизнес-требованиям и убедиться, что система соответствует ожиданиям пользователя, отмечая возможное расхождение между ними.

Правильный ответ:

Основные цели тестирования гораздо шире, чем просто поиск багов или проверка формального соответствия требованиям. В зрелом инженерном подходе тестирование — это инструмент управления рисками и доказательства того, что система делает то, что должна, в реальных условиях эксплуатации.

Ключевые цели:

  1. Подтверждение соответствия бизнес-целям и требованиям
  • Проверить, что реализованный функционал:
    • соответствует спецификациям, user stories, acceptance criteria;
    • корректно реализует бизнес-правила, расчеты, ограничения;
    • не искажает финансовые, юридические и операционные процессы.
  • Важно не только «passed/failed», а:
    • выявить неоднозначности требований;
    • подсветить расхождения между формальными требованиями и реальными сценариями бизнеса.
  1. Подтверждение ценности и ожидаемого поведения для пользователя
  • Даже если функционал формально соответствует ТЗ, он может быть:
    • неудобным, непредсказуемым, контринтуитивным;
    • технически корректным, но не решающим задачу пользователя.
  • Тестирование должно:
    • моделировать реальные сценарии использования;
    • выявлять UX-проблемы, пограничные кейсы, неожиданные потоки.
  1. Управление рисками и предотвращение регрессии
  • Одна из главных целей — уменьшить вероятность критичных отказов:
    • финансовые потери;
    • простои системы;
    • утечки данных;
    • нарушение регуляторных требований (GDPR, 152-ФЗ, PCI DSS и др.).
  • Тестирование должно быть риск-ориентированным:
    • больше глубины и сценариев там, где высокий impact;
    • разумный баланс покрытия — не максимальное количество тестов, а максимальный эффект на снижение рисков.
  • Регрессионное тестирование:
    • подтвердить, что новые изменения не сломали существующий функционал;
    • автоматизированные тесты на критические бизнес-потоки.
  1. Обеспечение надежности, производительности и устойчивости

Тестирование не ограничивается корректностью «при нормальном вводе»:

  • Производительность:
    • система выдерживает ожидаемую и пиковую нагрузку;
    • латентность и throughput в допустимых пределах;
    • нет деградаций после изменений.
  • Устойчивость:
    • корректное поведение при частичных отказах: падение сервисов, таймауты, проблемы с БД, очередями, внешними API;
    • механизмы ретраев, idempotency, circuit breaker ведут себя ожидаемо.
  • Это особенно критично для распределённых систем на Go.

Пример: высоконагруженный Go-сервис, обращающийся к БД.

Фрагмент кода:

func GetUser(ctx context.Context, db *sql.DB, id int64) (User, error) {
var u User
err := db.QueryRowContext(ctx,
`SELECT id, email, status FROM users WHERE id = $1`, id).
Scan(&u.ID, &u.Email, &u.Status)

if err == sql.ErrNoRows {
return User{}, ErrNotFound
}
if err != nil {
return User{}, fmt.Errorf("query user: %w", err)
}
return u, nil
}

Что важно протестировать:

  • корректный возврат пользователя при валидном ID;
  • корректное поведение при отсутствии пользователя (ErrNotFound);
  • поведение при ошибках БД (замок, таймаут, недоступность);
  • нагрузочные сценарии: как метод ведет себя при большом количестве конкурентных запросов;
  • соблюдение SLA по времени ответа.
  1. Повышение качества архитектуры и процессов разработки

Тестирование — сигнал не только о багах, но и о качестве архитектуры:

  • Если модуль трудно тестировать:
    • слишком много связей, нет интерфейсов, логика размазана — это индикатор плохого дизайна.
  • Хорошая цель тестирования:
    • стимулировать модульность, явные контракты, предсказуемое поведение;
    • встроить качество в архитектуру, а не пытаться «натестировать» поверх.

Пример: использование интерфейсов для тестируемости:

type UserStore interface {
GetByID(ctx context.Context, id int64) (User, error)
}

type UserService struct {
store UserStore
}

func (s *UserService) IsActive(ctx context.Context, id int64) (bool, error) {
u, err := s.store.GetByID(ctx, id)
if err != nil {
return false, err
}
return u.Status == "active", nil
}

Тест:

type stubUserStore struct {
user User
err error
}

func (s stubUserStore) GetByID(ctx context.Context, id int64) (User, error) {
return s.user, s.err
}

func TestUserService_IsActive(t *testing.T) {
svc := UserService{
store: stubUserStore{user: User{Status: "active"}},
}

active, err := svc.IsActive(context.Background(), 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !active {
t.Fatalf("expected active user")
}
}

Цель тестирования здесь — не просто проверить условие, а зафиксировать контракт поведения и упростить эволюцию кода.

  1. Документирование и создание доверия
  • Тесты выполняют роль "живой документации":
    • показывают как использовать API/модуль;
    • фиксируют принятые допущения и граничные условия.
  • Наличие хороших тестов:
    • формирует доверие к системе;
    • позволяет быстрее вносить изменения, не боясь поломать критичный функционал.

Сводя воедино:

Основные цели тестирования:

  • снизить риски для бизнеса и пользователей;
  • подтвердить соответствие требованиям и реальным сценариям использования;
  • обеспечить надежность, производительность и предсказуемость;
  • повысить качество архитектуры и процессов разработки;
  • создать основу для безопасных и быстрых изменений.

Такой подход показывает не «тестирование как чек-лист», а тестирование как часть инженерной ответственности за поведение системы в продакшене.

Вопрос 7. В чём разница между функциональным и нефункциональным тестированием?

Таймкод: 00:14:58

Ответ собеседника: правильный. Кандидат говорит, что функциональное тестирование проверяет соответствие функционала требованиям, а нефункциональное — характеристики системы: безопасность, производительность, нагрузка, UX/UI, доступность и другие аспекты.

Правильный ответ:

Формулировка верная, но для полноты важно показать более структурированное понимание, связанное с реальными системами и архитектурой.

Функциональное и нефункциональное тестирование отвечают на разные классы вопросов:

  • Функциональное: «Система делает то, что должна делать?»
  • Нефункциональное: «Насколько хорошо, быстро, безопасно и надёжно система это делает в реальных условиях?»

Разберём по сути и с практическими примерами.

Функциональное тестирование

Цель: проверить, что система корректно реализует заявленный бизнес-функционал и требования.

Основные характеристики:

  • Основывается на:
    • спецификациях,
    • user stories,
    • acceptance criteria,
    • бизнес-правилах.
  • Проверяет:
    • корректность ответов API;
    • целостность и правильность данных;
    • бизнес-валидации;
    • сценарии успеха и отказа.

Примеры для backend/Go:

  1. Тест API-метода создания пользователя:
func TestCreateUser_Success(t *testing.T) {
// given
req := CreateUserRequest{
Email: "user@example.com",
}

// when
resp, err := testClient.CreateUser(context.Background(), req)

// then
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.ID == 0 {
t.Fatalf("expected non-zero user ID")
}
}
  1. Тест бизнес-правила (например, нельзя создать пользователя с уже существующим email):
func TestCreateUser_DuplicateEmail(t *testing.T) {
// подготовка тестовых данных и первый пользователь уже есть

req := CreateUserRequest{
Email: "user@example.com",
}

_, err := testClient.CreateUser(context.Background(), req)
if err == nil {
t.Fatalf("expected error on duplicate email")
}
}

Суть: нас интересует "что именно происходит" — правильные статусы, корректные ответы, верная логика.

Нефункциональное тестирование

Цель: проверить характеристики качества системы. Даже идеально реализованный функционал бесполезен, если система:

  • не выдерживает нагрузку,
  • небезопасна,
  • нестабильна под отказами,
  • неудобна и непредсказуема.

Типичные виды нефункционального тестирования:

  1. Производительность и нагрузка (Performance/Load/Stress)

    • Вопросы:
      • выдержит ли система 1000/10 000 RPS?
      • не растет ли латентность до секунд при пиковой нагрузке?
    • Для Go-сервисов важно:
      • профилировать (pprof),
      • оптимизировать аллокации, пулы соединений, работу с БД,
      • проверять поведение при конкурентном доступе.

    SQL-пример (типичный hotspot-запрос):

    SELECT id, email, status
    FROM users
    WHERE status = 'active'
    ORDER BY created_at DESC
    LIMIT 100;
    • НФ-тесты здесь проверяют:
      • есть ли индекс по (status, created_at),
      • как меняется время выполнения при росте таблицы до миллионов записей.
  2. Надёжность, устойчивость, отказоустойчивость

    • Проверка поведения при:
      • падении одной из служб;
      • недоступности внешнего API;
      • временных сетевых проблемах.
    • В Go:
      • корректная работа с context (timeouts, cancellation),
      • retry-механизмы,
      • circuit breaker, idempotency.

    Пример корректной обработки таймаута:

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    if err := service.Process(ctx); err != nil {
    // ожидание: при превышении таймаута будет ошибка контекста
    }
  3. Безопасность

    • Проверка:
      • контроля доступа (role-based, permissions),
      • правильной обработки токенов и сессий,
      • шифрования чувствительных данных,
      • защиты от типичных атак (SQL injection, XSS, CSRF, brute-force, insecure direct object reference).
    • Примеры:
      • запросы только от авторизованных пользователей;
      • отсутствие прямой передачи паролей/секретов в логах.
  4. Юзабилити, UX, доступность

    • Для пользовательских интерфейсов:
      • насколько интерфейс понятен и предсказуем;
      • соответствие ожиданиям домена и целевой аудитории;
      • доступность для людей с ограниченными возможностями.
    • В backend-контексте:
      • консистентные коды ответов;
      • предсказуемая схема API;
      • хорошие сообщения об ошибках.
  5. Масштабируемость

    • Как система ведёт себя при росте:
      • пользователей,
      • данных,
      • числа интеграций.
    • Вопрос: можем ли горизонтально масштабировать Go-сервисы, БД, кэши, очереди без архитектурного переписывания.
  6. Поддерживаемость и операбельность

    • Логирование, метрики, трассировка:
      • удобно ли диагностировать проблемы;
      • есть ли понятные алерты, дашборды.
    • Это тоже нефункциональное требование: не влияет напрямую на бизнес-функции, но критично для эксплуатации.

Ключевое различие

  • Функциональное тестирование:
    • отвечает на вопрос: «Система правильно делает нужные операции?»
    • ориентируется на «что должно происходить» согласно требованиям.
  • Нефункциональное тестирование:
    • отвечает на вопрос: «Насколько качественно система это делает в реальных и граничных условиях?»
    • покрывает время отклика, устойчивость, безопасность, масштабируемость, удобство, наблюдаемость.

Важно понимать: обе группы одинаково критичны. Система, которая:

  • идеально реализует бизнес-логику, но падает при нагрузке — провал;
  • быстрый и красивый сервис, но с неправильной логикой списания денег — тоже провал.

Зрелый инженер рассматривает функциональные и нефункциональные требования как части единого контракта качества системы.

Вопрос 8. С какими видами нефункционального тестирования вы работали на практике?

Таймкод: 00:16:32

Ответ собеседника: неполный. Кандидат упоминает нагрузочное тестирование (анализ результатов, поиск узких мест во frontend и БД) и проверку прав доступа и ролей через токены, включая ошибки в разграничении доступа.

Правильный ответ:

Хороший ответ на этот вопрос должен:

  • чётко перечислять виды нефункционального тестирования;
  • опираться на реальные практики: что именно проверяли, какими инструментами, как интерпретировали результаты;
  • показывать понимание связки: профиль нагрузки → архитектура → БД → кэш → сеть → ограничения языка/рантайма (в т.ч. Go);
  • демонстрировать умение влиять на улучшение системы, а не просто запускать тесты.

Ниже вариант ответа, который показал бы глубокую экспертизу.

Основные виды нефункционального тестирования, с которыми разумно иметь опыт:

  1. Нагрузочное и стресс-тестирование (Performance / Load / Stress)

Цель: проверить поведение системы под ожидаемой и повышенной нагрузкой, выявить узкие места.

Что важно показать:

  • Умение моделировать realistic workload:
    • распределение RPS по методам;
    • частота типичных операций (login, search, write-heavy, read-heavy);
    • сценарии с пиками нагрузки.
  • Анализ результатов:
    • латентность (p50/p95/p99),
    • пропускная способность (throughput),
    • ошибки (5xx, timeouts),
    • поведение под стрессом (когда нагрузка > плановой).

Пример подхода (на уровне архитектуры и Go):

  • Тестируем сервис на Go, который читает/пишет в PostgreSQL.
  • В процессе нагрузочного тестирования видим рост латентности при увеличении числа соединений.
  • Действия:
    • анализируем:
      • настройки пулов соединений (max_open_conns, max_idle_conns),
      • планы выполнения SQL-запросов (EXPLAIN ANALYZE),
      • индексы,
      • блокировки и конкуренцию за ресурсы;
    • оптимизируем запросы, добавляем индексы, настраиваем пулы, кэшируем горячие данные.

SQL-пример с акцентом на оптимизацию:

EXPLAIN ANALYZE
SELECT id, email, status
FROM users
WHERE status = 'active'
ORDER BY created_at DESC
LIMIT 100;
  • Если видим Seq Scan на миллионы строк, добавляем индекс:
CREATE INDEX idx_users_status_created_at
ON users (status, created_at DESC);

И в Go-коде:

db.SetMaxOpenConns(50)
db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute)
  1. Тестирование устойчивости и отказоустойчивости (Reliability / Resilience)

Цель: проверить, как система ведёт себя при сбоях:

  • падение зависимых сервисов;
  • недоступность БД;
  • сетевые таймауты;
  • частичные сбои.

Практика:

  • симуляция отказов:
    • отключение части инстансов,
    • увеличение латентности,
    • искусственные ошибки на стороне внешнего API;
  • проверка:
    • корректной обработки ошибок,
    • ретраев с backoff,
    • idempotency,
    • отсутствие «смертельных» блокировок.

Пример на Go с корректной работой через context:

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

res, err := client.DoRequest(ctx, payload)
if errors.Is(err, context.DeadlineExceeded) {
// ожидаемое поведение: логируем, считаем метрику, пробуем ретрай, если бизнес-логика позволяет
}
  1. Тестирование безопасности (Security)

Цель: убедиться, что система защищена от типичных угроз и корректно реализует контроль доступа.

Практика, которую уместно упомянуть:

  • проверка аутентификации и авторизации:
    • работа с JWT/Access токенами,
    • корректность проверки ролей и прав;
  • попытки обхода ограничений:
    • доступ по чужим ID (Insecure Direct Object Reference),
    • вызов админских методов с обычной ролью;
  • защита от инъекций:
    • использование параметризованных запросов в SQL;
  • безопасная обработка ошибок:
    • отсутствие утечки чувствительной информации через сообщения об ошибках.

Go + SQL пример безопасного доступа:

func GetOrder(ctx context.Context, db *sql.DB, orderID, userID int64) (Order, error) {
var o Order
err := db.QueryRowContext(ctx, `
SELECT id, user_id, amount
FROM orders
WHERE id = $1 AND user_id = $2
`, orderID, userID).Scan(&o.ID, &o.UserID, &o.Amount)

if err == sql.ErrNoRows {
return Order{}, ErrNotFound
}
if err != nil {
return Order{}, fmt.Errorf("get order: %w", err)
}
return o, nil
}

Здесь тесты проверки безопасности включают:

  • пользователь не может получить чужой заказ;
  • админ может, если предусмотрена соответствующая роль;
  • прямой перебор ID не раскрывает чужие данные.
  1. Тестирование производительности отдельных компонент (Profiling / Micro-benchmarks)

Цель: локализовать горячие точки и неэффективный код.

Для Go:

  • использование:
    • pprof (CPU, memory, allocs),
    • testing.B для бенчмарков;
  • примеры:
    • сравнение разных алгоритмов парсинга/сериализации;
    • поиск лишних аллокаций или блокировок.

Пример бенчмарка:

func BenchmarkProcess(b *testing.B) {
for i := 0; i < b.N; i++ {
Process(data)
}
}
  1. Тестирование наблюдаемости и операбельности (Observability)

Цель: проверить, насколько система пригодна к эксплуатации:

  • есть ли:
    • структурированные логи,
    • метрики (Prometheus),
    • трассировки (OpenTelemetry),
    • полезные алерты;
  • можно ли:
    • быстро диагностировать инцидент,
    • отследить цепочку вызовов между сервисами,
    • оценить влияние изменений.

Практика:

  • проверка полноты логирования ключевых операций;
  • тесты, подтверждающие наличие метрик по:
    • латентности,
    • ошибкам,
    • количеству запросов;
  • в нагрузочных и отказоустойчивых тестах анализируются не только ответы, но и метрики/логи.
  1. Юзабилити и доступность (если релевантно)

Для backend-ориентированного ответа можно ограничиться:

  • предсказуемость API:
    • консистентные коды ошибок;
    • понятные сообщения (machine-friendly + человекочитаемые);
  • стабильность контрактов (backward compatibility), версия API.

Как собрать сильный ответ:

«На практике я работал с несколькими видами нефункционального тестирования.

Во-первых, нагрузочное и стресс-тестирование: моделировал реальные сценарии нагрузки, анализировал p95/p99, выявлял узкие места в базе данных и конфигурации пулов соединений, помогал оптимизировать запросы и структуру индексов.

Во-вторых, тестирование безопасности: проверял корректность разграничения доступа на уровне ролей и токенов, сценарии доступа к данным других пользователей, использование параметризованных запросов, отсутствие утечек технических деталей в ошибках.

Кроме того, участвовал в проверке устойчивости системы: симуляция недоступности внешних сервисов и БД, проверка корректной обработки таймаутов и ретраев, мониторинг поведения под частичными отказами.

Отдельно обращал внимание на наблюдаемость: проверка, что ключевые операции покрыты логами и метриками, чтобы по результатам нагрузочных и инцидентных сценариев можно было быстро локализовать проблему.

Фокус всегда на том, чтобы не просто “прогнать тесты”, а получить осмысленные сигналы о рисках для производительности, безопасности и надежности системы в продакшене.»

Вопрос 9. Какие артефакты тестирования вы используете и как у вас организован процесс документирования на проекте?

Таймкод: 00:18:19

Ответ собеседника: неполный. Кандидат описывает участие на этапе ревью требований: проверяет документацию на противоречия и ошибки, добивается уточнений, участвует в груминге перед разработкой, но не раскрывает полноту артефактов и системность процесса документирования.

Правильный ответ:

Сильный ответ должен показать, что тестирование интегрировано в жизненный цикл разработки, а артефакты не живут "отдельно", а служат:

  • управлению рисками,
  • прозрачности ожиданий,
  • воспроизводимости тестирования,
  • ускорению онбординга,
  • поддержке долговременной эволюции системы.

Ниже структурированный ответ с примерами артефактов и организации документации.

Основные артефакты тестирования

  1. Анализ и спецификации требований

Это фундамент. До написания тестов нужно стабилизировать то, что считаем "истиной":

  • Product Requirements / BRD / User Stories
  • Use Cases, диаграммы последовательностей, модели данных
  • Acceptance Criteria

Роль тестирования:

  • выявление противоречий, пробелов, копипасты, неявных правил;
  • формализация граничных случаев и негативных сценариев;
  • предложение более точных формулировок, которые можно проверить.

Результат:

  • согласованный baseline, на основе которого строятся сценарии, автотесты и контракты.
  1. Test Strategy / Test Plan

Высокоуровневый документ, который отвечает на вопросы:

  • что и как мы тестируем;
  • какие уровни тестирования используются:
    • unit, integration, API, E2E, нагрузочные, security, regression;
  • какие риски покрываем в первую очередь;
  • какая часть покрывается автотестами, какая — ручными сценариями;
  • инструменты (Go test, Postman/Newman, k6/JMeter, Cypress/Playwright, Docker Compose для интеграций, и т.п.);
  • критерии готовности (Definition of Done, вход/выход из тестирования).

Это не должен быть "роман на 50 страниц", достаточно компактного, актуального документа (Notion/Confluence/Markdown в репо), который реально используется.

  1. Test Cases / Checklists / Scenarios
  • Детализированные тест-кейсы уместны:
    • для критичных, регуляторных, финансовых, юридически значимых сценариев.
  • Чек-листы и сценарии:
    • для регрессии,
    • для smoke-тестов,
    • для быстрого прогона основных потоков.

Хорошая практика — привязка к требованиям/историям:

  • TС-123 связан с US-45 и API-эндпоинтом /v1/payments.
  • Позволяет видеть покрытие: какие требования формально проверены.

Пример компактного чек-листа для API (Markdown в репозитории):

  • POST /users:
    • Успешное создание с валидными данными
    • Дубликат email → 409 / business error
    • Невалидный email → 400
    • Нет обязательного поля → 400
    • Авторизация обязательна → 401

Такой чек-лист легко конвертируется в автотесты.

  1. Автоматизированные тесты как ключевой артефакт

Код автотестов — один из самых ценных и честных артефактов. В современных проектах он часто важнее формальных документов.

Типы:

  • Unit-тесты (Go testing):

    • проверяют бизнес-логику, мелкие функции, расчёты.
  • Интеграционные тесты:

    • Go + реальные/тестовые компоненты (PostgreSQL, Redis, Kafka);
    • часто с Docker Compose.
  • Контрактные тесты:

    • проверяют соответствие API/событий спецификациям (OpenAPI, Protobuf).
  • E2E/системные тесты:

    • полноразмерные пользовательские сценарии.

Пример unit-теста в Go (артефакт, фиксирующий бизнес-правило):

func CalculateDiscount(amount float64) float64 {
if amount >= 1000 {
return amount * 0.9
}
return amount
}

func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
input float64
want float64
}{
{"no discount", 500, 500},
{"with discount", 1000, 900},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := CalculateDiscount(tt.input)
if got != tt.want {
t.Fatalf("want %.2f, got %.2f", tt.want, got)
}
})
}
}

Этот тест:

  • документирует правило скидки;
  • защищает от регрессий при изменении логики.
  1. API спецификации и контрактная документация

Критичный артефакт для backend-экосистем:

  • OpenAPI/Swagger для REST;
  • Protobuf для gRPC;
  • схемы событий для event-driven систем (Kafka/NATS).

Тестирование использует их как:

  • источник истины для генерации автотестов;
  • базу для контрактного тестирования (consumer-driven, backward compatibility).

Пример использования: при изменении API:

  • автотесты валидируют, что старые контракты не сломаны;
  • документация обновляется вместе с кодом (генерация из исходников).
  1. Bug Reports / Defect Reports

Это не просто "тикеты с багом", а:

  • фиксация проблемы:
    • шаги воспроизведения,
    • ожидаемый/фактический результат,
    • окружение,
    • логи/метрики/скриншоты;
  • указание области риска;
  • основа для регрессионных тестов.

Зрелый подход:

  • по критичным багам:
    • добавляем автотест или чек-лист;
    • фиксируем причину (root cause) и меры предотвращения.
  1. Regression Suites и Smoke/Сanary-наборы

Отдельные артефакты:

  • список критических сценариев, которые должны быть проверены:
    • перед релизом;
    • после деплоя;
    • после изменения ключевых модулей.
  • часть — в виде автотестов;
  • часть — минимальный manual smoke, если автотесты не покрывают.
  1. Документация по средам, данным и тестовой инфраструктуре

Важно зафиксировать:

  • какие стенды есть (dev/test/stage/preprod/prod);
  • как поднимать локальное окружение:
    • docker-compose, k8s-манифесты;
  • какие тестовые данные используются:
    • анонимизированные,
    • фабрики/сидеры для автотестов.

Это уменьшает хаос и делает тесты воспроизводимыми.

  1. Организация документирования

Как это всё системно организовать:

  • Единое место хранения:

    • Confluence/Notion/Wiki для описаний процессов и стратегий;
    • Git-репозиторий для технических артефактов:
      • тест-кейсы в Markdown,
      • OpenAPI/Protobuf,
      • автотесты,
      • docker-compose для интеграционных тестов.
  • Связность:

    • требования → тест-кейсы → автотесты → отчёты CI;
    • тикеты в трекере (Jira/YouTrack) связаны с тестами и багами.
  • Обновляемость:

    • правила: при изменении фичи обновляем:
      • требования,
      • тест-кейсы/чек-листы,
      • автотесты,
      • API-спеки;
    • code review распространяется и на тесты, и на документацию.

Пример сильного ответа вживую:

«Мы используем несколько уровней артефактов. На входе — анализ требований: на этапе груминга и ревью спецификаций я фиксирую вопросы, противоречия и граничные случаи, пока это дешево изменить. Далее формируем живой Test Strategy и набор чек-листов/тест-кейсов для ключевых бизнес-потоков и рисковых зон.

Основной рабочий артефакт — автотесты: unit и интеграционные на Go, API-тесты, регрессионный набор, который гоняется в CI. Для контрактов поддерживаем OpenAPI/Protobuf, что позволяет валидировать совместимость.

Баги документируем так, чтобы по критичным инцидентам всегда появлялся либо тест, либо обновлённый сценарий. Вся документация хранится рядом с кодом и в общей wiki, чтобы её было легко поддерживать и связывать с задачами. Цель — не бумага ради бумаги, а минимальный набор артефактов, который даёт прозрачность, покрытие рисков и ускоряет разработку и онбординг.»

Вопрос 9. Какие артефакты тестирования вы используете и как организован процесс документирования на проекте?

Таймкод: 00:18:19

Ответ собеседника: правильный. Кандидат описывает полный цикл: ревью и уточнение требований, написание тест-кейсов как основы для автоматизации, чек-листы для временных задач, регрессионные наборы как тест-планы, impact analysis, согласование с разработчиками и соседними командами, фиксирование результатов в системе управления тестами.

Правильный ответ:

Ниже — расширенный, структурированный ответ, отражающий зрелый процесс и практики, применимые в сложных продуктах и распределённых системах.

Точка зрения: артефакты тестирования — это не «бумага ради процесса», а системный способ:

  • связать требования, риски и реализацию;
  • сделать качество воспроизводимым;
  • ускорить изменения, а не тормозить их.

Основные артефакты и как они встраиваются в цикл разработки

  1. Анализ требований и спецификаций (входной артефакт)

Что фиксируем:

  • уточнённые требования (user stories, acceptance criteria);
  • выявленные противоречия и решения по ним;
  • граничные случаи и бизнес-ограничения;
  • зависимости между сервисами и командами.

Практика:

  • участвовать в грумингах и технических обсуждениях;
  • оставлять комментарии к требованиям (Confluence/Notion/Jira) до начала разработки;
  • формализовать критичные моменты так, чтобы по ним можно было строить тесты и автотесты.

Результат:

  • требования становятся тестируемыми, понятными для dev/QA/analyst;
  • меньше «серых зон» и расхождений интерпретаций.
  1. Test Cases как основа для автоматизации

Тест-кейсы — не формальность, а промежуточный слой между требованиями и автотестами.

Характеристики сильных тест-кейсов:

  • ориентированы на бизнес-сценарии и риски, а не на микроклики;
  • покрывают:
    • позитивные сценарии,
    • негативные сценарии,
    • граничные значения,
    • критичные интеграции;
  • однозначно трактуются: по ним легко писать автотест и воспроизвести ручную проверку.

Формат (упрощённый пример для API, в Markdown или системе тест-менеджмента):

  • Scenario: Создание пользователя с валидными данными

    • Предусловия: нет пользователя с таким email
    • Шаги: POST /users с email=user@example.com
    • Ожидаемо: 201, валидный ID, запись в БД
  • Scenario: Попытка создать пользователя с существующим email

    • Ожидаемо: 409 или бизнес-ошибка, пользователь не дублируется.

Далее эти сценарии напрямую конвертируются в автотесты на Go.

Пример автотеста по тест-кейсу:

func TestCreateUser_DuplicateEmail(t *testing.T) {
ctx := context.Background()

email := "user@example.com"

// первый вызов — успешное создание
_, err := client.CreateUser(ctx, CreateUserRequest{Email: email})
if err != nil {
t.Fatalf("unexpected error on first create: %v", err)
}

// второй вызов — ожидаем контролируемую ошибку
_, err = client.CreateUser(ctx, CreateUserRequest{Email: email})
if err == nil {
t.Fatalf("expected error on duplicate email, got nil")
}
}
  1. Чек-листы

Используются:

  • для быстрых, временных или одноразовых проверок;
  • для smoke-тестов, sanity-проверок после деплоя;
  • для областей, где формальный детальный test case избыточен.

Чек-листы храним:

  • рядом с кодом (Markdown в репо);
  • либо в Wiki/системе тест-менеджмента, привязанной к релизам.

Плюсы:

  • легко поддерживать;
  • быстро прогонять;
  • удобно использовать как основу для регрессионных наборов.
  1. Регрессионные наборы и тест-планы

Регрессионный набор — это структурированный набор сценариев, покрывающий:

  • критические бизнес-потоки;
  • интеграции между сервисами;
  • ранее найденные дефекты (которые не должны повториться);
  • высокорисковые зоны (платежи, авторизация, расчёты, отчеты и т.д.).

Организация:

  • часть сценариев реализована как автотесты (unit, integration, API, E2E);
  • часть — как чек-листы для ручного прогона в специфичных кейсах;
  • тест-план релиза: какой поднабор регрессии гоняем в зависимости от изменений (risk-based regression).
  1. Impact analysis (анализ влияния изменений)

Ключевой элемент зрелого процесса:

  • при каждой фиче/изменении определяем:
    • какие модули, сервисы, API, таблицы БД затронуты;
    • какие зависящие компоненты и команды должны быть уведомлены;
    • какие тесты нужно:
      • обновить;
      • добавить;
      • прогнать из регрессии.

Результат:

  • нет избыточной полной регрессии «на всякий случай»;
  • нет слепых зон, потому что связь "изменение → риск → тесты" явная.
  1. Автоматизированные тесты как центральный артефакт

Автотесты — часть кода продукта, живут в репозитории, проходят code review.

Уровни:

  • Unit-тесты:
    • проверяют бизнес-логику, чистые функции, алгоритмы;
  • Интеграционные тесты:
    • реальная БД (через docker-compose), очереди, внешние сервисы в тестовом окружении;
  • Контрактные тесты:
    • проверка соответствия OpenAPI/Protobuf;
    • backward compatibility;
  • E2E/API-тесты:
    • ключевые пользовательские сценарии сквозь несколько сервисов.

Интеграционный пример с БД и Go:

func TestGetUser_Integration(t *testing.T) {
db := testDB() // поднятое тестовое окружение
seedUser := User{Email: "test@example.com", Status: "active"}
id := insertUser(t, db, seedUser)

got, err := GetUser(context.Background(), db, id)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if got.Email != seedUser.Email || got.Status != seedUser.Status {
t.Fatalf("unexpected user: %#v", got)
}
}

Эти тесты:

  • документируют фактическое поведение;
  • защищают от регрессий;
  • встраиваются в CI/CD для каждого коммита.
  1. Документация API и контрактов

Артефакты:

  • OpenAPI/Swagger спеки;
  • Protobuf-схемы;
  • схемы событий для Kafka/NATS.

Сильная практика:

  • спек лежит в репозитории;
  • изменения проходят через ревью;
  • по спекам:
    • генерируется клиентский код;
    • строятся автотесты;
    • валидируется совместимость между версиями.
  1. Отчеты, статус и трассируемость

Важно не просто "прогнать тесты", а:

  • иметь трассировку:
    • Требование → Тест-кейс → Автотест → Результат;
  • видеть покрытие критичных фич;
  • фиксировать результаты в системе:
    • TestRail, Zephyr, qTest, Xray или свои решения;
    • интеграция с CI: результаты автотестов подтягиваются автоматически.

По критичным багам:

  • заводим дефекты с полным описанием;
  • связываем с требованиями;
  • добавляем тесты, чтобы ошибка не повторилась.
  1. Хранение, обновление и "живость" документации

Ключевые принципы:

  • Единый источник правды:
    • технические артефакты (тесты, API спеки, env-конфиги) — в Git;
    • процессные и обзорные документы — в Wiki, но с ссылками на репозитории.
  • Обновление по change-flow:
    • любая существенная фича → обновление:
      • требований,
      • test cases/чек-листов,
      • автотестов,
      • регрессионных наборов;
  • Code review распространяется и на тесты, и на спеки.

Пример сильного резюме-ответа:

«Мы выстраиваем полный цикл: на старте ревьюим требования и фиксируем вопросы так, чтобы они были тестируемыми. На основе этого формируем тест-кейсы для ключевых сценариев — это наш основной артефакт, по которому потом строится автоматизация. Для оперативных и одноразовых проверок используем чек-листы.

Под регрессию поддерживаем отдельные наборы сценариев и регулярно их актуализируем на основе impact analysis: какие области затронуты изменениями, какие тесты нужно прогнать или обновить. Автотесты (unit, интеграционные, API, E2E) живут в репо рядом с кодом, проходят code review и автоматически гоняются в CI, результаты попадают в систему управления тестированием.

Документацию по API, контрактам и тестам держим синхронизированной с кодом. Цель — минимальный, но достаточный набор артефактов, который даёт прозрачность, трассируемость и уверенность в качестве без лишней бюрократии.»

Вопрос 10. Какие техники тест-дизайна вы чаще всего применяете в своей работе?

Таймкод: 00:27:40

Ответ собеседника: правильный. Кандидат называет классы эквивалентности, анализ граничных значений, диаграммы состояний и переходов, таблицы принятия решений и отмечает использование аналитических схем как основы для тест-кейсов.

Правильный ответ:

Ответ корректен по набору техник. Чтобы выглядеть сильнее, важно:

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

Ниже — кратко по ключевым техникам и их применению в реальных проектах.

Техники, которые стоит уверенно использовать и уметь объяснить

  1. Классы эквивалентности (Equivalence Partitioning)

Идея:

  • Разделяем множество входных значений на группы, которые система обрабатывает одинаково.
  • Из каждой группы берём по 1–2 представителя, вместо перебора всех значений.

Применение:

  • формы ввода, параметры API;
  • валидация числовых диапазонов, строк, флагов.

Пример:

  • Поле "amount": валидно 1–10 000.
  • Классы:
    • (<1) — невалидные,
    • (1–10 000) — валидные,
    • (>10 000) — невалидные.
  • Вместо сотен значений тестируем репрезентативные: 0, 1, 5000, 10000, 10001.

Автотест на Go, построенный по классам:

func TestValidateAmount(t *testing.T) {
tests := []struct {
name string
amount int
valid bool
}{
{"zero - invalid", 0, false},
{"min - valid", 1, true},
{"middle - valid", 5000, true},
{"max - valid", 10000, true},
{"above max - invalid", 10001, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidateAmount(tt.amount); (got == nil) != tt.valid {
t.Fatalf("expected valid=%v, got error=%v", tt.valid, got)
}
})
}
}
  1. Анализ граничных значений (Boundary Value Analysis)

Идея:

  • Ошибки чаще всего на границах диапазонов.
  • Тестируем значения: min-1, min, min+1, max-1, max, max+1.

Применение:

  • лимиты (сумма, длина строки, возраст, количество попыток);
  • пагинация, временные интервалы.

В связке с эквивалентностью:

  • сначала делим на классы,
  • потом на границах классов проверяем дополнительные точки.
  1. Таблицы принятия решений (Decision Tables)

Идея:

  • При сложных правилах с несколькими условиями (флаги, типы пользователей, статусы) явно описываем комбинации условий и ожидаемые действия.

Применение:

  • тарифы, скидки, права доступа;
  • расчет комиссий;
  • сложные бизнес-ветвления.

Пример (упрощённо):

  • Условия:
    • isPremium (Y/N)
    • amount > 1000 (Y/N)
  • Действие:
    • скидка 10%, 5% или 0%.

Таблица:

  • N, N → 0%
  • N, Y → 5%
  • Y, N → 5%
  • Y, Y → 10%

Код + тесты:

func DiscountRate(isPremium bool, amount float64) float64 {
switch {
case isPremium && amount > 1000:
return 0.10
case isPremium || amount > 1000:
return 0.05
default:
return 0.0
}
}

func TestDiscountRate(t *testing.T) {
tests := []struct {
name string
premium bool
amount float64
wantRate float64
}{
{"no, <1000", false, 500, 0.0},
{"no, >1000", false, 1500, 0.05},
{"yes, <1000", true, 500, 0.05},
{"yes, >1000", true, 1500, 0.10},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := DiscountRate(tt.premium, tt.amount); got != tt.wantRate {
t.Fatalf("want %.2f, got %.2f", tt.wantRate, got)
}
})
}
}

Таблица принятия решений прямо превращается в табличные тесты.

  1. Диаграммы состояний и переходов (State Transition Testing)

Идея:

  • Системы с жизненным циклом сущностей (order, payment, user, ticket) имеют конечные состояния и допустимые переходы.
  • Тестируем:
    • валидные переходы;
    • блокировку запрещенных переходов;
    • побочные эффекты переходов.

Применение:

  • статус заказов, платежей, подписок;
  • workflow задач;
  • авторизация/сессии.

Пример: статусы заказа:

  • NEW → PAID → SHIPPED → DELIVERED
  • NEW → CANCELED
  • PAID → REFUNDED
  • Запрещено: DELIVERED → NEW, CANCELED → PAID и т.д.

SQL-пример контроля:

-- Проверка допустимого перехода
SELECT 1
FROM allowed_transitions
WHERE from_status = $1 AND to_status = $2;

Тесты должны:

  • подтвердить, что все разрешенные переходы работают;
  • попытка сделать запрещенный — приводит к контролируемой ошибке.
  1. Комбинационное тестирование (Pairwise / N-wise)

Идея:

  • При большом количестве параметров все комбинации (full) взорвают количество тестов.
  • Pairwise/N-wise позволяет покрыть все пары/тройки значений параметров минимальным набором тестов.

Применение:

  • конфигурации фичей;
  • параметры запросов;
  • сочетание устройств/браузеров/настроек.

На собеседовании достаточно:

  • показать, что знаете о комбинировании входов, не перебираете всё в лоб.
  1. Причина-следствие (Cause-Effect Graphing)

Идея:

  • Формализовать взаимосвязь "условия → следствия";
  • полезно для сложных валидаций и правил.

В реальных беседах можно кратко упомянуть как развитие таблиц принятия решений.

Как связать это в сильный устный ответ

Пример:

«На практике регулярно использую базовые, но очень эффективные техники.

Для валидации входных данных и API-параметров — классы эквивалентности и анализ граничных значений: это позволяет минимизировать количество тестов без потери качества, фокусируясь на репрезентативных значениях и границах диапазонов.

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

Для сущностей с жизненным циклом — диаграммы состояний и переходов. Явно описываю допустимые переходы (например, статусы заказа или платежа) и проверяю, что система не допускает нелегальных переходов и корректно обрабатывает каждый валидный.

Когда параметров много, использую комбинированный подход: комбинирую техники (equivalence + boundaries + decision tables) и при необходимости применяю pairwise, чтобы не раздувать тестовый набор.

Часто аналитики готовят схемы процессов и состояний, и я использую их как источник истины: проверяю на противоречия, дополняю граничные и негативные сценарии, и уже на основе этого формирую тест-кейсы и автотесты. Цель — не просто назвать техники, а встроить их в системный, риск-ориентированный дизайн тестов.»

Вопрос 11. Какие граничные значения следует проверить при регистрации пользователей с допустимым возрастом от 18 до 50 лет?

Таймкод: 00:28:42

Ответ собеседника: правильный. Кандидат называет значения 17, 18, 49, 50 как базовый набор; также предлагает расширенный набор 17, 18, 19, 49, 50, 51 для отлова типичных ошибок в условиях и объясняет логику выбора.

Правильный ответ:

Задача — показать грамотное применение анализа граничных значений и классов эквивалентности, не сводя ответ к механическому перечислению чисел.

  1. Базовая логика

Условие: допустимый возраст — от 18 до 50 лет включительно.

  • Нижняя граница:
    • 17 — должно быть отклонено;
    • 18 — должно быть принято.
  • Верхняя граница:
    • 50 — должно быть принято;
    • 51 — должно быть отклонено.

Минимально достаточный набор граничных значений:

  • 17 (минимум - 1, invalid)
  • 18 (минимум, valid)
  • 50 (максимум, valid)
  • 51 (максимум + 1, invalid)

Это классический анализ граничных значений.

  1. Классы эквивалентности

Выделяем классы:

  • Младше 18:
    • пример: 0, 17 — всегда invalid.
  • От 18 до 50:
    • пример: 18, 25, 50 — valid.
  • Старше 50:
    • пример: 51, 99 — invalid.

Для повышения надежности можно взять:

  • по одному значению из каждого класса (например, 10, 30, 60),
  • плюс граничные значения (17, 18, 50, 51).
  1. Почему расширенный набор (17, 18, 19, 49, 50, 51) имеет смысл

Расширенный набор помогает ловить типичные ошибки реализации:

  • Перепутанные операторы:
    • > вместо >= или < вместо <=;
  • Ошибки в "зеркальной" логике:
    • допустили 17 или не допустили 50;
  • Неправильное объединение условий.

Например, если условие реализовано как:

func IsAgeAllowed(age int) bool {
return age > 18 && age < 50
}

Ошибки:

  • 18 (граничное) будет отклонено;
  • 50 (граничное) будет отклонено.

Тесты с 18 и 50 сразу это покажут. Тесты с 19 и 49 дополнительно подтверждают корректное поведение внутри диапазона.

  1. Пример корректной реализации и тестов на Go

Корректная функция:

func IsAgeAllowed(age int) bool {
return age >= 18 && age <= 50
}

Тесты с граничными значениями:

func TestIsAgeAllowed_Boundaries(t *testing.T) {
tests := []struct {
age int
want bool
}{
{17, false}, // ниже нижней границы
{18, true}, // нижняя граница
{19, true}, // внутри диапазона
{49, true}, // внутри диапазона
{50, true}, // верхняя граница
{51, false}, // выше верхней границы
}

for _, tt := range tests {
if got := IsAgeAllowed(tt.age); got != tt.want {
t.Errorf("age=%d: want %v, got %v", tt.age, tt.want, got)
}
}
}

Такой набор:

  • покрывает ключевые границы,
  • проверяет корректность диапазона,
  • помогает отлавливать логические ошибки в условиях.
  1. Сильный ответ в формате интервью

«При диапазоне 18–50 включительно я в первую очередь проверяю граничные значения: 17 (invalid), 18 (valid), 50 (valid), 51 (invalid). Это минимальный обязательный набор для анализа границ. Дополнительно беру по одному значению внутри диапазона, например 19 и 49, чтобы убедиться, что логика корректна не только на границах и нет ошибок в условии (например, перепутаны знаки > / >=, < / <=). В более формальном дизайне тестов это сочетаю с классами эквивалентности: младше 18, 18–50, старше 50.»

Вопрос 12. Дайте определение API своими словами.

Таймкод: 00:31:08

Ответ собеседника: неполный. Кандидат определяет API как интерфейс, через который системы и микросервисы взаимодействуют и обмениваются данными, но даёт неточную расшифровку аббревиатуры и не раскрывает ключевые аспекты.

Правильный ответ:

API (Application Programming Interface) — это формализованный программный интерфейс, который определяет, как одна программа, сервис или компонент может взаимодействовать с другим. Это контракт, описывающий:

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

Ключевые характеристики хорошего API:

  1. Явный контракт

API — это не просто набор урлов или функций, а строгий контракт, который:

  • стабилен и предсказуем;
  • документирован (OpenAPI/Swagger для REST, Protobuf для gRPC, схемы для событий);
  • отделяет внутреннюю реализацию от внешнего поведения.

Важно:

  • внутренний код может меняться;
  • контракт API должен оставаться обратимо совместимым или версионироваться.
  1. Инкапсуляция и слабая связность

API скрывает детали реализации:

  • потребителю не важно:
    • какая БД внутри,
    • какой язык или фреймворк,
    • какая внутренняя структура таблиц;
  • важны только:
    • входы,
    • выходы,
    • семантика операций.

Это позволяет:

  • менять реализацию без поломки клиентов;
  • строить микросервисную архитектуру, где сервисы общаются по контрактам, а не по «общей базе данных» или шэрингу внутренних структур.
  1. Типы API в реальных системах
  • Внутренние API:
    • между микросервисами (REST, gRPC, message-based);
  • Публичные API:
    • для внешних интеграций (партнёры, мобильные клиенты);
  • Библиотечные API:
    • функции/методы пакетов, SDK, драйверов;
  • Event-driven/API через сообщения:
    • контракты сообщений в Kafka/NATS/RabbitMQ.
  1. API как основа тестирования и качества

Для инженерного подхода важно:

  • API — главный объект контрактного тестирования:
    • мы тестируем не только «работает ли код», а «устойчив ли контракт»;
  • через API определяются:
    • SLA,
    • требования к производительности и надёжности,
    • правила совместимости при релизах (backward/forward compatible).
  1. Пример: простой HTTP API на Go

Определим минимальный REST-like API для создания и получения пользователя.

Контракт (словами):

  • POST /users:
    • Request: { "email": "string" }
    • Response 201: { "id": number, "email": "string" }
  • GET /users/{id}:
    • Response 200: { "id": number, "email": "string" }
    • Response 404: если пользователь не найден

Реализация (упрощённо):

type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
}

type Server struct {
users map[int64]User
nextID int64
}

func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Email == "" {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}

s.nextID++
u := User{ID: s.nextID, Email: req.Email}
s.users[u.ID] = u

w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(u)
}

func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}

u, ok := s.users[id]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}

_ = json.NewEncoder(w).Encode(u)
}

Здесь API — это:

  • набор доступных HTTP-методов и путей;
  • формат JSON-запросов и ответов;
  • коды состояния и их значения (201, 400, 404);
  • поведение при ошибках (invalid id, not found).

Тестирование этого API проверяет соблюдение контракта.

  1. Пример: SQL и API

API также определяет, как безопасно работать с данными, не раскрывая прямую структуру БД:

-- внутренний слой (реализация), не обязательный для клиента
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
status TEXT NOT NULL DEFAULT 'active'
);

Клиент не взаимодействует с таблицей напрямую, он работает через API (REST/gRPC), что:

  • позволяет менять схему без изменения клиентов;
  • усиливает безопасность;
  • централизует бизнес-логику.

Сильный ответ на интервью:

«API — это программный интерфейс и формализованный контракт взаимодействия между компонентами, сервисами или внешними клиентами. Он определяет, какие операции доступны, какие данные принимаются и возвращаются, какие ошибки возможны и при каких условиях. Хороший API инкапсулирует реализацию, стабилен, задокументирован, удобен потребителям и позволяет развивать систему эволюционно без ломки интеграций.»

Вопрос 13. Какие инструменты вы используете для тестирования API?

Таймкод: 00:31:33

Ответ собеседника: правильный. Использует Altair для GraphQL (запросы и документация), Postman для REST, упоминает практический опыт тестирования REST API на предыдущих проектах.

Правильный ответ:

Корректно назвать Postman/Altair — хороший старт. Более сильный ответ показывает:

  • осознанный выбор инструментов под тип API и задачи;
  • интеграцию с CI/CD;
  • связку с контрактами (OpenAPI/Swagger, GraphQL schema, Protobuf);
  • использование автотестов и вспомогательных утилит (на Go) как полноценного инструмента тестирования API.

Ниже структурированный ответ.

Инструменты для REST API

  1. Postman

Использование:

  • ручная проверка эндпоинтов:
    • positive/negative сценарии,
    • авторизация (Bearer, OAuth2, API keys),
    • проверка заголовков, кодов ответов, тела;
  • коллекции запросов:
    • привязка к фичам/микросервисам;
    • запуск в разных окружениях (dev/stage/prod);
  • скрипты:
    • pre-request scripts (генерация токенов, подготовка данных);
    • tests (проверка статус-кодов, схемы ответа, бизнес-условий).

Усиление:

  • Newman (CLI) для запуска Postman-коллекций в CI:
    • smoke/regression по API на каждом билде;
    • базовая автоматизация без сложной инфраструктуры.
  1. Insomnia / HTTPie / curl

Часто используются:

  • для быстрых ad-hoc проверок;
  • для дебага сложных кейсов;
  • когда нужен точный контроль над запросом и заголовками.

Пример простого curl-запроса:

curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com"}'

Инструменты для GraphQL

  1. Altair / GraphiQL / Apollo Sandbox

Использование:

  • интерактивный конструктор запросов и мутаций;
  • автодополнение из схемы;
  • просмотр документации (schema introspection);
  • быстрое прототипирование и проверка сложных запросов.

Практика:

  • проверка:
    • корректности схемы и типов;
    • работы фильтров, пагинации, вложенных полей;
    • обработки разрешений (какие поля доступны для разных ролей).

Важный момент: при тестировании GraphQL уделяется внимание:

  • ограничению глубины запросов;
  • лимитам (rate limiting, complexity);
  • корректным ошибкам (GraphQL errors, extensions).

Автоматизированное тестирование API на Go

Инструменты и подходы, которые стоит выделить, чтобы показать уровень:

  1. Стандартная библиотека Go (net/http, httptest)

Для REST/gRPC backend-а мощный подход — писать API-тесты прямо в коде:

  • unit-тесты хэндлеров;
  • интеграционные тесты поверх поднятого test server.

Пример теста REST-эндпоинта на Go:

func TestCreateUserAPI(t *testing.T) {
srv := newTestServer() // поднимаем in-memory HTTP-сервер с тестовой БД
defer srv.Close()

body := `{"email":"user@example.com"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

srv.router.ServeHTTP(w, req)

if w.Code != http.StatusCreated {
t.Fatalf("want status 201, got %d, body=%s", w.Code, w.Body.String())
}

var resp struct {
ID int64 `json:"id"`
Email string `json:"email"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid json: %v", err)
}
if resp.Email != "user@example.com" {
t.Fatalf("unexpected email: %s", resp.Email)
}
}

Плюсы:

  • API-тесты становятся частью регрессии;
  • гоняются в CI при каждом коммите;
  • живут рядом с кодом и обновляются синхронно.
  1. Контрактное тестирование

Инструменты/подходы:

  • OpenAPI/Swagger:
    • валидация ответов по схеме;
    • генерация клиентов/серверов;
  • Dredd, Schemathesis — проверка соответствия реализации спецификации;
  • для gRPC — Protobuf + автогенерация клиентов, тесты поверх контрактов.

Цель:

  • гарантировать, что изменения в API не ломают потребителей;
  • контролировать backward compatibility.
  1. Инструменты для нагрузочного тестирования API

Важно упомянуть как часть экосистемы:

  • k6, JMeter, Locust:
    • моделирование RPS, сценариев, авторизации;
    • проверка p95/p99 latency, ошибок, деградаций.

Например (k6, фрагмент):

import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
vus: 50,
duration: '1m',
};

export default function () {
const res = http.get('https://api.example.com/v1/users/1');
check(res, {
'status is 200': (r) => r.status === 200,
'latency < 200ms': (r) => r.timings.duration < 200,
});
sleep(1);
}

Связка с тестированием API:

  • функциональная проверка + нефункциональные характеристики.
  1. Безопасность и вспомогательные инструменты
  • Burp Suite / OWASP ZAP:
    • для проверки security аспектов API;
  • jwt.io, самописные утилиты:
    • для генерации/проверки токенов;
  • логирование и трассировка:
    • анализ корректности корреляции запросов, trace-id.

Сильный ответ в формате интервью

«Для тестирования REST API обычно использую Postman: коллекции, окружения, pre-request/ test-скрипты, а для автоматизации регресса — запуск коллекций через Newman в CI.

Для GraphQL применяю Altair или встроенный GraphiQL/Apollo Sandbox: они позволяют работать со схемой, быстро собирать сложные запросы и видеть документацию из introspection.

Параллельно я за то, чтобы существенная часть API-тестов была реализована как автотесты на Go: через httptest поднимаем тестовый сервер, проверяем контракты, статусы, схемы ответов. Контрактную часть фиксируем в OpenAPI/Protobuf и валидируем совместимость при изменениях.

Для нагрузочного и негативного сценариев подключаем k6/JMeter и при необходимости инструменты для security-сканирования. В целом, выбираю инструменты так, чтобы они интегрировались в CI/CD и давали быстрый, воспроизводимый фидбек по качеству API, а не жили отдельно от разработки.»

Вопрос 14. На какой порт по умолчанию приходит HTTPS-запрос?

Таймкод: 00:32:40

Ответ собеседника: правильный. Указывает, что по умолчанию используется порт 443.

Правильный ответ:

По умолчанию HTTPS-запросы обрабатываются на порту 443.

Дополнительные комментарии для полноты:

  • HTTP по умолчанию использует порт 80.
  • HTTPS — это HTTP поверх TLS/SSL, и стандартный порт для зашифрованного трафика — 443.
  • В реальных системах порт может быть изменён (например, 8443), но тогда он должен быть явно указан в конфигурации и/или URL.

Пример простого HTTPS-сервера на Go:

package main

import (
"log"
"net/http"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})

// Сервер будет слушать порт 443, требуется валидный сертификат и ключ.
err := http.ListenAndServeTLS(":443", "server.crt", "server.key", mux)
if err != nil {
log.Fatal(err)
}
}

На проде чаще используют reverse proxy (nginx, Envoy, Traefik), который принимает HTTPS на 443 и проксирует внутрь по HTTP (например, на 8080).

Вопрос 15. Что означает статус-код 400 в ответе на запрос?

Таймкод: 00:32:57

Ответ собеседника: правильный. Указывает, что 400 означает ошибку на стороне клиента (некорректный запрос), и отмечает, что на практике иногда этим кодом ошибочно маскируют серверные ошибки.

Правильный ответ:

Статус-код 400 Bad Request означает, что сервер не может обработать запрос из-за ошибки на стороне клиента. Типичные причины:

  • некорректный или битый формат данных (JSON/XML не парсится);
  • невалидные значения параметров (например, строка вместо числа, нарушены правила валидации);
  • отсутствуют обязательные поля;
  • неверная структура тела запроса;
  • нарушены синтаксические требования протокола HTTP.

Ключевые моменты:

  1. Семантика 4xx против 5xx
  • 4xx (включая 400):
    • «Клиент прислал неправильный запрос, даже если повторить его без изменений — он останется некорректным».
  • 5xx:
    • «Запрос был валидным, но проблема на стороне сервера/инфраструктуры».

Ошибка в выборе кода статуса:

  • использовать 400, когда на самом деле произошёл баг, паника, ошибка БД, таймаут — плохая практика:
    • это скрывает ответственность сервера;
    • затрудняет мониторинг и инцидент-менеджмент;
    • ломает корректную обработку ошибок на стороне клиента.
  1. Когда уместно 400 vs более специфичные коды
  • 400 — общее "Bad Request", часто используется для синтаксических/форматных проблем.
  • Более точные варианты:
    • 401 Unauthorized — нет/неверная аутентификация;
    • 403 Forbidden — аутентификация есть, но нет прав;
    • 404 Not Found — ресурс не найден;
    • 409 Conflict — конфликт состояния (например, дубликат);
    • 422 Unprocessable Entity — семантически некорректные данные (формат ок, но бизнес-валидация не прошла).

Хороший дизайн API:

  • различает синтаксические ошибки (400) и бизнес-ошибки/конфликты (422/409 и т.п.);
  • помогает клиентам корректно реагировать на ошибки.
  1. Пример реализации в Go

Разделение валидации и внутренних ошибок:

func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Age int `json:"age"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}

if req.Email == "" || req.Age < 18 {
// Клиент прислал данные, не соответствующие требованиям API
http.Error(w, "invalid input data", http.StatusBadRequest)
return
}

if err := saveUserToDB(r.Context(), req.Email, req.Age); err != nil {
// Это уже проблема на стороне сервера или БД
log.Printf("save user error: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
}

Здесь:

  • 400:
    • когда JSON не читается или нарушены правила валидации входа;
  • 500:
    • если упали на взаимодействии с БД или внутренней логике.

Такое разделение:

  • делает поведение предсказуемым;
  • упрощает контракт для клиентов;
  • позволяет по логам и метрикам чётко видеть, где проблема: в использовании API или в системе.
  1. Практический вывод

Грамотное использование 400:

  • это не просто "что-то не так";
  • это чёткий сигнал: «исправь запрос, не наш сервер».

Важно:

  • всегда сопровождать 400 понятным ответом в теле:
    • код/тип ошибки,
    • описание,
    • по возможности — указание на проблемное поле;
  • не использовать 400 для маскировки внутренних багов и сбоев.

Вопрос 16. Что означает код ответа 400 при выполнении HTTP-запроса?

Таймкод: 00:32:57

Ответ собеседника: правильный. Поясняет, что 400 сигнализирует об ошибке на стороне клиента (некорректный запрос), и отмечает, что на практике этот код иногда ошибочно используется вместо 500.

Правильный ответ:

Код ответа 400 Bad Request означает, что сервер не может обработать запрос из-за ошибки на стороне клиента. Ключевая идея: запрос некорректен в своей форме или содержимом, и повтор этого же запроса без исправлений не приведет к успеху.

Типичные причины для 400:

  • некорректный или поврежденный JSON/формат тела;
  • неверный Content-Type (например, text/plain вместо application/json);
  • отсутствие обязательных полей;
  • неверные типы данных (строка вместо числа, некорректная дата);
  • невалидные query-параметры (например, page=-1);
  • нарушение синтаксиса HTTP-запроса.

Важно отличать 400 от 5xx:

  • 4xx-коды:
    • проблема с запросом клиента;
    • клиент может исправить и повторить.
  • 5xx-коды:
    • проблема на стороне сервера/инфраструктуры;
    • запрос в целом валиден, но сервер не смог его корректно обработать.

Распространенная ошибка — возвращать 400 для внутренних ошибок (исключения, проблемы с БД, баги в логике). Это маскирует ответственность сервера, ломает мониторинг и мешает клиентам корректно обрабатывать ошибки. В таких случаях корректнее использовать 500 или другой соответствующий 5xx-код.

Пример корректной обработки в Go:

func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Age int `json:"age"`
}

if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}

if req.Email == "" || req.Age < 18 {
http.Error(w, "invalid input data", http.StatusBadRequest)
return
}

if err := saveUser(r.Context(), req.Email, req.Age); err != nil {
// внутренняя ошибка (БД, логика и т.п.) — это уже 5xx
log.Printf("save user failed: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
}

Зрелый подход:

  • 400 — только для ошибок клиента;
  • 4xx разбиваем по семантике (401, 403, 404, 409, 422), чтобы клиенту было понятно, что исправлять;
  • 5xx используем для реальных серверных проблем и отслеживаем их метриками и алертами.

Вопрос 17. Сколько проверок нужно выполнить для минимальной, но достаточной проверки алгоритма смены направления движения стрелок по дням недели на часах?

Таймкод: 00:34:49

Ответ собеседника: неправильный. Кандидат несколько раз меняет подход: сначала предлагает 7 проверок, затем 14 (с учётом 12:00 и 24:00), но не формулирует чёткий минимальный набор и путается в обосновании.

Правильный ответ:

Правильный подход к этому вопросу — не просто назвать число тестов, а показать системное применение техник тест-дизайна: анализ граничных значений, таблицы решений, тестирование переходов состояний.

Так как формулировка задачи дана устно и часто намеренно не до конца конкретна, сначала важно явно уточнить модель:

Типичная постановка (один из распространённых вариантов такой задачи):

  • Есть часы, которые в разные дни недели меняют направление движения стрелок.
  • Например:
    • в будние дни — стрелки идут по часовой стрелке,
    • в выходные — против часовой.
  • Или:
    • в разные дни действует разное правило, завязанное на смену дня и времени (например, в 00:00).

Цель вопроса: проверить, сможете ли вы:

  • выделить входные параметры;
  • сформировать классы эквивалентности;
  • найти критические точки смены направления;
  • посчитать минимальный набор проверок, который гарантированно валидирует алгоритм.

Разберём абстрактно и затем сведём к ответу.

  1. Выделяем параметры и поведение

Минимальный набор параметров:

  • день недели (7 значений);
  • время (непрерывный диапазон, но нас интересуют ключевые точки);
  • направление движения стрелок (результат алгоритма).

Ключевые риски:

  • неправильное направление в обычное время;
  • неверное переключение направления в момент смены дня;
  • off-by-one-ошибки вокруг границ (до/после полуночи).
  1. Применяем классы эквивалентности

День недели:

  • каждый день — потенциально отдельный класс, если для него может действовать своё правило;
  • но если правило одинаковое для группы дней (например, Пн–Пт одно, Сб–Вс другое), эту группу можно тестировать репрезентативно (по одному дню от группы).

Время:

  • внутри дня:
    • интервал «обычного» времени, где правило стабильно (класс эквивалентности);
    • границы смены (обычно около 00:00 или других специальных точек).
  1. Применяем анализ граничных значений и переходов

Если алгоритм привязан к смене дня (00:00), критичные точки:

  • "чуть до" смены дня;
  • "ровно в момент" смены;
  • "чуть после" смены.

Например, если:

  • Пн — направление A,
  • Вт — направление B,

то проверяем:

  • Пн, 23:59 — A;
  • Вт, 00:00 — B (или A, если по условию смена сдвинута — зависит от требований);
  • Вт, 00:01 — B.
  1. Как считать минимальный достаточный набор

Общий принцип для такого рода задач (и то, что обычно ожидают услышать на собеседовании):

  • Не нужно тестировать все возможные комбинации времени.
  • Нужно покрыть:
    1. по одному «обычному» моменту времени для каждой уникальной группы дней с одинаковым правилом;
    2. все границы перехода между разными правилами:
      • для каждой пары дней, где меняется правило направления, достаточно 2–3 проверки вокруг границы:
        • до границы,
        • в момент (если релевантно),
        • после границы.

Пример (конкретизация для демонстрации подхода):

Пусть:

  • Пн–Пт — стрелки по часовой,
  • Сб–Вс — стрелки против часовой,
  • правило меняется в 00:00 соответствующего дня.

Минимальный набор:

  1. Проверка обычного поведения в каждой группе:
  • Пн, 12:00 — по часовой (представитель Пн–Пт);
  • Сб, 12:00 — против часовой (представитель Сб–Вс).
  1. Проверка границ переходов:
  • Пт 23:59 — по часовой;
  • Сб 00:00 (или 00:01) — против часовой;
  • Вс 23:59 — против часовой;
  • Пн 00:00 (или 00:01) — по часовой.

Итого:

  • 2 проверки для обычного времени,
  • по 2 проверки для каждой границы перехода (до/после),
  • всего 6–8 проверок (в зависимости от того, считаем ли "ровно в 00:00" отдельно).

Это уже значительно лучше, чем наивные 7 или 14 проверок "по одному на день" без анализа переходов.

  1. Что важно показать на интервью

Ключ не в конкретном числе, а в аргументации:

  • Вы:
    • выделяете правила;
    • группируете дни с одинаковым поведением;
    • применяете классы эквивалентности;
    • тестируете только репрезентативные значения;
    • отдельно покрываете границы переходов (до/после).

Хороший ответ мог бы звучать так:

«Минимальный набор не равен просто 7 тестам по количеству дней. Я бы сделал так: сгруппировал дни по одинаковым правилам, для каждой группы проверил одно "обычное" время внутри дня, а затем добавил проверки на границах перехода — непосредственно до смены дня и после неё для тех пар дней, где меняется направление. В итоге это даёт небольшой набор из 6–8 тестов, который покрывает и корректность правил, и off-by-one-ошибки на границах, без полного перебора всех дней и времён.»

Такой подход демонстрирует владение тест-дизайном и умение считать минимальный, но достаточный набор проверок.

Вопрос 18. Составьте чек-лист для проверки нового поля выбора руководителя в карточке сотрудника на основе заданных требований.

Таймкод: 00:47:56

Ответ собеседника: неполный. Кандидат перечисляет базовые проверки (тип поля, раскрытие списка, обязательность, сортировка, одиночный выбор, отсутствие поиска, фильтрация по признаку руководителя с проверкой через БД), но не полностью покрывает негативные, граничные, UX/данные кейсы и местами делает незафиксированные допущения.

Правильный ответ:

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

  • Поле "Руководитель":
    • реализовано как выпадающий список;
    • обязательное для заполнения;
    • допускает выбор только одного руководителя;
    • заполняется только пользователями с признаком "может быть руководителем" (флаг в системе/БД);
    • список отсортирован по алфавиту (по ФИО или по регламентированному формату);
    • поле без строки поиска (если явно указано);
    • выбор сохраняется в карточке и корректно отображается везде, где используется.

Если в реальных условиях требования иные — чек-лист адаптируется, но логика остаётся.

Чек-лист проверок

  1. Отображение и базовое поведение
  • Поле "Руководитель" видно в карточке сотрудника в соответствии с требованиями:
    • в нужном разделе;
    • с корректной подписью/лейблом.
  • Поле по умолчанию (при создании нового сотрудника):
    • пусто или предзаполнено согласно бизнес-правилу (если такое есть);
    • состояние по умолчанию явно согласовано.
  • Клик по полю раскрывает выпадающий список.
  • Закрытие списка:
    • по выбору значения;
    • по клику вне списка;
    • по Esc (если заявлено UX-стандартом продукта).
  1. Тип и режим выбора
  • Можно выбрать только одно значение:
    • повторный клик по другому элементу меняет выбор;
    • нет возможности множественного выбора (Ctrl, Shift, чекбоксы и т.п. не работают).
  • Выбранное значение отображается в свернутом поле корректно (ФИО/идентификатор по требованиям).
  1. Обязательность поля
  • Попытка сохранить карточку сотрудника без выбранного руководителя:
    • сохранение блокируется;
    • отображается понятное сообщение об ошибке рядом с полем/общей областью ошибок;
    • текст ошибки соответствует требованиям (например: «Выберите руководителя»).
  • После выбора руководителя и повторной попытки сохранения:
    • ошибка исчезает;
    • карточка успешно сохраняется.
  1. Состав и фильтрация значений
  • В выпадающем списке отображаются только сотрудники, имеющие флаг "руководитель" (или соответствующую роль/признак).
  • В списке отсутствуют:
    • сам текущий сотрудник (нельзя выбрать себя в качестве руководителя, если таково требование — часто логично и стоит уточнить);
    • пользователи, помеченные как уволенные/заблокированные/неактивные, если по требованиям они не могут быть руководителями.
  • Граничный кейс: если в системе только один возможный руководитель:
    • он отображается в списке;
    • выбор возможен;
    • поведение не ломается.
  • Граничный кейс: если временно нет ни одного пользователя с признаком "руководитель":
    • список пуст;
    • поведение системы корректно:
      • либо блокировка сохранения с понятным сообщением,
      • либо особое правило (например, системный администратор по умолчанию) — уточняется в требованиях.
  1. Сортировка и формат отображения
  • Список отсортирован по алфавиту в соответствии с заданным форматом:
    • по ФИО или "Фамилия Имя Отчество", или по другому явно указанному полю;
  • Локализация и регистр:
    • сортировка корректна для русских/латинских букв, не "ломается" на разных алфавитах;
  • При совпадении фамилий:
    • порядок детерминирован (например, сначала по фамилии, потом по имени).
  • Значения отображаются в согласованном формате:
    • нет обрезанных строк;
    • при длинных ФИО отображение корректно (многоточие/tooltip).
  1. Взаимодействие с БД и данными (на уровне интеграции)

Проверки с опорой на БД (через SELECT, логично выполнять в тестовом контуре):

  • Список в UI соответствует данным в БД:
    • выбираются только те записи, где is_leader = true (или аналогичный флаг).
  • После выбора руководителя:
    • в таблице сотрудников корректно сохраняется ссылка на руководителя (например, employees.manager_id = id выбранного пользователя);
  • При открытии карточки уже существующего сотрудника:
    • в поле "Руководитель" показывается значение, соответствующее manager_id в БД.
  • Изменение руководителя в UI:
    • обновляет значение в БД;
    • старое значение не остаётся в кеше/кэше фронта.

Пример SQL для проверки данных (упрощённо):

SELECT e.id, e.name, e.manager_id, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id
WHERE e.id = :employee_id;
  1. Негативные и граничные сценарии
  • Поведение при невыбранном руководителе и попытке сохранения уже существующей записи, если поле стало обязательным:
    • миграционный сценарий: как система ведет себя для «старых» сотрудников без руководителя.
  • Попытка вручную подставить несуществующий или не имеющий права быть руководителем ID (если есть API или манипуляция через DevTools):
    • бэкенд отклоняет некорректное значение;
    • возвращается корректный HTTP-статус и понятное сообщение об ошибке;
    • UI не позволяет сохранить неконсистентные данные.
  • Поведение при временной недоступности списка (ошибка запроса на backend):
    • отображение понятной ошибки или fallback;
    • отсутствие «тихого» сохранения с пустым/некорректным руководителем.
  • Проверка прав доступа:
    • если в системе есть разные роли (HR, админ, линейный руководитель, сотрудник):
      • кто может редактировать поле;
      • кто может только просматривать;
      • у кого поле скрыто.
  1. UX/удобство (если релевантно требованиям)
  • Поле визуально помечено как обязательное (звёздочка, подсказка).
  • Ошибка при незаполненном поле подсвечивает именно это поле.
  • Навигация с клавиатуры:
    • можно дойти до поля табом;
    • открыть/закрыть список, выбрать значение (если это стандарт продукта).
  1. Автоматизация и регрессия
  • Ключевые сценарии (выбор руководителя, валидация обязательности, фильтрация по флагу, сохранение/редактирование) включены в регрессионные автотесты:
    • UI-тесты или API+контрактные тесты, если UI тонкий.
  • Добавлены проверки на уровне API (если есть endpoint для сохранения карточки):
    • отправка валидного manager_id → 200/201 и корректное сохранение;
    • отправка manager_id, который не является руководителем → корректная 4xx-ошибка.

Сильный ответ на интервью

«Я начинаю с явного перечисления требований к полю и на их основе строю чек-лист, разделяя проверки на группы: базовое отображение и поведение, обязательность, фильтрацию по признаку "руководитель", сортировку, негативные кейсы и интеграцию с данными. Обязательно проверяю, что в выпадающий список попадают только допустимые кандидаты, что нельзя выбрать себя или неактивных сотрудников (если это запрещено), что поле действительно блокирует сохранение без выбора и что выбор корректно сохраняется в БД и отображается при повторном открытии карточки. Ключевые сценарии выношу в автотесты, чтобы это вошло в регрессию. Чек-лист получается компактным, но покрывает и функциональные требования, и типичные граничные/негативные случаи.»

Вопрос 19. Выведите идентификаторы отделов и количество сотрудников в них, если в отделе не более трёх сотрудников (SQL-запрос).

Таймкод: 01:00:21

Ответ собеседника: правильный. Использует группировку по идентификатору отдела и HAVING для отбора отделов с количеством сотрудников не более трёх.

Правильный ответ:

Базовое корректное решение:

SELECT dept_id,
COUNT(*) AS employees_count
FROM employees
GROUP BY dept_id
HAVING COUNT(*) <= 3;

Разбор и важные моменты:

  1. Логика решения:
  • GROUP BY dept_id:
    • агрегирует сотрудников по отделам.
  • COUNT(*):
    • считает число сотрудников в каждом отделе.
  • HAVING COUNT(*) <= 3:
    • отфильтровывает группы (отделы) после агрегации;
    • условие по агрегатам нельзя указывать в WHERE, поэтому используется HAVING.
  1. Типичные нюансы, которые полезно учитывать на практике:
  • Учет уволенных/неактивных сотрудников:

    • если есть поле статуса (status, active, fired_at), возможно нужно считать только активных:
      SELECT dept_id,
      COUNT(*) AS employees_count
      FROM employees
      WHERE status = 'active'
      GROUP BY dept_id
      HAVING COUNT(*) <= 3;
  • Отделы без сотрудников:

    • если нужно включать отделы с 0–3 сотрудниками, а список отделов хранится отдельно:
      SELECT d.id          AS dept_id,
      COUNT(e.id) AS employees_count
      FROM departments d
      LEFT JOIN employees e
      ON e.dept_id = d.id
      AND e.status = 'active'
      GROUP BY d.id
      HAVING COUNT(e.id) <= 3;
    • Здесь:
      • LEFT JOIN позволяет показывать отделы с 0 сотрудников;
      • условие по активности сотрудников — в ON или WHERE в зависимости от логики.
  1. Производительность и качество запроса:
  • Индексы:
    • для большой таблицы employees полезен индекс по dept_id (и, при необходимости, по статусу):
      CREATE INDEX idx_employees_dept_active
      ON employees(dept_id, status);
  • HAVING используется только для условий по агрегированным значениям. Если часть условий не зависит от агрегатов, её нужно вынести в WHERE для уменьшения объема данных до агрегации.
  1. Инженерный контекст:
  • Такой запрос часто используется:
    • для аналитики (поиск малых отделов),
    • для валидации бизнес-правил (например, отдел не должен быть меньше/больше определенного размера).
  • В продакшене:
    • подобные запросы могут быть инкапсулированы во вьюхи или репозитории;
    • над ними строятся проверки, отчеты, ограничения.

Для собеседования достаточно уверенно написать запрос с GROUP BY + HAVING, но сильный ответ показывает понимание нюансов: активность сотрудников, отделы без сотрудников, использование LEFT JOIN, индексов и корректного разделения WHERE/HAVING.

Вопрос 20. Составьте SQL-запрос для получения сотрудников, у которых зарплата выше, чем у их руководителя.

Таймкод: 01:01:47

Ответ собеседника: неправильный. Кандидат затрудняется с самосоединением по полю руководителя, просит подсказки, не доводит запрос до корректного решения и сам признаёт, что ушёл не в ту сторону.

Правильный ответ:

Это классическая задача на самосоединение (self join) одной таблицы. Предположим структуру таблицы:

  • employees:
    • id — идентификатор сотрудника
    • name — имя сотрудника
    • manager_id — идентификатор руководителя (FK на employees.id)
    • salary — зарплата

Нужно выбрать сотрудников, у которых salary больше, чем salary их руководителя.

Базовое правильное решение:

SELECT  e.id          AS employee_id,
e.name AS employee_name,
e.salary AS employee_salary,
m.id AS manager_id,
m.name AS manager_name,
m.salary AS manager_salary
FROM employees e
JOIN employees m
ON e.manager_id = m.id
WHERE e.salary > m.salary;

Разбор по шагам:

  1. Самосоединение таблицы employees
  • Используем два разных псевдонима одной и той же таблицы:
    • e — "подчинённый" (employee),
    • m — "руководитель" (manager).
  • Логика связи:
    • e.manager_id = m.id:
      • у сотрудника e.manager_id указывает на id его руководителя,
      • это стандартный паттерн иерархии "сотрудник — руководитель" в одной таблице.
  1. Условие по зарплате
  • В WHERE сравниваем:
    • e.salary > m.salary
    • выбираем только тех сотрудников, чья зарплата строго выше зарплаты менеджера.
  1. Какие поля выводить

В зависимости от задачи можно:

  • вернуть только сотрудников:

    SELECT e.*
    FROM employees e
    JOIN employees m ON e.manager_id = m.id
    WHERE e.salary > m.salary;
  • или, что обычно полезнее, вместе с их руководителями, как в основном примере:

    • так удобнее проверять и интерпретировать результат.
  1. Граничные и практические моменты
  • Сотрудники без руководителя:

    • у топ-менеджеров/директоров manager_id может быть NULL;
    • JOIN по условию e.manager_id = m.id автоматически исключит такие записи, что нам и нужно:
      • сравнивать не с кем → такие сотрудники не попадают в выборку.
    • если по архитектуре manager_id может быть некорректным, стоит следить за целостностью данных (FK).
  • Null-значения зарплаты:

    • если у кого-то salary = NULL, сравнений с NULL нужно избегать или обрабатывать:
      WHERE e.salary > m.salary
      AND e.salary IS NOT NULL
      AND m.salary IS NOT NULL;
    • либо гарантировать NOT NULL на уровне схемы.
  • Производительность:

    • полезно иметь индексы:
      • по id (PK),
      • по manager_id,
      • иногда составные индексы если есть дополнительные фильтры.

Пример индекса:

CREATE INDEX idx_employees_manager_id ON employees(manager_id);
  1. Инженерный контекст

Этот запрос демонстрирует:

  • понимание self join;
  • умение мыслить в терминах связей внутри одной таблицы;
  • аккуратность в условиях соединения и фильтрации.

Сильное формулирование на интервью:

«Нам нужна самоссылка. Используем employees дважды: один раз как сотрудник, второй — как его руководитель, связав e.manager_id = m.id. Далее в WHERE сравниваем их зарплаты и выбираем тех, у кого e.salary > m.salary. Важно, что сотрудники без руководителя не попадут в выборку, и что условие сравнения по зарплате задаётся уже после корректного соединения.»

Вопрос 21. Составьте SQL-запрос для получения сотрудников, у которых зарплата выше, чем у их руководителя.

Таймкод: 01:11:29

Ответ собеседника: неполный. После подсказки про self-join описывает общий подход через соединение таблицы с самой собой по идентификатору руководителя, но пишет запрос с ошибками и не доводит его до корректного решения.

Правильный ответ:

Для этой задачи используется самосоединение (self join) таблицы сотрудников. Типичная структура:

  • employees:
    • id — идентификатор сотрудника
    • name — имя
    • manager_id — идентификатор руководителя (FK на employees.id)
    • salary — зарплата

Нужно вывести сотрудников, у которых salary больше, чем у их руководителя.

Базовое корректное решение:

SELECT
e.id AS employee_id,
e.name AS employee_name,
e.salary AS employee_salary,
m.id AS manager_id,
m.name AS manager_name,
m.salary AS manager_salary
FROM employees e
JOIN employees m
ON e.manager_id = m.id
WHERE e.salary > m.salary;

Ключевые моменты и объяснение:

  1. Самосоединение одной таблицы:

    • Мы используем таблицу employees дважды:
      • e — подчинённый (employee),
      • m — руководитель (manager).
    • Связь:
      • e.manager_id = m.id
      • означает: у сотрудника e руководитель — это запись m.
  2. Условие отбора:

    • В WHERE сравниваем зарплаты:
      • e.salary > m.salary
    • В выборку попадают только те сотрудники, чья зарплата строго выше зарплаты их руководителя.
  3. Сотрудники без руководителя:

    • У топ-руководителей manager_id может быть NULL.
    • INNER JOIN по e.manager_id = m.id автоматически исключит их:
      • нам и не нужно сравнивать их с кем-то.
  4. Нюансы для реальных систем:

    • Если возможны NULL в salary:
      • стоит явно исключить их, чтобы избежать "не сравнивается с NULL":
        WHERE e.salary IS NOT NULL
        AND m.salary IS NOT NULL
        AND e.salary > m.salary;
    • Для больших таблиц:
      • полезен индекс по manager_id, помимо PK по id:
        CREATE INDEX idx_employees_manager_id ON employees(manager_id);

Такой запрос демонстрирует корректное понимание self join и умение работать с иерархическими связями в одной таблице.

Вопрос 22. Составьте SQL-запрос для получения списка отделов с суммарной зарплатой сотрудников и выберите отдел с максимальной суммарной зарплатой.

Таймкод: 01:13:38

Ответ собеседника: неправильный. Пытается использовать GROUP BY и ORDER BY, но ошибочно группирует по зарплате, не использует SUM корректно, затем с подсказками переходит к идее подзапроса, однако не доводит решение до рабочего вида.

Правильный ответ:

Задача проверяет:

  • умение агрегировать данные по отделам;
  • корректное использование SUM, GROUP BY, ORDER BY;
  • умение выбрать максимум по агрегированному значению (через сортировку или подзапрос).

Предположим структуру:

  • employees:
    • id
    • name
    • dept_id
    • salary
  • (опционально) departments:
    • id
    • name
  1. Список отделов с суммарной зарплатой сотрудников

Базовый запрос:

SELECT
dept_id,
SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id;

Объяснение:

  • GROUP BY dept_id — группируем сотрудников по отделам;
  • SUM(salary) — считаем суммарную зарплату по каждому отделу;
  • никаких лишних полей, не входящих в GROUP BY или агрегат, быть не должно.

Если хотим получить имя отдела (при наличии таблицы departments):

SELECT
d.id AS dept_id,
d.name AS dept_name,
SUM(e.salary) AS total_salary
FROM departments d
JOIN employees e
ON e.dept_id = d.id
GROUP BY d.id, d.name;
  1. Отдел с максимальной суммарной зарплатой (вариант через сортировку)

Самый простой и читаемый способ:

SELECT
dept_id,
SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id
ORDER BY total_salary DESC
LIMIT 1;
  • Сначала считаем сумму по отделам;
  • затем сортируем по суммарной зарплате по убыванию;
  • LIMIT 1 выбирает отдел с максимальной суммой.

С именем отдела:

SELECT
d.id AS dept_id,
d.name AS dept_name,
SUM(e.salary) AS total_salary
FROM departments d
JOIN employees e
ON e.dept_id = d.id
GROUP BY d.id, d.name
ORDER BY total_salary DESC
LIMIT 1;
  1. Вариант через подзапрос (если прямой LIMIT/ORDER BY нежелателен или нужен более декларативный стиль)

Шаг 1 — агрегируем суммы по отделам:

SELECT
dept_id,
SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id;

Шаг 2 — найдём максимальную сумму:

SELECT MAX(total_salary)
FROM (
SELECT dept_id, SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id
) t;

Шаг 3 — выберем отдел(ы) с максимальной суммой:

SELECT
t.dept_id,
t.total_salary
FROM (
SELECT dept_id, SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id
) t
WHERE t.total_salary = (
SELECT MAX(total_salary)
FROM (
SELECT dept_id, SUM(salary) AS total_salary
FROM employees
GROUP BY dept_id
) x
);

Этот подход:

  • полезен, если нужно вернуть несколько отделов при одинаковой максимальной сумме;
  • демонстрирует владение подзапросами и агрегатами.
  1. Практические нюансы, которые стоит учитывать
  • Учитывать только активных сотрудников:
    WHERE status = 'active'
  • Включать отделы без сотрудников:
    • через LEFT JOIN с departments:
      SELECT
      d.id AS dept_id,
      d.name AS dept_name,
      COALESCE(SUM(e.salary), 0) AS total_salary
      FROM departments d
      LEFT JOIN employees e
      ON e.dept_id = d.id
      AND e.status = 'active'
      GROUP BY d.id, d.name
      ORDER BY total_salary DESC;
  • Индексы:
    • индекс по (dept_id) и (опционально) (dept_id, status) улучшит производительность агрегации.

Сильный ответ на интервью:

«Сначала агрегируем сумму зарплат по отделам через GROUP BY dept_id и SUM(salary). Затем либо сортируем по сумме и берём LIMIT 1, либо используем подзапрос: считаем суммы в подзапросе и выбираем отдел(ы) с максимальной суммой. При наличии таблицы departments лучше джойнить её, чтобы вернуть человекочитаемое имя отдела. Важно не группировать по зарплате и не мешать агрегаты с неагрегированными полями без группировки.»

Вопрос 23. Объясните, что делает предложенный фрагмент кода и к какому результату он приводит.

Таймкод: 01:28:19

Ответ собеседника: неполный. Корректно отслеживает выполнение цикла после исправления ошибки с декрементом, понимает, что цикл завершается и получается произведение чисел, но не формулирует, что это вычисление факториала, пока ему не подсказывают термин.

Правильный ответ:

Рассмотрим типичный фрагмент, который обычно дают в подобных задачах (обозначения могут немного отличаться, но суть одна):

n := 5
res := 1

for n > 0 {
res = res * n
n = n - 1
}

fmt.Println(res)

Пошаговое объяснение:

  1. Инициализация:

    • n — исходное число, для которого хотим посчитать результат.
    • res — аккумулятор, начальное значение 1 (нейтральный элемент для умножения).
  2. Цикл for n > 0:

    • пока n больше 0:
      • перемножаем текущее значение res на n;
      • уменьшаем n на 1 (декремент);
    • за счет декремента n цикл гарантированно завершится, когда n станет 0.
  3. Последовательность для n = 5:

    • старт: res = 1, n = 5
    • итерация 1: res = 1 * 5 = 5, n = 4
    • итерация 2: res = 5 * 4 = 20, n = 3
    • итерация 3: res = 20 * 3 = 60, n = 2
    • итерация 4: res = 60 * 2 = 120, n = 1
    • итерация 5: res = 120 * 1 = 120, n = 0
    • условие n > 0 ложно, цикл завершается.
    • на выводе: 120.
  4. Обобщение:

Код последовательно перемножает все целые числа от n до 1. Это и есть вычисление факториала числа n.

Факториал n (обозначается n!) — это:

  • n! = 1 * 2 * 3 * ... * n, при n >= 1
  • по определению 0! = 1

То есть приведенный фрагмент вычисляет:

  • для n = 5 → 5! = 120
  • для n = 3 → 3! = 6
  • для n = 1 → 1! = 1
  1. Важные моменты, на которые стоит обратить внимание на интервью:
  • Корректность условия останова:
    • используется n > 0 и в теле цикла есть декремент;
    • нет бесконечного цикла.
  • Инициализация аккумулятора:
    • именно 1, а не 0 (0 убил бы произведение).
  • Понимание семантики:
    • важно не только "умножает числа", но и явно распознать: это алгоритм вычисления факториала.
  1. Более идиоматичный пример на Go (как функция):
func Factorial(n int) int {
if n < 0 {
panic("negative input")
}

res := 1
for i := 2; i <= n; i++ {
res *= i
}
return res
}

Зрелый ответ в устной форме:

«Этот код инициализирует результат единицей и в цикле умножает его на текущее значение n, каждый раз уменьшая n на 1, пока n не станет 0. В итоге он возвращает произведение всех целых чисел от исходного n до 1 — то есть вычисляет факториал n.»