РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ Ручной тестировщик IT One / Газпромбанк - Middle 200+ тыс.
Сегодня мы разберем собеседование на позицию тестировщика в крупном банковском проекте, где кандидат демонстрирует уверенное владение ручным тестированием, участием в распиле монолита на микросервисы, подготовкой тест-кейсов под автоматизацию и практический опыт с API, нагрузочным тестированием и очередями. Беседа показывает, что он вдумчиво подходит к требованиям, умеет выстраивать процесс тестирования и коммуникацию в команде, но при этом испытывает затруднения в продвинутых SQL-задачах и местами излишне растекается в ответах, что важно учитывать при оценке его уровня.
Вопрос 1. Есть ли вопросы по описанию проекта, команды и условиям работы или можно переходить к обсуждению вашего опыта?
Таймкод: 00:05:29
Ответ собеседника: правильный. Кандидат подтверждает, что всё понятно, проявляет интерес к проекту и предлагает сразу перейти к обсуждению релевантного опыта.
Правильный ответ:
В данном случае "правильный" ответ — это не про знания технологий, а про поведение и зрелость кандидата.
Хорошая стратегия ответа:
- кратко подтвердить, что общее понимание есть;
- показать заинтересованность в продукте и домене;
- плавно перейти к релевантному опыту, связывая его с задачами команды;
- при необходимости оставить уточняющие вопросы на конец интервью или обозначить 1–2 действительно важных аспекта (без уводов в сторону бытовых условий).
Пример уместного ответа:
«Спасибо за описание, у меня в целом сложилось хорошее понимание. Проект выглядит интересным, особенно часть, связанная с [ключевой аспект: высоконагруженный backend, микросервисы, event-driven архитектура, интеграции и т.п.]. Мой опыт хорошо ложится на эти задачи: я занимался [кратко: разработкой высоконагруженных сервисов на Go, оптимизацией производительности, построением REST/gRPC API, работой с очередями, кэшем, распределёнными системами]. Давайте перейдём к обсуждению моего опыта, и по ходу или в конце я задам несколько уточняющих вопросов по архитектуре и процессам.»
Такой ответ показывает:
- понимание контекста;
- ориентацию на ценность для продукта;
- умение связывать свой опыт с задачами компании;
- приоритет обсуждения сущностных вещей (архитектура, процессы, ответственность), а не второстепенных.
Вопрос 2. В каком направлении вы хотите профессионально развиваться?
Таймкод: 00:10:25
Ответ собеседника: правильный. Кандидат говорит, что ему нравится тестирование; рассматривает развитие в сторону лидирования в области качества и автоматизации, при этом больше склоняется к автоматизации из-за её глубины и потенциала роста.
Правильный ответ:
Сильный ответ на этот вопрос должен показать осознанную траекторию развития, связанную с задачами компании, технологическим стеком и реальной ценностью для продукта. В контексте backend/Go-стека логично обозначить фокус на:
- углубление технической экспертизы;
- владение архитектурой и экосистемой вокруг Go;
- качество, надёжность и наблюдаемость сервисов;
- способность системно влиять на команду и продукт.
Пример развёрнутого ответа:
-
Техническая глубина в 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, метрики — это уже выход на уровень продакшен-готовых решений.
- Углубление в:
-
Архитектура и качество систем:
- Развитие в сторону проектирования:
- микросервисной архитектуры с чёткими контрактами;
- устойчивых к сбоям систем (ретраи, idempotency, дедупликация, circuit breaker);
- технологического ландшафта: очереди (Kafka/RabbitMQ/NATS), кэш (Redis), балансировка, конфигурация, секреты.
- Фокус на:
- code review как инструмент повышения качества;
- внедрение best practices: clean architecture, SOLID-принципы в контексте Go, четкое разделение домена и инфраструктуры;
- документирование решений и контрактов.
- Развитие в сторону проектирования:
-
Автоматизация качества и инженерные практики:
- Стремление не просто "писать тесты", а строить систему, где качество встроено:
- юнит-тесты, интеграционные, контрактные, нагрузочные;
- тестируемая архитектура (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, метрики дефектов).
- Стремление не просто "писать тесты", а строить систему, где качество встроено:
-
Наблюдаемость, надёжность и работа с данными:
- Развитие в сторону:
- логирования, трассировки (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)), - оценить нагрузку и оптимизировать запрос.
- Развитие в сторону:
-
Влияние на команду и процессы:
- Участие в:
- формировании технических стандартов;
- менторстве и ревью кода;
- выборе инструментов и архитектурных подходов.
- Цель — уметь не только делать самому, но и масштабировать практики качества и инженерной культуры на команду.
- Участие в:
Хороший ответ соединяет всё это в одну логичную линию:
- "Я хочу развиваться в сторону глубокой технической экспертизы в Go и построения надёжных, тестируемых, наблюдаемых backend-систем. Мне важно не только писать код, но и выстраивать архитектуру, автоматизировать качество, влиять на инженерные практики команды и обеспечивать предсказуемость и стабильность продукта под реальной нагрузкой."
Вопрос 3. Каковы ваши ожидания от нового места работы и какие условия для вас критичны?
Таймкод: 00:11:16
Ответ собеседника: правильный. Кандидат ценит понятные процессы и ожидания, нормальную организацию работы, здоровую атмосферу в команде; спокойно относится к рабочим задачам и нагрузке, не предъявляет завышенных требований.
Правильный ответ:
Сильный ответ на этот вопрос должен показать зрелость, адекватность ожиданий и ориентацию на долгосрочное сотрудничество и результат. Важно связать ожидания с эффективной работой и качеством продукта, а не с бытовыми мелочами.
Ключевые моменты, которые стоит обозначить:
-
Прозрачность целей и ожидаемого результата
- Ожидание:
- четко сформулированных задач и зон ответственности;
- понятных критериев успеха: что считается хорошо сделанной задачей или успешным релизом;
- адекватного приоритезирования (не когда все задачи «горят» всегда).
- Зачем:
- это позволяет принимать самостоятельные решения, оптимизировать архитектуру и не тратить ресурсы на хаотичные переделки.
- Ожидание:
-
Зрелые процессы разработки
- Ожидание базового инженерного фундамента:
- система контроля версий с понятным workflow (git-flow, trunk-based);
- code review как реальный инструмент качества, а не формальность;
- CI/CD для автоматической сборки, тестирования и деплоя;
- наличие базовой автоматизации: линтеры, тесты, статический анализ.
- Это снижает количество инцидентов, ускоряет поставку и позволяет сосредоточиться на сложных задачах, а не на ручной рутине.
- Ожидание базового инженерного фундамента:
-
Техническая адекватность и возможность делать хорошо
- Ожидание:
- возможность влиять на архитектурные решения и технический долг;
- готовность команды и компании обсуждать и внедрять best practices (наблюдаемость, логирование, тестирование, отказоустойчивость).
- Критично:
- отсутствие культуры “костыль ради костыля” и постоянного игнорирования последствий.
- Ожидание:
-
Коммуникация и командная культура
- Ожидание:
- уважительная, конструктивная коммуникация без токсичности и личных наездов;
- нормальная реакция на ошибки: разбор и улучшение процессов вместо поиска виноватых;
- готовность делиться знаниями, помогать коллегам, проводить технические обсуждения.
- Это важно, потому что сложные системы требуют сотрудничества, а не политических игр.
- Ожидание:
-
Отношение к нагрузке и срочности
- Адекватная позиция:
- нормальное отношение к пиковой нагрузке, дедлайнам и инцидентам, если это исключения, а не образ жизни;
- готовность включаться в сложные задачи и продакшн-инциденты, если за этим следует анализ причин и улучшение системы.
- Критично:
- отсутствие постоянного "вечного продакшен-пожара" и 24/7 хаоса без попыток системно исправить ситуацию.
- Адекватная позиция:
-
Возможности развития
- Не как "приятный бонус", а как инструмент роста ценности для компании:
- доступ к сложным техническим задачам;
- возможность участвовать в архитектурных решениях;
- обмен опытом внутри команды (митапы, ревью, парное программирование).
- Не как "приятный бонус", а как инструмент роста ценности для компании:
Пример уместного ответа:
«Для меня важно, чтобы в компании были понятные ожидания: зона ответственности, цели команды, критерии качества. Нравится работать там, где есть базовые инженерные практики: 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
Ответ собеседника: правильный. Кандидат выделяет: проверить соответствие бизнес-требованиям и убедиться, что система соответствует ожиданиям пользователя, отмечая возможное расхождение между ними.
Правильный ответ:
Основные цели тестирования гораздо шире, чем просто поиск багов или проверка формального соответствия требованиям. В зрелом инженерном подходе тестирование — это инструмент управления рисками и доказательства того, что система делает то, что должна, в реальных условиях эксплуатации.
Ключевые цели:
- Подтверждение соответствия бизнес-целям и требованиям
- Проверить, что реализованный функционал:
- соответствует спецификациям, user stories, acceptance criteria;
- корректно реализует бизнес-правила, расчеты, ограничения;
- не искажает финансовые, юридические и операционные процессы.
- Важно не только «passed/failed», а:
- выявить неоднозначности требований;
- подсветить расхождения между формальными требованиями и реальными сценариями бизнеса.
- Подтверждение ценности и ожидаемого поведения для пользователя
- Даже если функционал формально соответствует ТЗ, он может быть:
- неудобным, непредсказуемым, контринтуитивным;
- технически корректным, но не решающим задачу пользователя.
- Тестирование должно:
- моделировать реальные сценарии использования;
- выявлять UX-проблемы, пограничные кейсы, неожиданные потоки.
- Управление рисками и предотвращение регрессии
- Одна из главных целей — уменьшить вероятность критичных отказов:
- финансовые потери;
- простои системы;
- утечки данных;
- нарушение регуляторных требований (GDPR, 152-ФЗ, PCI DSS и др.).
- Тестирование должно быть риск-ориентированным:
- больше глубины и сценариев там, где высокий impact;
- разумный баланс покрытия — не максимальное количество тестов, а максимальный эффект на снижение рисков.
- Регрессионное тестирование:
- подтвердить, что новые изменения не сломали существующий функционал;
- автоматизированные тесты на критические бизнес-потоки.
- Обеспечение надежности, производительности и устойчивости
Тестирование не ограничивается корректностью «при нормальном вводе»:
- Производительность:
- система выдерживает ожидаемую и пиковую нагрузку;
- латентность и 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 по времени ответа.
- Повышение качества архитектуры и процессов разработки
Тестирование — сигнал не только о багах, но и о качестве архитектуры:
- Если модуль трудно тестировать:
- слишком много связей, нет интерфейсов, логика размазана — это индикатор плохого дизайна.
- Хорошая цель тестирования:
- стимулировать модульность, явные контракты, предсказуемое поведение;
- встроить качество в архитектуру, а не пытаться «натестировать» поверх.
Пример: использование интерфейсов для тестируемости:
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")
}
}
Цель тестирования здесь — не просто проверить условие, а зафиксировать контракт поведения и упростить эволюцию кода.
- Документирование и создание доверия
- Тесты выполняют роль "живой документации":
- показывают как использовать API/модуль;
- фиксируют принятые допущения и граничные условия.
- Наличие хороших тестов:
- формирует доверие к системе;
- позволяет быстрее вносить изменения, не боясь поломать критичный функционал.
Сводя воедино:
Основные цели тестирования:
- снизить риски для бизнеса и пользователей;
- подтвердить соответствие требованиям и реальным сценариям использования;
- обеспечить надежность, производительность и предсказуемость;
- повысить качество архитектуры и процессов разработки;
- создать основу для безопасных и быстрых изменений.
Такой подход показывает не «тестирование как чек-лист», а тестирование как часть инженерной ответственности за поведение системы в продакшене.
Вопрос 7. В чём разница между функциональным и нефункциональным тестированием?
Таймкод: 00:14:58
Ответ собеседника: правильный. Кандидат говорит, что функциональное тестирование проверяет соответствие функционала требованиям, а нефункциональное — характеристики системы: безопасность, производительность, нагрузка, UX/UI, доступность и другие аспекты.
Правильный ответ:
Формулировка верная, но для полноты важно показать более структурированное понимание, связанное с реальными системами и архитектурой.
Функциональное и нефункциональное тестирование отвечают на разные классы вопросов:
- Функциональное: «Система делает то, что должна делать?»
- Нефункциональное: «Насколько хорошо, быстро, безопасно и надёжно система это делает в реальных условиях?»
Разберём по сути и с практическими примерами.
Функциональное тестирование
Цель: проверить, что система корректно реализует заявленный бизнес-функционал и требования.
Основные характеристики:
- Основывается на:
- спецификациях,
- user stories,
- acceptance criteria,
- бизнес-правилах.
- Проверяет:
- корректность ответов API;
- целостность и правильность данных;
- бизнес-валидации;
- сценарии успеха и отказа.
Примеры для backend/Go:
- Тест 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")
}
}
- Тест бизнес-правила (например, нельзя создать пользователя с уже существующим 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")
}
}
Суть: нас интересует "что именно происходит" — правильные статусы, корректные ответы, верная логика.
Нефункциональное тестирование
Цель: проверить характеристики качества системы. Даже идеально реализованный функционал бесполезен, если система:
- не выдерживает нагрузку,
- небезопасна,
- нестабильна под отказами,
- неудобна и непредсказуема.
Типичные виды нефункционального тестирования:
-
Производительность и нагрузка (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),
- как меняется время выполнения при росте таблицы до миллионов записей.
- Вопросы:
-
Надёжность, устойчивость, отказоустойчивость
- Проверка поведения при:
- падении одной из служб;
- недоступности внешнего 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 {
// ожидание: при превышении таймаута будет ошибка контекста
} - Проверка поведения при:
-
Безопасность
- Проверка:
- контроля доступа (role-based, permissions),
- правильной обработки токенов и сессий,
- шифрования чувствительных данных,
- защиты от типичных атак (SQL injection, XSS, CSRF, brute-force, insecure direct object reference).
- Примеры:
- запросы только от авторизованных пользователей;
- отсутствие прямой передачи паролей/секретов в логах.
- Проверка:
-
Юзабилити, UX, доступность
- Для пользовательских интерфейсов:
- насколько интерфейс понятен и предсказуем;
- соответствие ожиданиям домена и целевой аудитории;
- доступность для людей с ограниченными возможностями.
- В backend-контексте:
- консистентные коды ответов;
- предсказуемая схема API;
- хорошие сообщения об ошибках.
- Для пользовательских интерфейсов:
-
Масштабируемость
- Как система ведёт себя при росте:
- пользователей,
- данных,
- числа интеграций.
- Вопрос: можем ли горизонтально масштабировать Go-сервисы, БД, кэши, очереди без архитектурного переписывания.
- Как система ведёт себя при росте:
-
Поддерживаемость и операбельность
- Логирование, метрики, трассировка:
- удобно ли диагностировать проблемы;
- есть ли понятные алерты, дашборды.
- Это тоже нефункциональное требование: не влияет напрямую на бизнес-функции, но критично для эксплуатации.
- Логирование, метрики, трассировка:
Ключевое различие
- Функциональное тестирование:
- отвечает на вопрос: «Система правильно делает нужные операции?»
- ориентируется на «что должно происходить» согласно требованиям.
- Нефункциональное тестирование:
- отвечает на вопрос: «Насколько качественно система это делает в реальных и граничных условиях?»
- покрывает время отклика, устойчивость, безопасность, масштабируемость, удобство, наблюдаемость.
Важно понимать: обе группы одинаково критичны. Система, которая:
- идеально реализует бизнес-логику, но падает при нагрузке — провал;
- быстрый и красивый сервис, но с неправильной логикой списания денег — тоже провал.
Зрелый инженер рассматривает функциональные и нефункциональные требования как части единого контракта качества системы.
Вопрос 8. С какими видами нефункционального тестирования вы работали на практике?
Таймкод: 00:16:32
Ответ собеседника: неполный. Кандидат упоминает нагрузочное тестирование (анализ результатов, поиск узких мест во frontend и БД) и проверку прав доступа и ролей через токены, включая ошибки в разграничении доступа.
Правильный ответ:
Хороший ответ на этот вопрос должен:
- чётко перечислять виды нефункционального тестирования;
- опираться на реальные практики: что именно проверяли, какими инструментами, как интерпретировали результаты;
- показывать понимание связки: профиль нагрузки → архитектура → БД → кэш → сеть → ограничения языка/рантайма (в т.ч. Go);
- демонстрировать умение влиять на улучшение системы, а не просто запускать тесты.
Ниже вариант ответа, который показал бы глубокую экспертизу.
Основные виды нефункционального тестирования, с которыми разумно иметь опыт:
- Нагрузочное и стресс-тестирование (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)
- Тестирование устойчивости и отказоустойчивости (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) {
// ожидаемое поведение: логируем, считаем метрику, пробуем ретрай, если бизнес-логика позволяет
}
- Тестирование безопасности (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 не раскрывает чужие данные.
- Тестирование производительности отдельных компонент (Profiling / Micro-benchmarks)
Цель: локализовать горячие точки и неэффективный код.
Для Go:
- использование:
pprof(CPU, memory, allocs),testing.Bдля бенчмарков;
- примеры:
- сравнение разных алгоритмов парсинга/сериализации;
- поиск лишних аллокаций или блокировок.
Пример бенчмарка:
func BenchmarkProcess(b *testing.B) {
for i := 0; i < b.N; i++ {
Process(data)
}
}
- Тестирование наблюдаемости и операбельности (Observability)
Цель: проверить, насколько система пригодна к эксплуатации:
- есть ли:
- структурированные логи,
- метрики (Prometheus),
- трассировки (OpenTelemetry),
- полезные алерты;
- можно ли:
- быстро диагностировать инцидент,
- отследить цепочку вызовов между сервисами,
- оценить влияние изменений.
Практика:
- проверка полноты логирования ключевых операций;
- тесты, подтверждающие наличие метрик по:
- латентности,
- ошибкам,
- количеству запросов;
- в нагрузочных и отказоустойчивых тестах анализируются не только ответы, но и метрики/логи.
- Юзабилити и доступность (если релевантно)
Для backend-ориентированного ответа можно ограничиться:
- предсказуемость API:
- консистентные коды ошибок;
- понятные сообщения (machine-friendly + человекочитаемые);
- стабильность контрактов (backward compatibility), версия API.
Как собрать сильный ответ:
«На практике я работал с несколькими видами нефункционального тестирования.
Во-первых, нагрузочное и стресс-тестирование: моделировал реальные сценарии нагрузки, анализировал p95/p99, выявлял узкие места в базе данных и конфигурации пулов соединений, помогал оптимизировать запросы и структуру индексов.
Во-вторых, тестирование безопасности: проверял корректность разграничения доступа на уровне ролей и токенов, сценарии доступа к данным других пользователей, использование параметризованных запросов, отсутствие утечек технических деталей в ошибках.
Кроме того, участвовал в проверке устойчивости системы: симуляция недоступности внешних сервисов и БД, проверка корректной обработки таймаутов и ретраев, мониторинг поведения под частичными отказами.
Отдельно обращал внимание на наблюдаемость: проверка, что ключевые операции покрыты логами и метриками, чтобы по результатам нагрузочных и инцидентных сценариев можно было быстро локализовать проблему.
Фокус всегда на том, чтобы не просто “прогнать тесты”, а получить осмысленные сигналы о рисках для производительности, безопасности и надежности системы в продакшене.»
Вопрос 9. Какие артефакты тестирования вы используете и как у вас организован процесс документирования на проекте?
Таймкод: 00:18:19
Ответ собеседника: неполный. Кандидат описывает участие на этапе ревью требований: проверяет документацию на противоречия и ошибки, добивается уточнений, участвует в груминге перед разработкой, но не раскрывает полноту артефактов и системность процесса документирования.
Правильный ответ:
Сильный ответ должен показать, что тестирование интегрировано в жизненный цикл разработки, а артефакты не живут "отдельно", а служат:
- управлению рисками,
- прозрачности ожиданий,
- воспроизводимости тестирования,
- ускорению онбординга,
- поддержке долговременной эволюции системы.
Ниже структурированный ответ с примерами артефактов и организации документации.
Основные артефакты тестирования
- Анализ и спецификации требований
Это фундамент. До написания тестов нужно стабилизировать то, что считаем "истиной":
- Product Requirements / BRD / User Stories
- Use Cases, диаграммы последовательностей, модели данных
- Acceptance Criteria
Роль тестирования:
- выявление противоречий, пробелов, копипасты, неявных правил;
- формализация граничных случаев и негативных сценариев;
- предложение более точных формулировок, которые можно проверить.
Результат:
- согласованный baseline, на основе которого строятся сценарии, автотесты и контракты.
- 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 в репо), который реально используется.
- Test Cases / Checklists / Scenarios
- Детализированные тест-кейсы уместны:
- для критичных, регуляторных, финансовых, юридически значимых сценариев.
- Чек-листы и сценарии:
- для регрессии,
- для smoke-тестов,
- для быстрого прогона основных потоков.
Хорошая практика — привязка к требованиям/историям:
- TС-123 связан с US-45 и API-эндпоинтом /v1/payments.
- Позволяет видеть покрытие: какие требования формально проверены.
Пример компактного чек-листа для API (Markdown в репозитории):
- POST /users:
- Успешное создание с валидными данными
- Дубликат email → 409 / business error
- Невалидный email → 400
- Нет обязательного поля → 400
- Авторизация обязательна → 401
Такой чек-лист легко конвертируется в автотесты.
- Автоматизированные тесты как ключевой артефакт
Код автотестов — один из самых ценных и честных артефактов. В современных проектах он часто важнее формальных документов.
Типы:
-
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)
}
})
}
}
Этот тест:
- документирует правило скидки;
- защищает от регрессий при изменении логики.
- API спецификации и контрактная документация
Критичный артефакт для backend-экосистем:
- OpenAPI/Swagger для REST;
- Protobuf для gRPC;
- схемы событий для event-driven систем (Kafka/NATS).
Тестирование использует их как:
- источник истины для генерации автотестов;
- базу для контрактного тестирования (consumer-driven, backward compatibility).
Пример использования: при изменении API:
- автотесты валидируют, что старые контракты не сломаны;
- документация обновляется вместе с кодом (генерация из исходников).
- Bug Reports / Defect Reports
Это не просто "тикеты с багом", а:
- фиксация проблемы:
- шаги воспроизведения,
- ожидаемый/фактический результат,
- окружение,
- логи/метрики/скриншоты;
- указание области риска;
- основа для регрессионных тестов.
Зрелый подход:
- по критичным багам:
- добавляем автотест или чек-лист;
- фиксируем причину (root cause) и меры предотвращения.
- Regression Suites и Smoke/Сanary-наборы
Отдельные артефакты:
- список критических сценариев, которые должны быть проверены:
- перед релизом;
- после деплоя;
- после изменения ключевых модулей.
- часть — в виде автотестов;
- часть — минимальный manual smoke, если автотесты не покрывают.
- Документация по средам, данным и тестовой инфраструктуре
Важно зафиксировать:
- какие стенды есть (dev/test/stage/preprod/prod);
- как поднимать локальное окружение:
- docker-compose, k8s-манифесты;
- какие тестовые данные используются:
- анонимизированные,
- фабрики/сидеры для автотестов.
Это уменьшает хаос и делает тесты воспроизводимыми.
- Организация документирования
Как это всё системно организовать:
-
Единое место хранения:
- 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, согласование с разработчиками и соседними командами, фиксирование результатов в системе управления тестами.
Правильный ответ:
Ниже — расширенный, структурированный ответ, отражающий зрелый процесс и практики, применимые в сложных продуктах и распределённых системах.
Точка зрения: артефакты тестирования — это не «бумага ради процесса», а системный способ:
- связать требования, риски и реализацию;
- сделать качество воспроизводимым;
- ускорить изменения, а не тормозить их.
Основные артефакты и как они встраиваются в цикл разработки
- Анализ требований и спецификаций (входной артефакт)
Что фиксируем:
- уточнённые требования (user stories, acceptance criteria);
- выявленные противоречия и решения по ним;
- граничные случаи и бизнес-ограничения;
- зависимости между сервисами и командами.
Практика:
- участвовать в грумингах и технических обсуждениях;
- оставлять комментарии к требованиям (Confluence/Notion/Jira) до начала разработки;
- формализовать критичные моменты так, чтобы по ним можно было строить тесты и автотесты.
Результат:
- требования становятся тестируемыми, понятными для dev/QA/analyst;
- меньше «серых зон» и расхождений интерпретаций.
- 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")
}
}
- Чек-листы
Используются:
- для быстрых, временных или одноразовых проверок;
- для smoke-тестов, sanity-проверок после деплоя;
- для областей, где формальный детальный test case избыточен.
Чек-листы храним:
- рядом с кодом (Markdown в репо);
- либо в Wiki/системе тест-менеджмента, привязанной к релизам.
Плюсы:
- легко поддерживать;
- быстро прогонять;
- удобно использовать как основу для регрессионных наборов.
- Регрессионные наборы и тест-планы
Регрессионный набор — это структурированный набор сценариев, покрывающий:
- критические бизнес-потоки;
- интеграции между сервисами;
- ранее найденные дефекты (которые не должны повториться);
- высокорисковые зоны (платежи, авторизация, расчёты, отчеты и т.д.).
Организация:
- часть сценариев реализована как автотесты (unit, integration, API, E2E);
- часть — как чек-листы для ручного прогона в специфичных кейсах;
- тест-план релиза: какой поднабор регрессии гоняем в зависимости от изменений (risk-based regression).
- Impact analysis (анализ влияния изменений)
Ключевой элемент зрелого процесса:
- при каждой фиче/изменении определяем:
- какие модули, сервисы, API, таблицы БД затронуты;
- какие зависящие компоненты и команды должны быть уведомлены;
- какие тесты нужно:
- обновить;
- добавить;
- прогнать из регрессии.
Результат:
- нет избыточной полной регрессии «на всякий случай»;
- нет слепых зон, потому что связь "изменение → риск → тесты" явная.
- Автоматизированные тесты как центральный артефакт
Автотесты — часть кода продукта, живут в репозитории, проходят 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 для каждого коммита.
- Документация API и контрактов
Артефакты:
- OpenAPI/Swagger спеки;
- Protobuf-схемы;
- схемы событий для Kafka/NATS.
Сильная практика:
- спек лежит в репозитории;
- изменения проходят через ревью;
- по спекам:
- генерируется клиентский код;
- строятся автотесты;
- валидируется совместимость между версиями.
- Отчеты, статус и трассируемость
Важно не просто "прогнать тесты", а:
- иметь трассировку:
- Требование → Тест-кейс → Автотест → Результат;
- видеть покрытие критичных фич;
- фиксировать результаты в системе:
- TestRail, Zephyr, qTest, Xray или свои решения;
- интеграция с CI: результаты автотестов подтягиваются автоматически.
По критичным багам:
- заводим дефекты с полным описанием;
- связываем с требованиями;
- добавляем тесты, чтобы ошибка не повторилась.
- Хранение, обновление и "живость" документации
Ключевые принципы:
- Единый источник правды:
- технические артефакты (тесты, 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) и архитектурой.
Ниже — кратко по ключевым техникам и их применению в реальных проектах.
Техники, которые стоит уверенно использовать и уметь объяснить
- Классы эквивалентности (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)
}
})
}
}
- Анализ граничных значений (Boundary Value Analysis)
Идея:
- Ошибки чаще всего на границах диапазонов.
- Тестируем значения: min-1, min, min+1, max-1, max, max+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)
}
})
}
}
Таблица принятия решений прямо превращается в табличные тесты.
- Диаграммы состояний и переходов (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;
Тесты должны:
- подтвердить, что все разрешенные переходы работают;
- попытка сделать запрещенный — приводит к контролируемой ошибке.
- Комбинационное тестирование (Pairwise / N-wise)
Идея:
- При большом количестве параметров все комбинации (full) взорвают количество тестов.
- Pairwise/N-wise позволяет покрыть все пары/тройки значений параметров минимальным набором тестов.
Применение:
- конфигурации фичей;
- параметры запросов;
- сочетание устройств/браузеров/настроек.
На собеседовании достаточно:
- показать, что знаете о комбинировании входов, не перебираете всё в лоб.
- Причина-следствие (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 для отлова типичных ошибок в условиях и объясняет логику выбора.
Правильный ответ:
Задача — показать грамотное применение анализа граничных значений и классов эквивалентности, не сводя ответ к механическому перечислению чисел.
- Базовая логика
Условие: допустимый возраст — от 18 до 50 лет включительно.
- Нижняя граница:
- 17 — должно быть отклонено;
- 18 — должно быть принято.
- Верхняя граница:
- 50 — должно быть принято;
- 51 — должно быть отклонено.
Минимально достаточный набор граничных значений:
- 17 (минимум - 1, invalid)
- 18 (минимум, valid)
- 50 (максимум, valid)
- 51 (максимум + 1, invalid)
Это классический анализ граничных значений.
- Классы эквивалентности
Выделяем классы:
- Младше 18:
- пример: 0, 17 — всегда invalid.
- От 18 до 50:
- пример: 18, 25, 50 — valid.
- Старше 50:
- пример: 51, 99 — invalid.
Для повышения надежности можно взять:
- по одному значению из каждого класса (например, 10, 30, 60),
- плюс граничные значения (17, 18, 50, 51).
- Почему расширенный набор (17, 18, 19, 49, 50, 51) имеет смысл
Расширенный набор помогает ловить типичные ошибки реализации:
- Перепутанные операторы:
>вместо>=или<вместо<=;
- Ошибки в "зеркальной" логике:
- допустили 17 или не допустили 50;
- Неправильное объединение условий.
Например, если условие реализовано как:
func IsAgeAllowed(age int) bool {
return age > 18 && age < 50
}
Ошибки:
- 18 (граничное) будет отклонено;
- 50 (граничное) будет отклонено.
Тесты с 18 и 50 сразу это покажут. Тесты с 19 и 49 дополнительно подтверждают корректное поведение внутри диапазона.
- Пример корректной реализации и тестов на 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)
}
}
}
Такой набор:
- покрывает ключевые границы,
- проверяет корректность диапазона,
- помогает отлавливать логические ошибки в условиях.
- Сильный ответ в формате интервью
«При диапазоне 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:
- Явный контракт
API — это не просто набор урлов или функций, а строгий контракт, который:
- стабилен и предсказуем;
- документирован (OpenAPI/Swagger для REST, Protobuf для gRPC, схемы для событий);
- отделяет внутреннюю реализацию от внешнего поведения.
Важно:
- внутренний код может меняться;
- контракт API должен оставаться обратимо совместимым или версионироваться.
- Инкапсуляция и слабая связность
API скрывает детали реализации:
- потребителю не важно:
- какая БД внутри,
- какой язык или фреймворк,
- какая внутренняя структура таблиц;
- важны только:
- входы,
- выходы,
- семантика операций.
Это позволяет:
- менять реализацию без поломки клиентов;
- строить микросервисную архитектуру, где сервисы общаются по контрактам, а не по «общей базе данных» или шэрингу внутренних структур.
- Типы API в реальных системах
- Внутренние API:
- между микросервисами (REST, gRPC, message-based);
- Публичные API:
- для внешних интеграций (партнёры, мобильные клиенты);
- Библиотечные API:
- функции/методы пакетов, SDK, драйверов;
- Event-driven/API через сообщения:
- контракты сообщений в Kafka/NATS/RabbitMQ.
- API как основа тестирования и качества
Для инженерного подхода важно:
- API — главный объект контрактного тестирования:
- мы тестируем не только «работает ли код», а «устойчив ли контракт»;
- через API определяются:
- SLA,
- требования к производительности и надёжности,
- правила совместимости при релизах (backward/forward compatible).
- Пример: простой 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 проверяет соблюдение контракта.
- Пример: 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
- Postman
Использование:
- ручная проверка эндпоинтов:
- positive/negative сценарии,
- авторизация (Bearer, OAuth2, API keys),
- проверка заголовков, кодов ответов, тела;
- коллекции запросов:
- привязка к фичам/микросервисам;
- запуск в разных окружениях (dev/stage/prod);
- скрипты:
- pre-request scripts (генерация токенов, подготовка данных);
- tests (проверка статус-кодов, схемы ответа, бизнес-условий).
Усиление:
- Newman (CLI) для запуска Postman-коллекций в CI:
- smoke/regression по API на каждом билде;
- базовая автоматизация без сложной инфраструктуры.
- 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
- Altair / GraphiQL / Apollo Sandbox
Использование:
- интерактивный конструктор запросов и мутаций;
- автодополнение из схемы;
- просмотр документации (schema introspection);
- быстрое прототипирование и проверка сложных запросов.
Практика:
- проверка:
- корректности схемы и типов;
- работы фильтров, пагинации, вложенных полей;
- обработки разрешений (какие поля доступны для разных ролей).
Важный момент: при тестировании GraphQL уделяется внимание:
- ограничению глубины запросов;
- лимитам (rate limiting, complexity);
- корректным ошибкам (GraphQL errors, extensions).
Автоматизированное тестирование API на Go
Инструменты и подходы, которые стоит выделить, чтобы показать уровень:
- Стандартная библиотека 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 при каждом коммите;
- живут рядом с кодом и обновляются синхронно.
- Контрактное тестирование
Инструменты/подходы:
- OpenAPI/Swagger:
- валидация ответов по схеме;
- генерация клиентов/серверов;
- Dredd, Schemathesis — проверка соответствия реализации спецификации;
- для gRPC — Protobuf + автогенерация клиентов, тесты поверх контрактов.
Цель:
- гарантировать, что изменения в API не ломают потребителей;
- контролировать backward compatibility.
- Инструменты для нагрузочного тестирования 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:
- функциональная проверка + нефункциональные характеристики.
- Безопасность и вспомогательные инструменты
- 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.
Ключевые моменты:
- Семантика 4xx против 5xx
- 4xx (включая 400):
- «Клиент прислал неправильный запрос, даже если повторить его без изменений — он останется некорректным».
- 5xx:
- «Запрос был валидным, но проблема на стороне сервера/инфраструктуры».
Ошибка в выборе кода статуса:
- использовать 400, когда на самом деле произошёл баг, паника, ошибка БД, таймаут — плохая практика:
- это скрывает ответственность сервера;
- затрудняет мониторинг и инцидент-менеджмент;
- ломает корректную обработку ошибок на стороне клиента.
- Когда уместно 400 vs более специфичные коды
- 400 — общее "Bad Request", часто используется для синтаксических/форматных проблем.
- Более точные варианты:
- 401 Unauthorized — нет/неверная аутентификация;
- 403 Forbidden — аутентификация есть, но нет прав;
- 404 Not Found — ресурс не найден;
- 409 Conflict — конфликт состояния (например, дубликат);
- 422 Unprocessable Entity — семантически некорректные данные (формат ок, но бизнес-валидация не прошла).
Хороший дизайн API:
- различает синтаксические ошибки (400) и бизнес-ошибки/конфликты (422/409 и т.п.);
- помогает клиентам корректно реагировать на ошибки.
- Пример реализации в 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 или в системе.
- Практический вывод
Грамотное использование 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).
Цель вопроса: проверить, сможете ли вы:
- выделить входные параметры;
- сформировать классы эквивалентности;
- найти критические точки смены направления;
- посчитать минимальный набор проверок, который гарантированно валидирует алгоритм.
Разберём абстрактно и затем сведём к ответу.
- Выделяем параметры и поведение
Минимальный набор параметров:
- день недели (7 значений);
- время (непрерывный диапазон, но нас интересуют ключевые точки);
- направление движения стрелок (результат алгоритма).
Ключевые риски:
- неправильное направление в обычное время;
- неверное переключение направления в момент смены дня;
- off-by-one-ошибки вокруг границ (до/после полуночи).
- Применяем классы эквивалентности
День недели:
- каждый день — потенциально отдельный класс, если для него может действовать своё правило;
- но если правило одинаковое для группы дней (например, Пн–Пт одно, Сб–Вс другое), эту группу можно тестировать репрезентативно (по одному дню от группы).
Время:
- внутри дня:
- интервал «обычного» времени, где правило стабильно (класс эквивалентности);
- границы смены (обычно около 00:00 или других специальных точек).
- Применяем анализ граничных значений и переходов
Если алгоритм привязан к смене дня (00:00), критичные точки:
- "чуть до" смены дня;
- "ровно в момент" смены;
- "чуть после" смены.
Например, если:
- Пн — направление A,
- Вт — направление B,
то проверяем:
- Пн, 23:59 — A;
- Вт, 00:00 — B (или A, если по условию смена сдвинута — зависит от требований);
- Вт, 00:01 — B.
- Как считать минимальный достаточный набор
Общий принцип для такого рода задач (и то, что обычно ожидают услышать на собеседовании):
- Не нужно тестировать все возможные комбинации времени.
- Нужно покрыть:
- по одному «обычному» моменту времени для каждой уникальной группы дней с одинаковым правилом;
- все границы перехода между разными правилами:
- для каждой пары дней, где меняется правило направления,
достаточно 2–3 проверки вокруг границы:
- до границы,
- в момент (если релевантно),
- после границы.
- для каждой пары дней, где меняется правило направления,
достаточно 2–3 проверки вокруг границы:
Пример (конкретизация для демонстрации подхода):
Пусть:
- Пн–Пт — стрелки по часовой,
- Сб–Вс — стрелки против часовой,
- правило меняется в 00:00 соответствующего дня.
Минимальный набор:
- Проверка обычного поведения в каждой группе:
- Пн, 12:00 — по часовой (представитель Пн–Пт);
- Сб, 12:00 — против часовой (представитель Сб–Вс).
- Проверка границ переходов:
- Пт 23:59 — по часовой;
- Сб 00:00 (или 00:01) — против часовой;
- Вс 23:59 — против часовой;
- Пн 00:00 (или 00:01) — по часовой.
Итого:
- 2 проверки для обычного времени,
- по 2 проверки для каждой границы перехода (до/после),
- всего 6–8 проверок (в зависимости от того, считаем ли "ровно в 00:00" отдельно).
Это уже значительно лучше, чем наивные 7 или 14 проверок "по одному на день" без анализа переходов.
- Что важно показать на интервью
Ключ не в конкретном числе, а в аргументации:
- Вы:
- выделяете правила;
- группируете дни с одинаковым поведением;
- применяете классы эквивалентности;
- тестируете только репрезентативные значения;
- отдельно покрываете границы переходов (до/после).
Хороший ответ мог бы звучать так:
«Минимальный набор не равен просто 7 тестам по количеству дней. Я бы сделал так: сгруппировал дни по одинаковым правилам, для каждой группы проверил одно "обычное" время внутри дня, а затем добавил проверки на границах перехода — непосредственно до смены дня и после неё для тех пар дней, где меняется направление. В итоге это даёт небольшой набор из 6–8 тестов, который покрывает и корректность правил, и off-by-one-ошибки на границах, без полного перебора всех дней и времён.»
Такой подход демонстрирует владение тест-дизайном и умение считать минимальный, но достаточный набор проверок.
Вопрос 18. Составьте чек-лист для проверки нового поля выбора руководителя в карточке сотрудника на основе заданных требований.
Таймкод: 00:47:56
Ответ собеседника: неполный. Кандидат перечисляет базовые проверки (тип поля, раскрытие списка, обязательность, сортировка, одиночный выбор, отсутствие поиска, фильтрация по признаку руководителя с проверкой через БД), но не полностью покрывает негативные, граничные, UX/данные кейсы и местами делает незафиксированные допущения.
Правильный ответ:
Ниже пример чек-листа, оформленный как рабочий артефакт: системно, без лишней бюрократии, с учётом функциональных, граничных и интеграционных аспектов. Предполагаемые требования (под них строим проверки):
- Поле "Руководитель":
- реализовано как выпадающий список;
- обязательное для заполнения;
- допускает выбор только одного руководителя;
- заполняется только пользователями с признаком "может быть руководителем" (флаг в системе/БД);
- список отсортирован по алфавиту (по ФИО или по регламентированному формату);
- поле без строки поиска (если явно указано);
- выбор сохраняется в карточке и корректно отображается везде, где используется.
Если в реальных условиях требования иные — чек-лист адаптируется, но логика остаётся.
Чек-лист проверок
- Отображение и базовое поведение
- Поле "Руководитель" видно в карточке сотрудника в соответствии с требованиями:
- в нужном разделе;
- с корректной подписью/лейблом.
- Поле по умолчанию (при создании нового сотрудника):
- пусто или предзаполнено согласно бизнес-правилу (если такое есть);
- состояние по умолчанию явно согласовано.
- Клик по полю раскрывает выпадающий список.
- Закрытие списка:
- по выбору значения;
- по клику вне списка;
- по Esc (если заявлено UX-стандартом продукта).
- Тип и режим выбора
- Можно выбрать только одно значение:
- повторный клик по другому элементу меняет выбор;
- нет возможности множественного выбора (Ctrl, Shift, чекбоксы и т.п. не работают).
- Выбранное значение отображается в свернутом поле корректно (ФИО/идентификатор по требованиям).
- Обязательность поля
- Попытка сохранить карточку сотрудника без выбранного руководителя:
- сохранение блокируется;
- отображается понятное сообщение об ошибке рядом с полем/общей областью ошибок;
- текст ошибки соответствует требованиям (например: «Выберите руководителя»).
- После выбора руководителя и повторной попытки сохранения:
- ошибка исчезает;
- карточка успешно сохраняется.
- Состав и фильтрация значений
- В выпадающем списке отображаются только сотрудники, имеющие флаг "руководитель" (или соответствующую роль/признак).
- В списке отсутствуют:
- сам текущий сотрудник (нельзя выбрать себя в качестве руководителя, если таково требование — часто логично и стоит уточнить);
- пользователи, помеченные как уволенные/заблокированные/неактивные, если по требованиям они не могут быть руководителями.
- Граничный кейс: если в системе только один возможный руководитель:
- он отображается в списке;
- выбор возможен;
- поведение не ломается.
- Граничный кейс: если временно нет ни одного пользователя с признаком "руководитель":
- список пуст;
- поведение системы корректно:
- либо блокировка сохранения с понятным сообщением,
- либо особое правило (например, системный администратор по умолчанию) — уточняется в требованиях.
- Сортировка и формат отображения
- Список отсортирован по алфавиту в соответствии с заданным форматом:
- по ФИО или "Фамилия Имя Отчество", или по другому явно указанному полю;
- Локализация и регистр:
- сортировка корректна для русских/латинских букв, не "ломается" на разных алфавитах;
- При совпадении фамилий:
- порядок детерминирован (например, сначала по фамилии, потом по имени).
- Значения отображаются в согласованном формате:
- нет обрезанных строк;
- при длинных ФИО отображение корректно (многоточие/tooltip).
- Взаимодействие с БД и данными (на уровне интеграции)
Проверки с опорой на БД (через 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;
- Негативные и граничные сценарии
- Поведение при невыбранном руководителе и попытке сохранения уже существующей записи, если поле стало обязательным:
- миграционный сценарий: как система ведет себя для «старых» сотрудников без руководителя.
- Попытка вручную подставить несуществующий или не имеющий права быть руководителем ID (если есть API или манипуляция через DevTools):
- бэкенд отклоняет некорректное значение;
- возвращается корректный HTTP-статус и понятное сообщение об ошибке;
- UI не позволяет сохранить неконсистентные данные.
- Поведение при временной недоступности списка (ошибка запроса на backend):
- отображение понятной ошибки или fallback;
- отсутствие «тихого» сохранения с пустым/некорректным руководителем.
- Проверка прав доступа:
- если в системе есть разные роли (HR, админ, линейный руководитель, сотрудник):
- кто может редактировать поле;
- кто может только просматривать;
- у кого поле скрыто.
- если в системе есть разные роли (HR, админ, линейный руководитель, сотрудник):
- UX/удобство (если релевантно требованиям)
- Поле визуально помечено как обязательное (звёздочка, подсказка).
- Ошибка при незаполненном поле подсвечивает именно это поле.
- Навигация с клавиатуры:
- можно дойти до поля табом;
- открыть/закрыть список, выбрать значение (если это стандарт продукта).
- Автоматизация и регрессия
- Ключевые сценарии (выбор руководителя, валидация обязательности, фильтрация по флагу, сохранение/редактирование) включены в регрессионные автотесты:
- 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;
Разбор и важные моменты:
- Логика решения:
- GROUP BY dept_id:
- агрегирует сотрудников по отделам.
- COUNT(*):
- считает число сотрудников в каждом отделе.
- HAVING COUNT(*) <= 3:
- отфильтровывает группы (отделы) после агрегации;
- условие по агрегатам нельзя указывать в WHERE, поэтому используется HAVING.
- Типичные нюансы, которые полезно учитывать на практике:
-
Учет уволенных/неактивных сотрудников:
- если есть поле статуса (
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 в зависимости от логики.
- если нужно включать отделы с 0–3 сотрудниками, а список отделов хранится отдельно:
- Производительность и качество запроса:
- Индексы:
- для большой таблицы employees полезен индекс по dept_id (и, при необходимости, по статусу):
CREATE INDEX idx_employees_dept_active
ON employees(dept_id, status);
- для большой таблицы employees полезен индекс по dept_id (и, при необходимости, по статусу):
- HAVING используется только для условий по агрегированным значениям. Если часть условий не зависит от агрегатов, её нужно вынести в WHERE для уменьшения объема данных до агрегации.
- Инженерный контекст:
- Такой запрос часто используется:
- для аналитики (поиск малых отделов),
- для валидации бизнес-правил (например, отдел не должен быть меньше/больше определенного размера).
- В продакшене:
- подобные запросы могут быть инкапсулированы во вьюхи или репозитории;
- над ними строятся проверки, отчеты, ограничения.
Для собеседования достаточно уверенно написать запрос с 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;
Разбор по шагам:
- Самосоединение таблицы employees
- Используем два разных псевдонима одной и той же таблицы:
- e — "подчинённый" (employee),
- m — "руководитель" (manager).
- Логика связи:
e.manager_id = m.id:- у сотрудника e.manager_id указывает на id его руководителя,
- это стандартный паттерн иерархии "сотрудник — руководитель" в одной таблице.
- Условие по зарплате
- В WHERE сравниваем:
e.salary > m.salary- выбираем только тех сотрудников, чья зарплата строго выше зарплаты менеджера.
- Какие поля выводить
В зависимости от задачи можно:
-
вернуть только сотрудников:
SELECT e.*
FROM employees e
JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary; -
или, что обычно полезнее, вместе с их руководителями, как в основном примере:
- так удобнее проверять и интерпретировать результат.
- Граничные и практические моменты
-
Сотрудники без руководителя:
- у топ-менеджеров/директоров
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 на уровне схемы.
- если у кого-то salary = NULL, сравнений с NULL нужно избегать или обрабатывать:
-
Производительность:
- полезно иметь индексы:
- по
id(PK), - по
manager_id, - иногда составные индексы если есть дополнительные фильтры.
- по
- полезно иметь индексы:
Пример индекса:
CREATE INDEX idx_employees_manager_id ON employees(manager_id);
- Инженерный контекст
Этот запрос демонстрирует:
- понимание 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;
Ключевые моменты и объяснение:
-
Самосоединение одной таблицы:
- Мы используем таблицу employees дважды:
- e — подчинённый (employee),
- m — руководитель (manager).
- Связь:
e.manager_id = m.id- означает: у сотрудника e руководитель — это запись m.
- Мы используем таблицу employees дважды:
-
Условие отбора:
- В WHERE сравниваем зарплаты:
e.salary > m.salary
- В выборку попадают только те сотрудники, чья зарплата строго выше зарплаты их руководителя.
- В WHERE сравниваем зарплаты:
-
Сотрудники без руководителя:
- У топ-руководителей
manager_idможет быть NULL. - INNER JOIN по
e.manager_id = m.idавтоматически исключит их:- нам и не нужно сравнивать их с кем-то.
- У топ-руководителей
-
Нюансы для реальных систем:
- Если возможны NULL в salary:
- стоит явно исключить их, чтобы избежать "не сравнивается с NULL":
WHERE e.salary IS NOT NULL
AND m.salary IS NOT NULL
AND e.salary > m.salary;
- стоит явно исключить их, чтобы избежать "не сравнивается с NULL":
- Для больших таблиц:
- полезен индекс по
manager_id, помимо PK поid:CREATE INDEX idx_employees_manager_id ON employees(manager_id);
- полезен индекс по
- Если возможны NULL в salary:
Такой запрос демонстрирует корректное понимание self join и умение работать с иерархическими связями в одной таблице.
Вопрос 22. Составьте SQL-запрос для получения списка отделов с суммарной зарплатой сотрудников и выберите отдел с максимальной суммарной зарплатой.
Таймкод: 01:13:38
Ответ собеседника: неправильный. Пытается использовать GROUP BY и ORDER BY, но ошибочно группирует по зарплате, не использует SUM корректно, затем с подсказками переходит к идее подзапроса, однако не доводит решение до рабочего вида.
Правильный ответ:
Задача проверяет:
- умение агрегировать данные по отделам;
- корректное использование
SUM,GROUP BY,ORDER BY; - умение выбрать максимум по агрегированному значению (через сортировку или подзапрос).
Предположим структуру:
employees:idnamedept_idsalary
- (опционально)
departments:idname
- Список отделов с суммарной зарплатой сотрудников
Базовый запрос:
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;
- Отдел с максимальной суммарной зарплатой (вариант через сортировку)
Самый простой и читаемый способ:
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;
- Вариант через подзапрос (если прямой 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
);
Этот подход:
- полезен, если нужно вернуть несколько отделов при одинаковой максимальной сумме;
- демонстрирует владение подзапросами и агрегатами.
- Практические нюансы, которые стоит учитывать
- Учитывать только активных сотрудников:
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)
Пошаговое объяснение:
-
Инициализация:
n— исходное число, для которого хотим посчитать результат.res— аккумулятор, начальное значение 1 (нейтральный элемент для умножения).
-
Цикл
for n > 0:- пока
nбольше 0:- перемножаем текущее значение
resнаn; - уменьшаем
nна 1 (декремент);
- перемножаем текущее значение
- за счет декремента
nцикл гарантированно завершится, когдаnстанет 0.
- пока
-
Последовательность для
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.
- старт:
-
Обобщение:
Код последовательно перемножает все целые числа от 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
- Важные моменты, на которые стоит обратить внимание на интервью:
- Корректность условия останова:
- используется
n > 0и в теле цикла есть декремент; - нет бесконечного цикла.
- используется
- Инициализация аккумулятора:
- именно 1, а не 0 (0 убил бы произведение).
- Понимание семантики:
- важно не только "умножает числа", но и явно распознать: это алгоритм вычисления факториала.
- Более идиоматичный пример на 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.»
