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

РЕАЛЬНОЕ СОБЕСЕДОВАНИЕ QA Automation Engineer Java Перфоманс Лаб - Junior 70+ тыс.

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

Сегодня мы разберем собеседование начинающего автоматизатора, на котором кандидат демонстрирует уверенность в базовых инструментах (Selenium, REST Assured, Jenkins, Allure, CI/CD), но при этом заметно проваливается в фундаментальных знаниях Java, тест-дизайна и ключевых принципов тестирования. Диалог показывает типичный разрыв между практическим опытом «по гайдам» и отсутствием системного понимания, что делает это интервью показательной иллюстрацией того, какие пробелы чаще всего мешают джунам пройти технический скрининг.

Вопрос 1. Каковы основные цели тестирования программного обеспечения?

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

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

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

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

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

  1. Выявление дефектов как можно раньше

    • Найти ошибки в логике, реализации, интеграции, конфигурации, которые могут привести к багам в продакшене.
    • Чем раньше найден дефект (например, через unit-тесты в Go), тем дешевле его исправление.
  2. Проверка соответствия требованиям и спецификациям

    • Подтвердить, что система делает то, что было заявлено: бизнес-требования, функциональные требования, нефункциональные требования.
    • Это включает:
      • функциональность (корректные результаты),
      • производительность (latency, throughput),
      • безопасность,
      • надежность.
  3. Проверка на соответствие ожиданиям пользователя и бизнес-целям

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

    • Через тестируемость кода (testability) можно оценить качество архитектуры:
      • наличие четких контрактов (интерфейсы в Go),
      • отсутствие сильной связанности,
      • корректное разбиение на модули.
    • Если код сложно покрыть тестами — это часто сигнал к рефакторингу.
  5. Предотвращение регрессий

    • Набор автотестов (unit, integration, e2e) должен гарантировать, что новые изменения не ломают уже работающий функционал.
    • Это фундамент для безопасного рефакторинга и быстрой разработки.
  6. Проверка нефункциональных характеристик

    • Производительность: нагрузочное, стресс-тестирование.
    • Масштабируемость.
    • Отказоустойчивость: как сервис ведет себя при сбоях зависимостей.
    • Безопасность: попытки обхода авторизации, инъекции, некорректные входные данные.
  7. Снижение рисков и повышение уверенности перед релизом

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

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

Пример (Go, простая иллюстрация целей тестирования):

type Calculator interface {
Sum(a, b int) int
}

type DefaultCalculator struct{}

func (c DefaultCalculator) Sum(a, b int) int {
return a + b
}

// Unit-тест: проверка корректности и фиксация ожидаемого поведения.
func TestDefaultCalculator_Sum(t *testing.T) {
c := DefaultCalculator{}
tests := []struct {
a, b int
want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}

for _, tt := range tests {
got := c.Sum(tt.a, tt.b)
if got != tt.want {
t.Fatalf("Sum(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}

Этот тест:

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

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

Вопрос 2. Какие основные виды тестирования существуют и в чем их суть?

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

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

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

Вопрос про виды тестирования — это не только разделение на функциональное и нефункциональное. Важно понимать несколько осей классификаций: по цели, по уровню, по степени изоляции, по автоматизации, по моменту применения. Ниже — системный обзор, ориентированный на реальную разработку сервисов (например, на Go).

Основные разрезы и виды тестирования:

  1. По уровню (глубине) тестирования
  • Юнит-тестирование (Unit Testing)

    • Проверка отдельных, максимально изолированных компонентов: функций, методов, небольших модулей.
    • Используются моки/стабы для зависимостей (БД, внешние сервисы, очереди).
    • Цель: быстрое выявление логических ошибок, обеспечение тестируемости кода.
    • В Go:
      • файлы вида: xxx_test.go
      • тестируем чистую логику: парсинг, валидация, бизнес-правила.

    Пример:

    func Add(a, b int) int {
    return a + b
    }

    func TestAdd(t *testing.T) {
    if got := Add(2, 3); got != 5 {
    t.Fatalf("expected 5, got %d", got)
    }
    }
  • Интеграционное тестирование (Integration Testing)

    • Проверка взаимодействия нескольких реальных компонентов:
      • сервис + реальная БД (PostgreSQL),
      • сервис + Kafka,
      • несколько микросервисов вместе.
    • Цель: убедиться, что контракты и конфигурация корректны.
    • В Go часто используют:
      • docker-compose для поднятия зависимостей,
      • реальные HTTP-запросы/SQL-запросы.
  • Системное тестирование (System Testing)

    • Проверка всей системы целиком как "черного ящика".
    • Близко к реальному окружению, реальные сценарии.
    • Цель: убедиться, что все работает как целостный продукт.
  • Приемочное тестирование (Acceptance Testing, UAT)

    • Проверка того, что решение удовлетворяет бизнес-требованиям и может быть принято заказчиком/пользователем.
    • Автоматизированное (BDD, сценарии) или ручное.
  1. По цели / характеру тестирования
  • Функциональное тестирование

    • Проверяет, что система делает то, что должна: бизнес-логика, валидации, права доступа, правила обработки данных.
    • Не интересуют производительность или UI-красота — только корректность поведения.
    • Пример функционального сценария:
      • "Создать пользователя, залогиниться, получить токен, сделать защищенный запрос, получить 200 OK."
  • Нефункциональное тестирование

    • Проверяет характеристики, не связанные напрямую с бизнес-функциями:
      • производительность,
      • надежность,
      • масштабируемость,
      • безопасность,
      • удобство использования,
      • восстанавливаемость.
    • Примеры:
      • Нагрузочное тестирование: выдержит ли сервис 10k RPS?
      • Стресс-тест: что будет при 100k RPS и падении части инстансов?
      • Тестирование безопасности: SQL-инъекции, XSS, brute-force, неверная авторизация.
  1. По изменению кода и времени запуска
  • Регрессионное тестирование

    • Проверяет, что новый код не сломал существующий функционал.
    • Обычно реализуется через:
      • стабильный набор автотестов (unit + integration + e2e),
      • обязательный прогон в CI при каждом merge request.
    • Цель — безопасная эволюция системы.
  • Smoke-тесты

    • Быстрые проверки "жива ли система" после деплоя:
      • поднимается ли сервис,
      • отвечает ли health-check,
      • работают ли критические endpoint'ы.
    • Минимальный набор, но обязательный.
  • Sanity-тестирование

    • Узконаправленная проверка конкретных изменений:
      • "Мы правили оплату — проверим оплату чуть глубже, но не весь продукт."
  1. По видимости кода
  • "Черный ящик" (Black-box testing)

    • Тестировщик не опирается на реализацию, только на спецификацию и API.
    • Применяется для системных, приемочных тестов, e2e.
  • "Серый ящик" (Gray-box testing)

    • Частичное знание внутренней логики:
      • знаем схемы БД, архитектуру,
      • понимаем, какие сценарии наиболее уязвимы.
    • Часто применяется в тестировании безопасности, интеграций.
  • "Белый ящик" (White-box testing)

    • Тестирование с учетом внутренней структуры кода:
      • покрытие веток,
      • проверка граничных условий.
    • Типично для юнит-тестов.
  1. По степени автоматизации
  • Ручное тестирование

    • Исследовательское, UX-проверки, сложные бизнес-сценарии.
    • Полезно для поиска неочевидных багов, которые сложно формализовать.
  • Автоматизированное тестирование

    • Юнит, интеграционные, e2e, нагрузочные.
    • Обязательная часть современного процесса разработки.
    • Важные свойства:
      • повторяемость,
      • предсказуемость,
      • быстрый фидбек в CI/CD.
  1. Дополнительные важные виды
  • Тестирование безопасности (Security / Penetration Testing)

    • Проверка авторизации/аутентификации,
    • защита от SQL/XSS/CSRF,
    • работа с секретами, JWT, TLS.
  • Тестирование совместимости (Compatibility)

    • Работа с разными версиями браузеров, API, клиентов.
    • Для микросервисов: backward/forward compatibility.
  • Тестирование доступности и отказоустойчивости

    • Как система ведет себя при:
      • падении БД,
      • таймаутах внешних сервисов,
      • частичной потере сети.
    • В Go-сервисах:
      • корректная обработка context.Context,
      • retry/policy,
      • graceful shutdown.

Небольшой пример интеграционного теста с БД в Go (упрощенный):

func TestUserRepository_CreateAndGet(t *testing.T) {
db := openTestDB(t) // реальная тестовая БД (docker)
repo := NewUserRepository(db)

ctx := context.Background()
created, err := repo.Create(ctx, User{Name: "Alice", Email: "a@example.com"})
if err != nil {
t.Fatalf("create user: %v", err)
}

got, err := repo.GetByID(ctx, created.ID)
if err != nil {
t.Fatalf("get user: %v", err)
}

if got.Email != "a@example.com" {
t.Fatalf("unexpected email: %s", got.Email)
}
}

И SQL-фрагмент для функционального теста (проверка целостности данных):

SELECT id, email
FROM users
WHERE email = 'a@example.com';

Кратко: важно уметь не просто назвать "функциональное/нефункциональное", а понимать уровни (unit, integration, e2e, acceptance), цели (регрессия, нагрузка, безопасность), подходы ("черный"/"белый" ящик) и уметь применять их для построения надежного контура качества вокруг своего сервиса.

Вопрос 3. Какие дополнительные типы тестирования существуют и в чем суть интеграционного тестирования?

Таймкод: 00:02:35

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

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

Здесь важно:

  1. шире назвать типы тестирования (по целям, технике, фазе),
  2. корректно и глубоко раскрыть интеграционное тестирование.

Дополнительные (ключевые) типы тестирования:

  1. Регрессионное тестирование

    • Цель: убедиться, что изменения (новый функционал, рефакторинг, исправление багов) не сломали существующее поведение.
    • Практика:
      • обязательный прогон набора автотестов (unit + integration + e2e) при каждом коммите/merge;
      • фиксация найденных багов тестами, чтобы не вернулись.
    • Важный момент: качественный регрессионный набор — основа безопасных частых релизов.
  2. Статическое тестирование

    • Проводится без выполнения кода.
    • Сюда входят:
      • анализ требований и спецификаций,
      • code review,
      • статический анализ: golangci-lint, vet, линтеры SQL-мigrations.
    • Цель: находить дефекты в архитектуре, контракте, стиле, типичных паттернах ошибок (data race, неверная работа с ошибками, SQL-инъекции) до запуска кода.
    • В Go статический анализ особенно силен: строгая типизация + линтеры + go vet.
  3. Динамическое тестирование

    • Проводится при выполнении кода.
    • Включает:
      • unit-тесты, интеграционные тесты, e2e, нагрузочные тесты.
    • Цель: проверить фактическое поведение системы в рантайме.
  4. Smoke-тестирование

    • Быстрая проверка "живости" системы:
      • сервис стартует,
      • отвечает на health-check,
      • базовые endpoint'ы работают.
    • Запускается после деплоя, при выкладке на новое окружение.
  5. Sanity-тестирование

    • Узко направленная проверка после небольших изменений:
      • "Мы изменили модуль платежей — проверим конкретно платежи и связанные ключевые сценарии."
    • Глубже, чем smoke по конкретной области, но не полный регресс.
  6. Тестирование безопасности

    • Цели:
      • защита данных,
      • корректность аутентификации и авторизации,
      • устойчивость к типичным атакам (SQL-инъекции, XSS, CSRF, brute-force).
    • Для сервисов:
      • проверка валидации входных данных на API,
      • корректная работа с токенами (JWT, OAuth),
      • шифрование и хранение секретов.
  7. Нагрузочное, стресс- и тестирование производительности

    • Нагрузочное:
      • проверяем, выдерживает ли система ожидаемую нагрузку (например, 5k RPS).
    • Стресс-тест:
      • проверяем поведение за пределами нормальной нагрузки, как система деградирует.
    • Performance-тест:
      • измеряем время отклика, использование CPU/RAM, эффективность запросов к БД.
    • В связке с Go:
      • бенчмарки (testing.B),
      • профилирование (pprof),
      • оптимизация запросов к БД и работы с памятью.
  8. Тестирование совместимости

    • Проверка работы с разными версиями:
      • API клиентов,
      • протоколов,
      • схем БД (backward/forward compatibility миграций),
      • например, микросервис должен работать и с новой, и со старой версией другого сервиса.
  9. Тестирование отказоустойчивости и устойчивости к сбоям (resilience / chaos testing)

    • Проверка поведения при:
      • падении БД,
      • задержках и таймаутах,
      • частичной потере сети,
      • отказе отдельных инстансов.
    • Цель: убедиться, что система:
      • корректно обрабатывает ошибки,
      • не теряет данные,
      • восстанавливается после сбоя.
    • Для Go:
      • правильное использование context.Context,
      • таймауты, ретраи, circuit breaker,
      • graceful shutdown.
  10. Исследовательское тестирование (exploratory testing)

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

Теперь ключевое: интеграционное тестирование.

Суть интеграционного тестирования:

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

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

  • Проверяются:

    • контракты между сервисами (HTTP/gRPC),
    • работа с реальной или максимально близкой к боевой БД,
    • взаимодействие с брокерами сообщений (Kafka, RabbitMQ),
    • корректность конфигурации (URL, порты, схемы, миграции),
    • сериализация/десериализация (JSON, Protobuf),
    • транзакции, согласованность данных.
  • Цель:

    • обнаружить ошибки "стыков":
      • несовпадение форматов,
      • неверные URL/эндпоинты,
      • расхождения в схемах таблиц,
      • неправильные статусы HTTP,
      • нарушения инвариантов между компонентами.
    • гарантировать, что компоненты, которые по отдельности "зеленые" по unit-тестам, вместе работают корректно.
  • Типичные подходы:

    • Использовать реальную тестовую инфраструктуру:
      • PostgreSQL/MySQL в Docker,
      • локальный Kafka/Redis,
      • поднимаемый сервис и реальные HTTP-запросы.
    • Минимизировать моки: на этом уровне интересует реальное поведение.

Пример интеграционного теста в Go с БД и HTTP-ручкой (упрощенный):

func TestCreateUser_Integration(t *testing.T) {
// Поднимаем тестовую БД (например, через docker или in-memory, если подходит)
db := mustOpenTestDB(t)
applyMigrations(db)

// Инициализируем зависимости и HTTP-сервер
repo := NewUserRepository(db)
handler := NewUserHandler(repo)
srv := httptest.NewServer(http.HandlerFunc(handler.CreateUser))
defer srv.Close()

// Отправляем реальный HTTP-запрос
payload := `{"email": "test@example.com", "name": "Test User"}`
resp, err := http.Post(srv.URL, "application/json", strings.NewReader(payload))
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}

// Проверяем, что пользователь реально записан в БД
var count int
err = db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = $1`, "test@example.com").Scan(&count)
if err != nil {
t.Fatalf("query failed: %v", err)
}
if count != 1 {
t.Fatalf("expected 1 user in db, got %d", count)
}
}

SQL-фрагмент, связанный с интеграционным тестом:

CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

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

  • перечисляет не только базовые, но и практические типы тестирования,
  • четко разводит статическое/динамическое, регрессионное, smoke/sanity,
  • глубоко понимает интеграционное тестирование как проверку контрактов и взаимодействий реальных компонентов, а не просто "сервер-БД-клиент в общем".

Вопрос 4. Какие существуют основные принципы тестирования и сколько их?

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

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

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

Классически выделяют 7 базовых принципов тестирования (по ISTQB). Важно не просто знать числа, а понимать, как эти принципы применять на практике — в процессе разработки, тестирования, code review, CI/CD.

  1. Тестирование демонстрирует наличие дефектов, а не их отсутствие
  • Тесты могут показать, что в системе есть ошибки, но никогда не доказывают, что их нет вообще.
  • Вывод:
    • цель — снижение рисков до приемлемого уровня, а не иллюзия "у нас нет багов";
    • даже при 100% покрытии кода тестами остаются риски: неверные требования, пропущенные сценарии, ошибки интеграций, конкурентные условия.
  • Практика:
    • в Go: не увлекаться погоней за percent coverage, а концентрироваться на критичных сценариях, инвариантах, граничных условиях.
  1. Полное тестирование невозможно
  • Нельзя протестировать все комбинации входных данных, состояний, окружений для сколь-нибудь нетривиальной системы.
  • Вывод:
    • тестирование всегда выборочно и риск-ориентированно;
    • нужно уметь расставлять приоритеты.
  • Практика:
    • выделяем:
      • критичные бизнес-сценарии (платежи, авторизация, транзакции),
      • проблемные места (конкурентный доступ, кэш, интеграции),
      • граничные значения и edge cases.
  • Пример (Go): при функции валидации не тестируем все числа от -2^31 до 2^31-1, а берем граничные и характерные значения.
  1. Раннее тестирование
  • Тестирование должно начинаться как можно раньше в жизненном цикле разработки.
  • Вывод:
    • подключаем тестирование уже на стадии требований, архитектуры, дизайна API;
    • пишем unit-тесты параллельно с кодом;
    • проверяем миграции БД и контракты сервисов до релиза.
  • Практика:
    • в Go:
      • пишем тесты сразу с логикой,
      • используем статический анализ (go vet, golangci-lint) в CI,
      • валидируем OpenAPI/Protobuf контракты заранее.
  1. Скопление дефектов (Defect Clustering)
  • Большая часть дефектов обычно сосредоточена в относительно небольшом числе модулей/компонентов.
    • сложные модули,
    • интенсивно меняющийся код,
    • старый "легаси" без тестов.
  • Вывод:
    • усиливаем тестирование именно в этих зонах:
      • больше unit-тестов;
      • глубже интеграционные;
      • целенаправленное регрессионное тестирование.
  • Практика:
    • анализируем историю багов и падений,
    • добавляем тесты и рефакторинг точечно.
  1. Парадокс пестицида (Pesticide Paradox)
  • Если постоянно запускать один и тот же набор тестов, то через какое-то время они перестают находить новые дефекты.
  • Вывод:
    • набор тестов должен эволюционировать:
      • по мере появления новых багов добавляем тесты, которые их ловят,
      • пересматриваем сценарии,
      • дополняем граничные случаи, новые интеграции.
  • Практика:
    • каждый найденный баг — это повод написать тест, воспроизводящий его, и оставить его в регрессионном наборе;
    • пересматривать автотесты при изменении архитектуры и бизнес-логики.
  1. Тестирование зависит от контекста
  • Подход к тестированию определяется типом системы и рисками:
    • финансовые сервисы vs лендинг,
    • низкоуровневый высоконагруженный сервис vs административная панель.
  • Вывод:
    • нельзя навязывать один и тот же набор практик всем проектам;
    • выбираем глубину и виды тестирования исходя из:
      • критичности данных,
      • требований к SLA, безопасности, производительности.
  • Практика:
    • для платежного API:
      • жёсткие тесты безопасности,
      • idempotency, целостность транзакций, миграции;
    • для внутреннего сервиса аналитики:
      • фокус на корректности расчетов и производительности при больших объемах данных.
  1. Заблуждение об отсутствии ошибок
  • Отсутствие найденных багов в тестах не означает, что продукт полезен или соответствует ожиданиям.
  • Пример:
    • все тесты зелёные, но реализованы неверные бизнес-правила;
    • система идеально стабильно делает не то, что нужно пользователям.
  • Вывод:
    • важно тестировать не только реализацию, но и корректность требований, сценарии использования, UX;
    • нужны приемочные тесты, участие бизнеса/аналитиков.
  • Практика:
    • описывать функциональность в виде сценариев (user stories, BDD),
    • проверять, что API и поведение соответствуют реальным бизнес-процессам.

Дополнительные практические акценты:

  • Эти принципы напрямую влияют на инженерные решения:
    • пишем тестируемый код (чистые функции, интерфейсы, разбиение на слои),
    • интегрируем тесты в CI/CD,
    • автоматизируем критичные проверки,
    • анализируем дефекты и улучшаем покрытие не по проценту, а по рискам.

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

Вопрос 5. Какие существуют основные техники тест-дизайна?

Таймкод: 00:03:39

Ответ собеседника: неправильный. Слышал о техниках, но не смог назвать ни одной.

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

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

Ключевые техники (практически значимые):

  1. Эквивалентное разбиение (Equivalence Partitioning)

Суть:

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

Пример:

  • Поле "age" должно быть в диапазоне 18–65.
    • Классы:
      • < 18 (некорректно),
      • 18–65 (корректно),
      • > 65 (некорректно).
  • Вместо проверки всех возможных значений выбираем:
    • 10, 30, 70 как представители разных классов.

Практический пример в Go:

func ValidateAge(age int) bool {
return age >= 18 && age <= 65
}

func TestValidateAge_Equivalence(t *testing.T) {
cases := []struct {
age int
want bool
}{
{10, false}, // < 18
{30, true}, // 18–65
{70, false}, // > 65
}

for _, c := range cases {
if got := ValidateAge(c.age); got != c.want {
t.Fatalf("age=%d: want %v, got %v", c.age, c.want, got)
}
}
}
  1. Анализ граничных значений (Boundary Value Analysis)

Суть:

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

Продолжая пример с возрастом 18–65:

  • Тесты: 17, 18, 65, 66.

Go-пример:

func TestValidateAge_Boundary(t *testing.T) {
cases := []struct {
age int
want bool
}{
{17, false},
{18, true},
{65, true},
{66, false},
}

for _, c := range cases {
if got := ValidateAge(c.age); got != c.want {
t.Fatalf("age=%d: want %v, got %v", c.age, c.want, got)
}
}
}
  1. Попарное тестирование (Pairwise Testing)

Суть:

  • Когда есть много параметров с разными значениями, полное переборное тестирование (combinatorial explosion) невозможно.
  • Pairwise (или более общие t-wise техники) предполагает:
    • мы подбираем набор тестов так, чтобы каждая пара (или тройка) возможных значений параметров хотя бы раз встретилась.
  • Позволяет резко сократить число тестов при хорошем покрытии взаимодействий параметров.

Пример:

  • Параметры: браузер (Chrome/Firefox/Safari), ОС (Windows/macOS/Linux), язык (EN/RU).
  • Вместо 3×3×2 = 18 комбинаций подбирается небольшой набор, где каждая пара значений покрыта.

Применение в backend:

  • Конфигурации фич-флагов,
  • Типы авторизации,
  • Форматы запросов.
  1. Таблица принятия решений (Decision Table Testing)

Суть:

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

Пример:

  • Условия:
    • Пользователь активен? (Y/N)
    • У него есть роль admin? (Y/N)
  • Действие:
    • Разрешить доступ к админ-панели? (Y/N)

Таблица:

  • Активен=Y, Admin=Y → доступ разрешен.
  • Активен=Y, Admin=N → доступ запрещен.
  • Активен=N, Admin=Y → доступ запрещен.
  • Активен=N, Admin=N → доступ запрещен.

Go-пример:

func CanAccessAdmin(isActive, isAdmin bool) bool {
return isActive && isAdmin
}

func TestCanAccessAdmin_DecisionTable(t *testing.T) {
cases := []struct {
active bool
admin bool
want bool
}{
{true, true, true},
{true, false, false},
{false, true, false},
{false, false, false},
}

for _, c := range cases {
got := CanAccessAdmin(c.active, c.admin)
if got != c.want {
t.Fatalf("active=%v, admin=%v: want %v, got %v",
c.active, c.admin, c.want, got)
}
}
}
  1. Тестирование на основе состояний (State Transition Testing)

Суть:

  • Используется, когда система описывается конечным автоматом: состояния + события + переходы.
  • Проверяем:
    • корректные переходы,
    • невозможность недопустимых переходов,
    • поведение при "запрещенных" событиях.

Примеры:

  • Жизненный цикл заказа: Created → Paid → Shipped → Completed / Cancelled.
  • Подписка: Trial → Active → Suspended → Canceled.

Практически:

  • Описываем состояния и переходы явно.
  • Делаем тесты на:
    • допустимые переходы,
    • запрет нелегальных (например, Shipped до Paid).

SQL-пример для проверки корректного состояния:

-- Нельзя перевести заказ в 'shipped', если он не 'paid'
SELECT COUNT(*)
FROM orders
WHERE status = 'shipped'
AND prev_status <> 'paid';
  1. Тестирование на основе использования (Use Case / Scenario-based)

Суть:

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

Пример:

  • "Зарегистрироваться → подтвердить email → залогиниться → создать заказ → оплатить → получить чек."

Применение:

  • E2E, интеграционные тесты.
  • Проверка, что система реально решает бизнес-задачу, а не просто корректно отвечает на отдельные вызовы.
  1. Причинно-следственная связь (Cause-Effect Graphing)

Суть:

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

Полезно:

  • при сложных правилах тарифов, скидок, access rules.
  1. Комбинаторные и негативные тесты
  • Комбинаторные:
    • систематизация комбинаций параметров (связано с pairwise).
  • Негативные тесты:
    • проверка поведения на некорректных данных, ошибках сети, нарушении контрактов.
    • Это не отдельная формальная техника, но обязательная часть дизайна тестов.

Пример негативных тестов в Go:

func TestParseUser_Negative(t *testing.T) {
// пустой body
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(""))
// ожидаем 400, ошибку парсинга
}

Почему это важно для разработки:

  • Техники тест-дизайна позволяют:
    • не писать случайные тесты "по ощущениям",
    • строить минимальный, но осмысленный набор тестов,
    • лучше покрывать граничные случаи и сложные правила,
    • экономить время при сохранении качества.

Хороший ответ на интервью:

  • называет хотя бы 4–5 техник:
    • эквивалентное разбиение,
    • анализ граничных значений,
    • таблица решений,
    • тестирование переходов состояний,
    • попарное тестирование,
    • сценарные/use case тесты,
  • и кратко, по существу объясняет каждую, желательно с привязкой к реальному коду или бизнес-логике.

Вопрос 6. Из каких основных элементов состоит тест-кейс?

Таймкод: 00:04:03

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

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

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

Классические основные элементы тест-кейса:

  1. Идентификатор (ID)

    • Уникальный номер/код тест-кейса.
    • Нужен для:
      • однозначной ссылки в баг-репортах, планах, отчётах,
      • связи с автотестом, задачей в трекере, требованием.
    • Пример: TC-API-USER-001.
  2. Название (Title / Summary)

    • Кратко и понятно описывает суть проверки.
    • Требование: из названия должно быть понятно, что проверяем.
    • Примеры:
      • "Успешная регистрация пользователя с валидными данными"
      • "Ошибка при попытке логина с неверным паролем"
  3. Предусловия (Preconditions)

    • Что должно быть выполнено/настроено ДО начала теста.
    • Примеры:
      • Пользователь уже зарегистрирован.
      • В БД есть заказ со статусом "Paid".
      • Сервис запущен, миграции применены, токен получен.
    • В реальных проектах:
      • часто оформляются как фикстуры, миграции, скрипты и setup в автотестах.
  4. Входные данные / Тестовые данные (Test Data)

    • Конкретные значения, которые используются в тесте.
    • Примеры:
      • email, пароль,
      • ID заказа,
      • тело HTTP-запроса,
      • записи в таблицах БД.
    • Важно:
      • данные должны быть воспроизводимыми и однозначно описанными.
  5. Шаги выполнения (Steps)

    • Последовательность действий, которые нужно выполнить.
    • Шаги должны быть:
      • однозначными,
      • воспроизводимыми,
      • не зависящими от "домыслов" исполнителя.
    • В контексте backend:
      • это могут быть конкретные HTTP-запросы, вызовы gRPC, SQL-операции.
  6. Ожидаемый результат (Expected Result)

    • Четко описывает, что должно произойти после выполнения каждого шага или сценария в целом.
    • Важно:
      • результат должен быть проверяемым (assertable), без размытых формулировок.
    • Примеры:
      • возвращен HTTP 201 и JSON с полем id,
      • запись создана в таблице users с нужными значениями,
      • при неверном пароле — HTTP 401 и понятное сообщение об ошибке.
  7. Постусловия / Восстановление (Postconditions / TearDown) — опционально, но важно в зрелых процессах

    • Состояние системы после теста:
      • что должно сохраниться,
      • что нужно очистить (cleanup), чтобы не ломать другие тесты.
    • Примеры:
      • удалить тестового пользователя,
      • откатить изменения в БД,
      • завершить сессию/очистить токены.
  8. Дополнительно, для практического уровня:

  • Связь с требованиями (Requirements Mapping)

    • Ссылка на требования, user story, спецификацию API.
    • Позволяет понять: какой бизнес-требование покрывает тест-кейс.
  • Приоритет (Priority)

    • Важность тест-кейса:
      • критичный (блокирующий релиз),
      • высокий/средний/низкий.
    • Используется при планировании регресса.
  • Тип теста

    • Функциональный, нефункциональный, позитивный, негативный, интеграционный, e2e и т.п.

Пример: как формальный тест-кейс трансформируется в автотест (Go + HTTP + SQL)

Тест-кейс (вербально):

  • ID: TC-API-USER-001
  • Название: Успешное создание пользователя с валидными данными
  • Предусловия: сервис запущен, БД очищена, применены миграции
  • Шаги:
    1. Отправить POST /users с корректным JSON.
  • Входные данные:
    • {"email": "test@example.com", "name": "Test User"}
  • Ожидаемый результат:
    • HTTP 201
    • В ответе есть id
    • В таблице users есть запись с указанным email

Автотест на Go:

func TestCreateUser_Success(t *testing.T) {
db := mustOpenTestDB(t)
applyMigrations(db)
defer db.Close()

srv := newTestServer(db) // HTTP-сервер поверх реального репозитория
defer srv.Close()

body := `{"email":"test@example.com","name":"Test User"}`
resp, err := http.Post(srv.URL+"/users", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}

var id int64
if err := json.NewDecoder(resp.Body).Decode(&struct {
ID *int64 `json:"id"`
}{ID: &id}); err != nil || id == 0 {
t.Fatalf("expected valid id in response, got err=%v id=%d", err, id)
}

var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM users WHERE email = $1`, "test@example.com").Scan(&count); err != nil {
t.Fatalf("db check failed: %v", err)
}
if count != 1 {
t.Fatalf("expected 1 user in db, got %d", count)
}
}

Этот пример наглядно показывает связь:

  • ID/название → имя теста;
  • предусловия → setup (migrations, тестовая БД, сервер);
  • шаги → HTTP-запрос;
  • входные данные → тело запроса;
  • ожидаемый результат → проверки статуса, тела ответа и содержимого БД;
  • постусловия → очистка ресурсов (defer Close, изолированная тестовая БД).

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

Вопрос 7. Какие основные принципы объектно-ориентированного программирования существуют?

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

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

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

Основные принципы ООП:

  • инкапсуляция
  • наследование
  • полиморфизм
  • абстракция

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

Инкапсуляция

Суть:

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

Ключевые эффекты:

  • уменьшение связанности,
  • ясные контракты,
  • защита инвариантов (нельзя установить некорректное состояние).

В Go:

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

Пример:

type User struct {
id int64 // неэкспортируемое поле
Email string // экспортируемое поле
}

// Конструктор контролирует инварианты
func NewUser(id int64, email string) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid id")
}
if email == "" {
return nil, fmt.Errorf("email required")
}
return &User{id: id, Email: email}, nil
}

// Только метод открывает доступ к id
func (u *User) ID() int64 {
return u.id
}

Наследование

Классическое наследование:

  • Один класс "расширяет" другой, перенимая поля и поведение базового класса.
  • Проблемы:
    • жесткая связанность,
    • хрупкие иерархии,
    • сложность изменения базовых классов.

Современный практический подход:

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

В Go:

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

Пример композиции:

type Logger struct{}

func (l Logger) Log(msg string) {
fmt.Println(msg)
}

type Service struct {
Logger
}

func (s Service) DoWork() {
s.Log("working") // используем поведение встроенного поля
}

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

Полиморфизм

Суть:

  • Возможность работать с разными типами через единый интерфейс, не зная их конкретной реализации.
  • Код, который опирается на абстракцию, а не на конкретный тип.

В Go:

  • Интерфейсы — ключевой инструмент полиморфизма.
  • Тип "реализует" интерфейс неявно, если имеет необходимые методы.

Пример:

type Notifier interface {
Notify(msg string) error
}

type EmailNotifier struct{}

func (EmailNotifier) Notify(msg string) error {
fmt.Println("email:", msg)
return nil
}

type SmsNotifier struct{}

func (SmsNotifier) Notify(msg string) error {
fmt.Println("sms:", msg)
return nil
}

func SendAlert(n Notifier, msg string) {
_ = n.Notify(msg)
}

Мы можем подставить любую реализацию Notifier (в том числе мок в тестах), это полиморфизм в действии.

Абстракция

Суть:

  • Выделение только существенных деталей и скрытие второстепенных.
  • Работа через понятные модели и интерфейсы вместо конкретной реализации.
  • Тесно связана с инкапсуляцией и полиморфизмом.

В Go:

  • Интерфейс описывает, "что умеет" компонент, а не "как он это делает".
  • Хорошие абстракции:
    • простые,
    • предметно-ориентированные,
    • минималистичные (interface segregation).

Пример:

type UserRepository interface {
Create(ctx context.Context, u User) (int64, error)
GetByID(ctx context.Context, id int64) (*User, error)
}

Реализация может быть на PostgreSQL, in-memory, mock для тестов — вызывающему это не важно, он работает с абстракцией.

Краткий вывод:

  • Инкапсуляция: контролируем доступ к состоянию и скрываем детали.
  • Абстракция: выделяем ключевые операции и контракты.
  • Полиморфизм: работаем через абстракции (например, интерфейсы), подставляя разные реализации.
  • Наследование: в классическом виде часто избыточно; на практике лучше композиция и интерфейсы, как в Go.

Такое понимание принципов важно, даже если язык (как Go) формально не является классическим ООП-языком: сами идеи лежат в основе архитектурных решений, проектирования API, модульности и тестируемости.

Вопрос 8. Что такое инкапсуляция?

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

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

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

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

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

Ключевые аспекты инкапсуляции:

  1. Управление доступом к данным

Инкапсуляция защищает состояние от прямых неконтролируемых изменений извне.

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

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

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

Инкапсуляция формирует контракт:

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

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

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

Важно скрывать не только данные, но и:

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

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

Инкапсуляция в Go (практически)

Go не имеет модификаторов доступа вроде public/private/protected в стиле других языков, но реализует инкапсуляцию через:

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

Пример: скрываем внутреннее состояние пользователя и контролируем его создание.

package domain

import (
"errors"
"strings"
)

type User struct {
id int64 // скрыто от других пакетов
email string // тоже скрыто
name string // тоже скрыто
}

// Конструктор инкапсулирует правила создания пользователя.
func NewUser(id int64, email, name string) (*User, error) {
if id <= 0 {
return nil, errors.New("id must be positive")
}
email = strings.TrimSpace(email)
if email == "" {
return nil, errors.New("email required")
}
name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("name required")
}

return &User{
id: id,
email: email,
name: name,
}, nil
}

// Публичные методы предоставляют контролируемый доступ.
func (u *User) ID() int64 {
return u.id
}

func (u *User) Email() string {
return u.email
}

func (u *User) Name() string {
return u.name
}

// Меняем имя только через метод, чтобы сохранять инварианты.
func (u *User) Rename(newName := string) error {
newName = strings.TrimSpace(newName)
if newName == "" {
return errors.New("name required")
}
u.name = newName
return nil
}

Плюсы такого подхода:

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

Инкапсуляция и тестирование

Хорошая инкапсуляция облегчает тестирование:

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

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

type UserRepository interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id int64) (*User, error)
}

type pgUserRepo struct {
db *sql.DB
}

func NewPgUserRepo(db *sql.DB) UserRepository {
return &pgUserRepo{db: db}
}

func (r *pgUserRepo) Create(ctx context.Context, u *User) error {
query := `
INSERT INTO users (id, email, name)
VALUES ($1, $2, $3)
`
_, err := r.db.ExecContext(ctx, query, u.ID(), u.Email(), u.Name())
return err
}

Здесь:

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

Кратко:

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

Просто "сделать методы приватными" — недостаточно; важно проектировать стабильные контракты и четко разделять внешнее поведение и внутренние детали.

Вопрос 9. Что такое наследование?

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

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

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

Наследование — это механизм, при котором один тип (дочерний/производный) перенимает данные и поведение другого типа (базового/родительского), расширяя или переопределяя его функциональность.

Классическое понимание (в ООП-языках с классами):

  • Дочерний класс:
    • "наследует" поля и методы родительского класса,
    • может добавлять новые поля и методы,
    • может переопределять поведение родительских методов.
  • Это позволяет:
    • переиспользовать код,
    • описывать иерархии "is-a" (является).

Простой пример (понятийно):

  • Есть базовый тип "Transport" с методом "Move()".
  • "Car", "Bike", "Truck" наследуют Transport, разделяя часть поведения и интерфейс.

Однако важно понимать несколько ключевых моментов:

  1. Наследование — это не только "удобно скопировать методы"
  • Наследование выражает отношение "является" (is-a), а не просто "хочу использовать чужой код".
    • "Кошка" является "Животным" — ок.
    • "Логгер" не является "Файлом", хотя может его использовать — здесь нужна композиция, а не наследование.
  • Неправильное использование наследования ведет к:
    • хрупким иерархиям,
    • сильной связанности,
    • сложности изменений в базовых классах (ломается куча наследников).
  1. Предпочтение композиции над наследованием

В современных архитектурных подходах и при проектировании сервисов:

  • Глубокие иерархии классов считаются антипаттерном.
  • Рекомендуется:
    • использовать композицию (включение одного объекта внутрь другого),
    • строить поведение через интерфейсы и делегирование, а не через жесткое наследование.
  1. Наследование в контексте Go

Go не поддерживает классическое наследование "class A extends B", и это осознанное решение.

Вместо него:

  • Композиция и встраивание (embedding) структур:
    • один тип может содержать другой и "поднимать" его методы.
  • Интерфейсы:
    • позволяют описывать абстракции и полиморфизм без наследования реализации.

Пример композиции и embedding в Go:

type Logger struct{}

func (Logger) Log(msg string) {
fmt.Println(msg)
}

type Service struct {
Logger // embedding: методы Logger "поднимаются" на Service
}

func (s Service) DoWork() {
s.Log("doing work") // используем унаследованное поведение через композицию
}

Здесь:

  • Service не "является" Logger в семантическом смысле домена, он "имеет" логгер.
  • Но через embedding мы переиспользуем реализацию.
  • Это ближе к "наследованию реализации", но без жесткой иерархии типов.
  1. Наследование и архитектура

При ответе на интервью важно показать зрелое понимание:

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

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

Вопрос 10. Что такое полиморфизм?

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

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

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

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

Ключевая идея:
"Один интерфейс — множество реализаций."

Важно:

  • Мы опираемся на абстракцию (интерфейс), а конкретное поведение выбирается в рантайме (или во время компиляции — в случае обобщений/генериков).
  • Код, использующий абстракцию, не меняется при добавлении новых реализаций.
  • Это фундамент для гибкости архитектуры, подмены зависимостей, тестируемости.

Формы полиморфизма (концептуально):

  1. Подтипный полиморфизм (основной в прикладном коде)

    • Разные типы реализуют один и тот же интерфейс/базовый контракт.
    • Вызывающий код работает через этот контракт.
    • В классических ООП-языках: через наследование и виртуальные методы.
    • В современных практиках и в Go: через интерфейсы.
  2. Параметрический полиморфизм (через generics)

    • Обобщённый код, который работает с разными типами, не завися от их конкретики.
    • В Go 1.18+ — через параметризованные типы и функции.
  3. Полиморфизм по перегрузке / ad-hoc (меньше актуален в Go)

    • Разные реализации под одно имя функции, отличающиеся сигнатурой.
    • В Go классической перегрузки по сигнатурам нет, это осознанное упрощение.

Полиморфизм в Go через интерфейсы

Go реализует полиморфизм в первую очередь через интерфейсы с неявной реализацией: если тип имеет нужные методы, он реализует интерфейс автоматически — без явного "implements".

Пример:

type Notifier interface {
Notify(msg string) error
}

type EmailNotifier struct {
Address string
}

func (e EmailNotifier) Notify(msg string) error {
fmt.Println("Email to", e.Address, ":", msg)
return nil
}

type SMSNotifier struct {
Phone string
}

func (s SMSNotifier) Notify(msg string) error {
fmt.Println("SMS to", s.Phone, ":", msg)
return nil
}

func SendAlert(n Notifier, msg string) {
// Полиморфизм: SendAlert не знает, Email это или SMS,
// оно опирается только на контракт Notifier.
_ = n.Notify(msg)
}

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

func main() {
email := EmailNotifier{Address: "user@example.com"}
sms := SMSNotifier{Phone: "+1234567890"}

SendAlert(email, "Server down")
SendAlert(sms, "High latency detected")
}

Здесь:

  • Notifier — абстракция (интерфейс);
  • EmailNotifier и SMSNotifier — разные реализации;
  • SendAlert — полиморфный код, который работает с любым типом, реализующим Notifier.

Это и есть практический полиморфизм.

Полиморфизм и тестируемость

Полиморфизм критичен для тестирования и архитектуры:

  • Позволяет подменять реальные реализации на моки/фейки в тестах, не меняя бизнес-код.

Пример:

type MockNotifier struct {
Messages []string
}

func (m *MockNotifier) Notify(msg string) error {
m.Messages = append(m.Messages, msg)
return nil
}

func TestSendAlert(t *testing.T) {
mock := &MockNotifier{}
SendAlert(mock, "Test notification")

if len(mock.Messages) != 1 || mock.Messages[0] != "Test notification" {
t.Fatalf("unexpected messages: %#v", mock.Messages)
}
}

Код SendAlert:

  • не знает, что работает с мок-объектом;
  • тест использует ту же абстракцию Notifier, это и есть полиморфизм.

Полиморфизм и generics в Go

С появлением generics в Go (параметрический полиморфизм):

func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}

Функция Min:

  • работает с разными типами (int, float64, string), не зная конкретного типа заранее;
  • это пример параметрического полиморфизма.

SQL-аналогия (концептуально)

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

  • Разные реализации хранилищ (PostgreSQL, MySQL, in-memory, mock) реализуют один и тот же контракт репозитория.
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
}
  • Код сервиса вызывает repo.GetByID, не зная и не завися от того, какой SQL или storage под капотом.
  • Мы можем заменить реализацию, не меняя код сервиса — это архитектурный полиморфизм.

Кратко:

  • Полиморфизм — способность писать код, работающий с разными реализациями через единый контракт.
  • В реальной разработке:
    • это достигается через интерфейсы и generics,
    • уменьшает связанность,
    • упрощает расширяемость и тестирование,
    • намного важнее, чем формальное зазубривание определения.

Вопрос 11. Что такое абстракция в ООП?

Таймкод: 00:06:22

Ответ собеседника: неполный. Называет абстрактные классы и "что-то абстрактное", но не раскрывает идею выделения существенных свойств и сокрытия лишних деталей.

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

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

Ключевые идеи абстракции:

  1. Фокус на сути, а не на деталях
  • Мы строим модель, которая:
    • скрывает сложность,
    • подчеркивает важные для домена свойства и операции.
  • Абстракция не про "абстрактные классы ради синтаксиса", а про осмысленные контрактные интерфейсы.

Примеры идей:

  • Пользователь домена:
    • важны id, имя, email, роли, инварианты;
    • не важны внутренние кеши, формат хранения в БД.
  • Хранилище:
    • важны операции: сохранить, прочитать, удалить;
    • не важно, Postgres там, Redis, Kafka, файл или in-memory map.
  1. Разделение "что" и "как"

Абстракция задает контракт:

  • "что доступно снаружи" (операции, методы, интерфейсы),
  • но не раскрывает реализацию.

Это:

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

Абстракция в Go (без "абстрактных классов")

Go не имеет абстрактных классов, но обеспечивает мощную абстракцию через интерфейсы и разбиение на пакеты.

Пример 1. Интерфейс репозитория как абстракция поверх БД:

type User struct {
ID int64
Email string
Name string
}

// Абстракция хранилища пользователей
type UserRepository interface {
Create(ctx context.Context, u *User) error
GetByID(ctx context.Context, id int64) (*User, error)
}

Реализация под капотом (Postgres):

type pgUserRepo struct {
db *sql.DB
}

func NewPgUserRepo(db *sql.DB) UserRepository {
return &pgUserRepo{db: db}
}

func (r *pgUserRepo) Create(ctx context.Context, u *User) error {
_, err := r.db.ExecContext(ctx,
`INSERT INTO users (id, email, name) VALUES ($1, $2, $3)`,
u.ID, u.Email, u.Name,
)
return err
}

func (r *pgUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
row := r.db.QueryRowContext(ctx,
`SELECT id, email, name FROM users WHERE id = $1`, id,
)
var u User
if err := row.Scan(&u.ID, &u.Email, &u.Name); err != nil {
return nil, err
}
return &u, nil
}

Сервис верхнего уровня использует UserRepository, не зная, что внутри — SQL, in-memory или mock:

type UserService struct {
repo UserRepository
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
return s.repo.GetByID(ctx, id)
}

Здесь:

  • UserRepository — абстракция;
  • pgUserRepo — конкретная реализация (инкапсулирует SQL и структуру таблиц);
  • бизнес-логика работает с абстракцией, а не с деталями.

Пример 2. Абстракция над механизмом доставки уведомлений:

type Notifier interface {
Notify(ctx context.Context, to, message string) error
}

Дальше могут быть реализации:

  • Email, SMS, Push, логгер для тестов. Код, который шлет уведомления, опирается на интерфейс, а не на конкретный транспорт.

Почему это важно на уровне зрелого разработчика:

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

Плохой vs хороший подход:

  • Плохо:
    • в каждом месте напрямую дергать *sql.DB, писать сырой SQL, знать структуру таблиц.
  • Хорошо:
    • иметь абстракцию репозитория/сервиса,
    • спрятать SQL, миграции, ретраи, кеш под капотом,
    • дать бизнес-коду работать с понятными методами: CreateOrder, ChargePayment, SendVerificationEmail.

Кратко:

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

Вопрос 12. Для чего используется ключевое слово super в Java?

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

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

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

Ключевое слово super в Java используется в контексте наследования и служит для явного обращения к членам (полям, методам, конструкторам) родительского (базового) класса из дочернего. Это важно для управления переопределением и инициализацией.

Основные применения super:

  1. Вызов конструктора родительского класса
  • В начале конструктора подкласса можно (и часто нужно) явно вызвать конструктор суперкласса.
  • Синтаксис: super(args);
  • Если не вызвать явно, Java попытается вызвать super() (конструктор без аргументов). Если его нет — будет ошибка компиляции.

Пример (Java):

class Base {
protected int id;

public Base(int id) {
this.id = id;
}
}

class Child extends Base {
private String name;

public Child(int id, String name) {
super(id); // вызов конструктора родительского класса
this.name = name; // инициализация своего поля
}
}

Зачем это нужно:

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

Пример:

class Base {
public void log(String msg) {
System.out.println("Base: " + msg);
}
}

class Child extends Base {
@Override
public void log(String msg) {
super.log(msg); // вызываем реализацию из Base
System.out.println("Child extra log: " + msg);
}
}

Зачем это нужно:

  • расширить поведение, а не полностью заменить;
  • сохранить часть логики базового класса.
  1. Обращение к полям и методам родительского класса при конфликте имен
  • Если в дочернем классе есть поле или метод с тем же именем, что и в родительском, super позволяет явно сослаться на версию из родителя.

Пример:

class Base {
protected String name = "base";
}

class Child extends Base {
protected String name = "child";

public void printNames() {
System.out.println(name); // "child"
System.out.println(super.name); // "base"
}
}

Основная идея:

  • super — инструмент управления наследованием:
    • явное указание: "используй реализацию/конструктор родительского класса";
    • помогает избежать двусмысленности и контролировать порядок инициализации/переопределения.

Связь с практикой и Go-контекстом:

  • В Go нет наследования классов и ключевого слова super, вместо этого:
    • используется композиция и embedding;
    • вызов "родительского" поведения делается через встраиваемое поле.

Условный аналог:

type Base struct{}

func (Base) Log(msg string) {
fmt.Println("Base:", msg)
}

type Child struct {
Base
}

func (c Child) Log(msg string) {
c.Base.Log(msg) // аналог super.log(msg)
fmt.Println("Child:", msg)
}

Кратко:

  • В Java super используется для:
    • вызова конструктора суперкласса,
    • вызова методов/доступа к полям суперкласса,
    • разрешения конфликтов имен при переопределении.

Вопрос 13. Для чего используется оператор instanceof в Java?

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

Ответ собеседника: неправильный. Говорит, что не встречал и не отвечает по сути.

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

В Java оператор instanceof используется для проверки, является ли объект экземпляром конкретного класса, подкласса или реализует ли он указанный интерфейс. Он работает на этапе выполнения (runtime) и опирается на реальный тип объекта, а не только на тип ссылки.

Общая форма:

obj instanceof SomeType
  • возвращает true, если:
    • obj не равен null, и
    • фактический тип объекта obj — это SomeType или его подкласс, или тип реализует интерфейс SomeType;
  • возвращает false в остальных случаях (включая obj == null).

Типичные применения:

  1. Проверка типа перед downcast

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

if (obj instanceof String) {
String s = (String) obj;
// работаем со строкой
}

Без такой проверки небезопасное приведение типа приведет к ClassCastException.

  1. Работа с полиморфизмом и разными реализациями

Иногда, особенно в legacy-коде или при работе с общими интерфейсами, используется instanceof для выбора поведения в зависимости от реального типа:

if (notifier instanceof EmailNotifier) {
// спец-логика для email
} else if (notifier instanceof SmsNotifier) {
// спец-логика для sms
}

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

  1. Проверка реализации интерфейсов
if (service instanceof AutoCloseable) {
((AutoCloseable) service).close();
}
  1. Pattern matching (современный Java-синтаксис)

В новых версиях Java instanceof поддерживает pattern matching:

if (obj instanceof String s) {
// сразу доступна переменная s нужного типа
System.out.println(s.toUpperCase());
}

Это делает код безопаснее и короче: проверка и приведение объединены.

Ограничения и нюансы:

  • Если типы вообще не совместимы на уровне компиляции, выражение instanceof недопустимо:
    • например, ("test" instanceof Integer) — ошибка на этапе компиляции.
  • null instanceof Type всегда false.

Архитектурный комментарий:

  • Частое использование instanceof в бизнес-логике — сигнал, что:
    • полиморфизм спроектирован плохо,
    • лучше выделить интерфейс или абстракцию и спрятать ветвление внутрь реализаций.
  • instanceof уместен:
    • в инфраструктурном коде (сериализация, логирование, адаптеры),
    • для защитных проверок,
    • при миграции и работе с устаревшими API.

Связь с Go:

  • В Go аналогом по смыслу можно считать type assertion и type switch:
var v any = getValue()

switch val := v.(type) {
case string:
fmt.Println("string:", val)
case int:
fmt.Println("int:", val)
default:
fmt.Println("unknown type")
}

Это концептуально близко к instanceof + pattern matching в Java, но встроено в модель типов Go.

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

Вопрос 14. Что такое принципы SOLID и в чем суть каждого из них?

Таймкод: 00:06:52

Ответ собеседника: неполный. Говорит, что знаком, но не может перечислить и объяснить принципы; ограничивается тем, что SOLID связан с ООП.

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

SOLID — это набор из пяти принципов проектирования, направленных на создание гибких, сопровождаемых и тестируемых систем. Хотя формулировались они для ООП, их идеи отлично применимы и в Go через композицию, интерфейсы, явные зависимости и чистые функции.

Разберем каждый принцип с практическим уклоном и примерами.

  1. Single Responsibility Principle (SRP)
    Принцип единственной ответственности

Суть:

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

Признаки нарушения:

  • "Бог-объекты"/"god packages", которые:
    • и ходят в БД,
    • и шлют email,
    • и реализуют бизнес-логику,
    • и парсят HTTP-запросы.

Практика в Go:

  • Разделяем:
    • слой HTTP/транспорт,
    • бизнес-логику (сервисы),
    • доступ к данным (репозитории),
    • инфраструктуру (логирование, кэш, клиенты).

Пример (плохой):

type UserHandler struct {
db *sql.DB
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// парсинг HTTP
// валидация
// бизнес-логика
// SQL-запросы
// логирование
}

Лучше:

type UserService interface {
Create(ctx context.Context, req CreateUserRequest) (*User, error)
}

type UserHandler struct {
svc UserService
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
// только HTTP-концерны + вызов svc
}

SRP упрощает тестирование, рефакторинг и локализацию изменений.

  1. Open/Closed Principle (OCP)
    Принцип открытости/закрытости

Суть:

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

Типичный антипример:

  • switch/if по типу/enum, который раздувается при каждом добавлении нового варианта.

Лучший подход:

  • использовать интерфейсы/стратегии/регистрации вместо разрастающихся ветвлений.

Пример (плохо):

func CalculatePrice(t string, base float64) float64 {
switch t {
case "standard":
return base
case "premium":
return base * 1.2
default:
return base
}
}

Каждый новый тариф — изменение этой функции.

Пример (лучше, через интерфейс):

type PricePolicy interface {
Calculate(base float64) float64
}

type StandardPolicy struct{}
func (StandardPolicy) Calculate(base float64) float64 { return base }

type PremiumPolicy struct{}
func (PremiumPolicy) Calculate(base float64) float64 { return base * 1.2 }

func FinalPrice(policy PricePolicy, base float64) float64 {
return policy.Calculate(base)
}

Добавление нового типа цены — новая реализация PricePolicy, существующий код не трогаем.

  1. Liskov Substitution Principle (LSP)
    Принцип подстановки Барбары Лисков

Суть (формально):

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

Проще:

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

Нарушения LSP:

  • "Подтип" кидает panic/ошибки для валидных кейсов базового контракта.
  • Меняет семантику:
    • базовый тип "сохраняет", а подтип "иногда не сохраняет".
  • Игнорирует инварианты.

В Go это критично:

  • Интерфейс задает контракт поведения, не только сигнатуры.
  • Если тип технически реализует интерфейс, но логически нарушает ожидания — это нарушение LSP.

Пример:

type Reader interface {
Read(p []byte) (n int, err error)
}

Реализация, которая всегда возвращает 0, nil и ничего не читает — формально подходит, но логически нарушает ожидания — плохая, ломает LSP.

Практика:

  • Проектируя интерфейсы, явно описывать поведение: ошибки, инварианты, гарантии.
  • Реализации обязаны этим гарантиям следовать.
  1. Interface Segregation Principle (ISP)
    Принцип разделения интерфейсов

Суть:

  • Клиенты не должны зависеть от методов, которые они не используют.
  • Лучше много маленьких специализированных интерфейсов, чем один "толстый".

Антипаттерн:

type Repository interface {
Create(...)
Get(...)
Update(...)
Delete(...)
List(...)
// + ещё 20 методов
}

Проблемы:

  • сложно мокать,
  • конкретные клиенты используют 1–2 метода и тащат весь "комбайн".

Лучше:

type UserReader interface {
GetByID(ctx context.Context, id int64) (*User, error)
List(ctx context.Context) ([]User, error)
}

type UserWriter interface {
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
}

В Go ISP особенно органичен:

  • интерфейсы обычно объявляются на стороне потребителя,
  • минимальные интерфейсы упрощают тестирование и переиспользование.
  1. Dependency Inversion Principle (DIP)
    Принцип инверсии зависимостей

Суть:

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

Практически:

  • бизнес-логика не должна напрямую зависеть от конкретной БД, HTTP-клиента, Kafka-клиента и т.п.;
  • вместо этого:
    • объявляем интерфейсы (контракты) на уровне домена,
    • инфраструктура (Postgres, Redis, HTTP) предоставляет реализации этих интерфейсов.

Пример (плохо):

type UserService struct {
db *sql.DB
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
// прямой SQL
}

Сервис напрямую зависит от деталей хранения.

Пример (правильно с DIP):

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

type UserService struct {
repo UserRepository
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
return s.repo.GetByID(ctx, id)
}

Реализация для Postgres:

type PgUserRepository struct {
db *sql.DB
}

func (r *PgUserRepository) GetByID(ctx context.Context, id int64) (*User, error) {
const q = `SELECT id, email, name FROM users WHERE id = $1`
var u User
if err := r.db.QueryRowContext(ctx, q, id).Scan(&u.ID, &u.Email, &u.Name); err != nil {
return nil, err
}
return &u, nil
}

Теперь:

  • UserService зависит от абстракции UserRepository, а не от деталей SQL.
  • В тестах можно подставить in-memory или mock реализацию.

Итоговые акценты:

  • SOLID — не про "зазубрил 5 букв", а про:
    • низкую связанность,
    • высокую связность внутри модуля,
    • явные абстракции,
    • тестируемость,
    • предсказуемую эволюцию кода.
  • В контексте Go:
    • вместо наследования: композиция + интерфейсы;
    • интерфейсы маленькие и объявляются на стороне потребителя;
    • зависимости инвертируются через конструкторы;
    • сервисы, репозитории, клиенты оформляются как явные абстракции.

Хороший ответ на интервью:

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

Вопрос 15. Что означают принципы DRY, KISS и YAGNI и как их применять на практике?

Таймкод: 00:07:41

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

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

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

Разберем каждый, с акцентом на бэкенд и Go.

DRY — Don't Repeat Yourself

Суть:

  • "Не повторяйся."
  • Каждое знание (правило, бизнес-логика, формат, алгоритм) должно иметь единственный источник истины (Single Source of Truth).
  • Речь не о банальном избегании копипасты текста, а об устранении дублирования знаний и ответственности.

Типичные нарушения DRY:

  • Одинаковая бизнес-логика валидации раскидана:
    • в UI,
    • в API,
    • в сервисе,
    • в БД-триггерах — и везде по-разному.
  • Дублирование структур и маппинга: одни и те же поля описаны в нескольких местах без единого контура.
  • Множество почти одинаковых SQL-запросов вместо одного параметризуемого.

Пример нарушения (Go):

// В одном месте:
if age < 18 {
return errors.New("too young")
}

// В другом:
if age <= 17 {
return errors.New("age is not allowed")
}

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

Применение DRY правильно:

  • Выделять общую бизнес-логику в функции/сервисы.
  • Общие валидации — в одну точку.
  • Общие SQL-конструкции — в репозитории, а не по всему коду.
  • Общие константы — в единый пакет/модуль.

Пример (Go):

func ValidateAge(age int) error {
if age < 18 {
return fmt.Errorf("age must be >= 18")
}
return nil
}

Теперь любое место использует одну функцию — меняем правило один раз.

Важно:

  • Не путать DRY с "любой ценой всё объединить":
    • выносить только устойчивые общие вещи;
    • не создавать абстракции ради двух строк.

KISS — Keep It Simple, Stupid

Суть:

  • "Делай проще."
  • Предпочитай простое, очевидное решение сложному, если оно покрывает требования.
  • Сложность — ваш враг: она удорожает поддержку, тестирование, онбординг и увеличивает вероятность багов.

Антипаттерны против KISS:

  • Ввод сложных абстракций, фабрик, DI-фреймворков, где достаточно простых конструкторов.
  • Переусложненные generic-решения, когда конкретный код проще и понятнее.
  • Избыточные уровни косвенности — "архитектура ради архитектуры".

Пример нарушения (Go):

// Сложный обобщённый event bus, DI, reflection
// для простого случая, где достаточно обычного вызова функции.

Применение KISS:

  • Писать прямолинейный код, который легко прочитать:
    • без лишних уровней абстракции;
    • без магии, скрытой в helper'ах на 300 строк.
  • Выбирать понятные решения:
    • простые структуры,
    • явные зависимости через конструктор,
    • минимальные интерфейсы.
  • Оптимизацию и усложнение — только при реальной необходимости (и с измерениями).

Пример (Go, просто и понятно):

type UserService struct {
repo UserRepository
}

func (s *UserService) Activate(ctx context.Context, id int64) error {
u, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if u.Active {
return nil
}
u.Active = true
return s.repo.Update(ctx, u)
}

Здесь:

  • минимум магии,
  • легко понять и протестировать.

YAGNI — You Ain't Gonna Need It

Суть:

  • "Тебе это не понадобится."
  • Не реализуй функциональность "на будущее", пока нет конкретной потребности.
  • Основан на том, что предположения о будущем часто неверны, а лишний код:
    • усложняет систему,
    • создает точки отказа,
    • требует поддержки и тестирования.

Антипаттерны против YAGNI:

  • "Сделаем супер-универсальный модуль, вдруг пригодится."
  • "Добавим 5 режимов конфигурации, пока не попросили."
  • "Сделаем агностик всех баз данных", имея только PostgreSQL и без реального кейса смены.

Последствия:

  • растет сложность,
  • увеличивается объем тестирования,
  • замедляются изменения,
  • команда боится трогать код.

Применение YAGNI:

  • Реализуем только подтвержденные требования.
  • Делаем код расширяемым, но не реализуем расширения без реального запроса.
  • Закладываем архитектуру, которая допускает эволюцию (через абстракции, SRP, DIP), но не пишем неиспользуемые ветки.

Пример нарушения (SQL + Go):

-- Десятки полей "на будущее", которые не используются.
ALTER TABLE users
ADD COLUMN backup_email TEXT,
ADD COLUMN legacy_id TEXT,
ADD COLUMN future_feature_flag BOOLEAN;
type User struct {
ID int64
Email string
BackupEmail *string // не используется
LegacyID *string // не используется
FutureFeatureOn bool // не используется
}

Правильно:

  • добавлять поля и функциональность, когда есть реальное бизнес-требование.

Связь между DRY, KISS, YAGNI и реальной разработкой

Эти принципы работают вместе:

  • DRY:
    • уменьшает дублирование знаний,
    • облегчает изменение бизнес-логики.
  • KISS:
    • контролирует сложность решений,
    • делает систему понятной и поддерживаемой.
  • YAGNI:
    • защищает от преждевременной архитектурной "инженерии",
    • снижает объем мертвого и неиспользуемого кода.

Практические инженерные выводы:

  • Перед созданием абстракции:
    • убедись, что есть минимум 2–3 реальных кейса, которые её оправдывают.
  • Перед добавлением функционала:
    • спроси: есть ли конкретный сценарий использования сейчас?
  • Перед копипастой:
    • оцени: это реально общая логика или пока лучше оставить дублирование, чем городить хрупкий "общий" модуль.
  • Придерживайся идеи:
    • сначала простое рабочее решение,
    • затем — рефакторинг, когда появляются реальные потребности и повторения.

Хороший ответ на интервью:

  • правильно расшифровывает:
    • DRY, KISS, YAGNI;
  • коротко объясняет суть:
    • DRY — одна точка истины;
    • KISS — не усложняй без нужды;
    • YAGNI — не делай "про запас";
  • приводит 1–2 практических примера из реальной разработки (желательно в контексте своего стека).

Вопрос 16. Для чего используются фреймворки JUnit и TestNG?

Таймкод: 00:07:53

Ответ собеседника: неполный. Упоминает, что сталкивался (особенно с JUnit) и что они нужны для написания автоматизированных тестов, без детализации возможностей и роли в процессе.

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

JUnit и TestNG — это тестовые фреймворки для Java, которые обеспечивают структурированный, повторяемый и автоматизируемый подход к написанию и запуску тестов. Их задача не только "давать писать автотесты", но и:

  • стандартизировать формат тестов,
  • обеспечить удобный lifecycle (setup/teardown),
  • дать механизм ассертов и проверок,
  • упростить организацию и группировку тестов,
  • интегрироваться с инструментами сборки и CI/CD,
  • поддержать параметризацию, запуск по группам, параллельное выполнение.

Ключевые особенности и зачем они нужны:

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

Пример на JUnit 5 (тест простой функции):

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class MathUtilTest {

@Test
void add_shouldReturnSum() {
int result = MathUtil.add(2, 3);
assertEquals(5, result);
}
}

Здесь:

  • @Test помечает тестовый метод;
  • assertEquals — часть API для проверок;
  • любой разработчик и CI понимает, как это запускать.
  1. Жизненный цикл теста (setup/teardown)

JUnit и TestNG предоставляют хуки для подготовки и очистки состояния:

  • JUnit:
    • @BeforeAll, @AfterAll,
    • @BeforeEach, @AfterEach.
  • TestNG:
    • @BeforeSuite, @AfterSuite,
    • @BeforeClass, @AfterClass,
    • @BeforeMethod, @AfterMethod.

Это нужно для:

  • инициализации тестового окружения (in-memory БД, mock-сервисы, тестовые данные),
  • очистки после запуска,
  • избежания копипасты сетапа в каждом тесте.
  1. Ассерты и работа с ошибками

Фреймворки предоставляют удобный API для проверок:

  • JUnit: assertEquals, assertTrue, assertThrows, и др.
  • TestNG: Assert.assertEquals, assertTrue, и т.д.

Это:

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

Особенно в TestNG (и частично в JUnit 5):

  • Группы тестов:
    • можно пометить тесты как smoke, regression, integration, slow и запускать выборочно.
  • Параметризованные тесты:
    • запуск одного и того же теста с разными входными данными.
  • Зависимости между тестами (в TestNG):
    • dependsOnMethods, dependsOnGroups.

Это важно для крупных проектов:

  • можно выстраивать разные пайплайны:
    • быстрые проверочные тесты,
    • полный регресс,
    • интеграционные тесты отдельно.
  1. Интеграция с инструментами сборки и CI/CD

JUnit и TestNG:

  • нативно поддерживаются Maven, Gradle, IDE (IntelliJ IDEA, Eclipse),
  • формируют стандартные отчеты (XML/HTML),
  • хорошо интегрируются с:
    • Jenkins,
    • GitLab CI,
    • GitHub Actions,
    • TeamCity,
    • SonarQube и т.п.

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

  • автоматически прогонять тесты при каждом коммите/merge,
  • падать пайплайн при регрессии,
  • собирать метрики покрытия и качества.
  1. Почему это важно понимать даже Go-разработчику

Хотя вопрос про Java-стек, по сути JUnit/TestNG в экосистеме Java — аналог пакета testing и экосистемы вокруг него в Go:

  • Go:
    • использует стандартный testing пакет:
      • go test,
      • *_test.go,
      • бенчмарки, примеры,
    • плюс дополнительные библиотеки (testify, gomega и др.).
  • Java:
    • использует JUnit/TestNG как стандартный фундамент для тестов.

Общие идеи:

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

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

  • JUnit и TestNG — фреймворки модульного и интеграционного тестирования в Java.
  • Они позволяют:
    • объявлять тесты через аннотации,
    • управлять жизненным циклом тестов (setup/teardown),
    • использовать ассерты,
    • группировать и параметризовывать тесты,
    • интегрировать тесты с Maven/Gradle и CI.
  • Их роль — основа автоматизированного тестирования в Java-проектах, примерно как go test и вспомогательные библиотеки в Go.

Вопрос 17. Что такое аннотация в Java и какие основные аннотации JUnit используются?

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

Ответ собеседника: неполный. Путается с given, приводит только @Test как пример пометки тестового метода; идею аннотации в целом улавливает, но набор и назначение ключевых аннотаций JUnit объясняет слабо.

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

Аннотация в Java — это специальная мета-информация, которая добавляется к классам, методам, полям, параметрам и т.д. и может использоваться:

  • компилятором (например, для предупреждений, генерации кода);
  • рантаймом (через reflection);
  • фреймворками и библиотеками (JUnit, Spring, Hibernate и др.) для изменения поведения без дополнительного "боилерплейта" кода.

Аннотация не меняет логику сама по себе, но является декларативным способом сообщить фреймворку: "с этим элементом нужно поступить особым образом".

Общий вид аннотации:

@Test
void shouldDoSomething() {
...
}

Здесь @Test — аннотация, которая говорит JUnit: этот метод нужно выполнить как тест.

Основные аннотации JUnit (актуально для JUnit 5; укажу отличия от JUnit 4, т.к. это часто спрашивают)

  1. @Test
  • Помечает метод как тестовый.
  • JUnit запускает такие методы как тесты.
  • В JUnit 5 можно добавлять атрибуты, например @Test + @DisplayName.

Пример:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class MathUtilTest {

@Test
void add_shouldReturnSum() {
assertEquals(5, MathUtil.add(2, 3));
}
}
  1. @BeforeEach / @AfterEach (JUnit 5) (@Before / @After в JUnit 4)
  • @BeforeEach:
    • метод выполняется перед КАЖДЫМ тестовым методом.
    • используют для подготовки окружения:
      • создание объектов,
      • очистка/инициализация данных.
  • @AfterEach:
    • метод выполняется после КАЖДОГО теста.
    • используют для очистки:
      • закрытие соединений,
      • удаление временных файлов.

Пример:

import org.junit.jupiter.api.*;

class UserServiceTest {
UserService service;

@BeforeEach
void setUp() {
service = new UserService(...); // инициализация перед каждым тестом
}

@AfterEach
void tearDown() {
// очистка, если нужна
}

@Test
void testCreateUser() {
// используем уже подготовленный service
}
}
  1. @BeforeAll / @AfterAll (JUnit 5) (@BeforeClass / @AfterClass в JUnit 4)
  • @BeforeAll:
    • метод выполняется один раз перед всеми тестами класса.
    • обычно используется для:
      • поднятия in-memory БД,
      • запуска testcontainers,
      • тяжелых инициализаций.
  • @AfterAll:
    • один раз после всех тестов.
    • используется для:
      • остановки контейнеров,
      • финальной очистки.

Требования:

  • В JUnit 5 метод с @BeforeAll/@AfterAll по умолчанию должен быть static (если не включен per-class lifecycle).

Пример:

class IntegrationTest {

@BeforeAll
static void initAll() {
// старт testcontainers, миграции БД
}

@AfterAll
static void tearDownAll() {
// остановка контейнеров
}

@Test
void testScenario() {
// интеграционный тест
}
}
  1. @Disabled (JUnit 5) (@Ignore в JUnit 4)
  • Помечает тест или класс тестов как пропущенный.
  • Используется для временного выключения тестов (например, нестабильных или требующих специфичного окружения).
  • Важно:
    • не злоупотреблять,
    • документировать причину.

Пример:

@Disabled("Needs external service, fix later")
@Test
void externalServiceTest() {
// не будет выполняться
}
  1. @DisplayName (JUnit 5)
  • Позволяет задать человекочитаемое имя теста в отчетах.
  • Удобно для документации поведения.
@DisplayName("Should return 404 for non-existing user")
@Test
void nonExistingUserReturns404() {
...
}
  1. Параметризированные тесты (JUnit 5)
  • @ParameterizedTest:
    • позволяет запускать один тестовый метод с разными входными данными.
  • В связке с:
    • @ValueSource,
    • @CsvSource,
    • @MethodSource,
    • и т.д.

Пример:

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class EvenTest {

@ParameterizedTest
@ValueSource(ints = {2, 4, 6})
void isEven_shouldReturnTrueForEvenNumbers(int number) {
assertTrue(NumberUtil.isEven(number));
}
}

Это реализует техники тест-дизайна (эквивалентные классы, граничные значения) в удобной форме.

  1. Аннотации для условий выполнения
  • @EnabledOnOs, @DisabledOnOs,
  • @EnabledOnJre, @DisabledOnJre,
  • @EnabledIf, @DisabledIf,
  • и т.п.

Они позволяют:

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

Зачем это всё важно (и как правильно объяснить):

  • Аннотации в контексте JUnit:

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

    • дает определение аннотации;
    • называет ключевые аннотации хотя бы для JUnit 4 или 5:
      • @Test,
      • @BeforeEach/@AfterEach (или @Before/@After),
      • @BeforeAll/@AfterAll (или @BeforeClass/@AfterClass),
      • @Disabled/@Ignore,
      • плюс упоминание параметризированных тестов;
    • показывает понимание, что это механизм, позволяющий фреймворку управлять тестами без явного "ручного" кода.

Если переводить аналогию на Go:

  • В Go нет аннотаций, но роль "маркировки тестов" выполняет соглашение:
    • файл *_test.go,
    • функции TestXxx(t *testing.T),
    • фреймворк go test по сигнатуре и имени сам понимает, что это тест.
  • В Java эту роль берут на себя аннотации JUnit.

Вопрос 18. Что такое API и как оно расшифровывается?

Таймкод: 00:09:35

Ответ собеседника: неполный. Описывает как взаимодействие клиента и сервера через методы; неверно расшифровывает как Application Interface Programming.

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

API расшифровывается как Application Programming Interface — интерфейс программирования приложений.

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

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

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

  1. Контракт между системами

API — это не "просто методы сервера", а четко определенный контракт.

  • Для HTTP-сервисов:
    • URI-эндпоинты,
    • HTTP-методы (GET, POST, PUT, DELETE, PATCH),
    • структуры запросов (JSON, Protobuf, form-data),
    • структуры ответов,
    • коды статусов (200, 201, 400, 401, 404, 500, и т.д.),
    • требования к аутентификации/авторизации (Bearer токен, API ключи и т.п.).

Пример (REST API контракт, упрощенно):

  • POST /users
    • Request body:
      • {"email": "user@example.com", "name": "John"}
    • Response:
      • 201 Created
      • {"id": 123, "email": "user@example.com", "name": "John"}
  1. Абстракция и сокрытие реализации

API:

  • говорит "что можно сделать",
  • но не раскрывает "как оно сделано внутри":
    • БД, кеши, брокеры сообщений, алгоритмы и т.д. скрыты от клиента.
  • Это:
    • снижает связанность,
    • позволяет менять внутреннюю реализацию без ломки клиентов, если контракт API остается стабильным.
  1. Типы API (важные для бэкенда)
  • Внешние (public / partner API):
    • используются другими командами, клиентами, партнерами.
    • высокие требования к стабильности, версионированию, документации.
  • Внутренние (internal API):
    • взаимодействие микросервисов внутри компании.
  • Языковые (library API):
    • функции, методы и интерфейсы библиотек и пакетов (в том числе в Go).
  • OS / платформенные API:
    • системные вызовы, драйверы, SDK.
  1. API в контексте Go-сервисов

На практике под API часто имеют в виду:

  • HTTP/REST API,
  • gRPC API,
  • GraphQL,
  • внутренние сервисные контракты.

Пример простого HTTP API на Go:

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

func createUserHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}

var req struct {
Email string `json:"email"`
Name string `json:"name"`
}

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

// здесь могла бы быть бизнес-логика и сохранение в БД
user := User{ID: 1, Email: req.Email, Name: req.Name}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(user)
}

Этот обработчик — часть API:

  • контракт: POST /users
  • вход: JSON с email и name,
  • выход: 201 + JSON с созданным пользователем.

SQL-слой под капотом — реализация, скрытая за API:

INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id;
  1. Важные аспекты хорошего API

Для уровня сильного инженера важно понимать не только определение, но и критерии качественного API:

  • Ясность и предсказуемость:
    • понятные URL,
    • консистентные коды ответов,
    • единый стиль ошибок.
  • Версионирование:
    • аккуратные изменения без внезапного лома клиентов.
  • Документация:
    • OpenAPI/Swagger для REST,
    • proto-файлы для gRPC.
  • Безопасность:
    • аутентификация, авторизация,
    • rate limiting,
    • защита данных.
  • Тестируемость:
    • контрактные тесты,
    • интеграционные тесты,
    • стабильное поведение.

Кратко:

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

Вопрос 19. Какие инструменты используются для ручного тестирования и для чего они нужны?

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

Ответ собеседника: неполный. Лично почти не использовал; называет Postman, далее обобщает ручное тестирование как black-box через интерфейс без доступа к коду, но не раскрывает инструменты и их назначение.

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

Ручное тестирование — это не только "кликание по UI", а осознанная проверка функционала, API, интеграций и нефункциональных аспектов без (или до) полной автоматизации. Для эффективного ручного тестирования используются специализированные инструменты. Сильный ответ показывает знание ключевых классов инструментов и умеет связать их с типами проверок.

Основные категории и примеры инструментов:

  1. Инструменты для тестирования API

Используются для ручной проверки REST/gRPC/GraphQL API, валидации контрактов, отладки.

  • Postman

    • Ручные запросы к API (GET/POST/PUT/DELETE и пр.).
    • Работа с заголовками, токенами (Bearer, API key), cookies.
    • Коллекции запросов для разных окружений.
    • Валидация ответов, черновики для будущих автотестов.
    • Удобен и для разработчиков, и для QA.
  • Insomnia, Hoppscotch, Bruno и аналоги

    • Альтернативы Postman для API-запросов.
    • Поддержка переменных окружения, коллекций, авторизации.

Практическая роль:

  • Быстро проверить эндпоинт, сценарий, контракт.
  • Репродуцировать баг, прислать точный запрос/ответ.
  • Служат "ручным клиентом" для backend-API до/вместо UI.
  1. Инструменты для тестирования веб-интерфейса (UI)

Используются для ручной проверки фронтенда, верстки, поведения в браузере.

  • DevTools в браузерах (Chrome, Firefox)

    • Network: отслеживание HTTP-запросов/ответов, статусов, заголовков.
    • Console: ошибки JS.
    • Application/Storage: cookies, localStorage, sessionStorage.
    • Performance: время загрузки, профилирование.
    • Responsive Mode: проверка адаптивности.
  • Browser plugins:

    • для проверки доступности (a11y),
    • для анализа верстки и стилей.

Практическая роль:

  • Проверить корректность UI, интеграцию с API, ошибки в сети.
  • Найти и воспроизвести баги, связанные с клиентской логикой.
  1. Инструменты для работы с запросами, трафиком и прокси

Используются для анализа, перехвата, модификации запросов и ответов.

  • Fiddler, Charles Proxy, mitmproxy
    • Перехват и просмотр HTTP/HTTPS трафика.
    • Редактирование запросов/ответов на лету.
    • Поиск проблем:
      • некорректные заголовки,
      • куки, редиректы,
      • кеширование,
      • безопасность.

Практическая роль:

  • Диагностика сложных проблем взаимодействия фронт/бэк/мобильные клиенты.
  • Тестирование поведения при измененных/поврежденных запросах.
  1. Инструменты для работы с БД и SQL

Ручное тестирование часто включает проверку данных, целостности и побочных эффектов.

  • pgAdmin, DBeaver, DataGrip, TablePlus, SQLyog и др.
    • Просмотр данных.
    • Выполнение SQL-запросов.
    • Проверка, что после действий в UI/API данные корректно записались.

Пример SQL-проверки после ручного теста API:

SELECT id, email, name
FROM users
WHERE email = 'test@example.com';

Практическая роль:

  • Подтвердить, что бизнес-операции оставляют систему в корректном состоянии.
  • Проверить миграции, ограничения, каскадное удаление и т.п.
  1. Инструменты для тестирования мобильных приложений
  • Android Studio Emulator, Xcode Simulator.
  • Appium Inspector и прочие.
  • Используются для ручной проверки мобильных клиентов, верстки, интеграции с API.

Практическая роль:

  • Проверка UI/UX, сетевых запросов, поведения оффлайн/онлайн.
  1. Инструменты для отслеживания логов, метрик и ошибок

Даже в ручном тестировании важно смотреть, как ведет себя backend.

  • Kibana, Grafana, Loki, Splunk, ELK-стэк.
  • Sentry, Rollbar и подобные.
  • journalctl / docker logs / kubectl logs.

Практическая роль:

  • При ручном воспроизведении бага сразу смотреть:
    • что в логах сервиса,
    • есть ли stack trace,
    • как меняются метрики (ошибки, latency).
  1. Инструменты для тест-менеджмента и документации

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

  • TestRail, Zephyr, Qase, Xray, TestLink.
    • Хранение тест-кейсов.
    • Планирование регрессий.
    • Привязка тестов к требованиям и багам.
  • Jira/YouTrack/Trello:
    • фиксация дефектов,
    • отслеживание статуса.

Практическая роль:

  • Структурировать ручные проверки:
    • какие кейсы прошли,
    • какие упали,
    • какие покрывают критичные требования.
  1. Инструменты для тестирования производительности и безопасности (в полу-ручном режиме)

Хотя это уже ближе к специализированному/автоматизированному тестированию, для зрелого ответа уместно помнить:

  • JMeter, k6, Gatling:
    • запуск нагрузочных сценариев (часто управляются вручную для исследования).
  • OWASP ZAP, Burp Suite:
    • ручное и полуавтоматическое тестирование безопасности:
      • SQL-инъекции,
      • XSS,
      • CSRF,
      • неправильная авторизация.

Практическая роль:

  • Ручной исследовательский анализ уязвимостей и поведения под нагрузкой.

Как связать это в ответе кратко и по делу:

Хороший ответ на интервью мог бы быть таким:

  • Инструменты для ручного тестирования:
    • Postman / Insomnia — ручная проверка REST/gRPC API.
    • Браузерные DevTools — анализ UI, запросов, ошибок.
    • Charles / Fiddler / mitmproxy — перехват и модификация HTTP-трафика.
    • DBeaver / DataGrip / pgAdmin — проверка состояния БД после операций.
    • TestRail / Qase — ведение тест-кейсов и результатов.
    • Sentry / Kibana / Grafana — анализ логов и ошибок при ручных сценариях.
  • И объяснить:
    • ручное тестирование — это обычно black-box (работаем через UI или API по спецификации),
    • но в зрелых командах оно почти всегда опирается и на внутренние инструменты: логи, БД, трейсинг, чтобы глубже понять поведение системы.

Такой ответ показывает не только знание названий, но и понимание, как именно эти инструменты помогают качественно проверить backend и интеграции.

Вопрос 20. Какие основные HTTP-методы используются при работе с веб-сервисами?

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

Ответ собеседника: правильный. Перечисляет GET, POST, PUT, DELETE.

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

Для работы с веб-сервисами (особенно в REST-архитектуре) основными считаются следующие HTTP-методы:

  1. GET

    • Назначение:
      • Получение (чтение) ресурса или коллекции ресурсов.
    • Характеристики:
      • Не должен менять состояние сервера (safe).
      • Должен быть идемпотентным (повторные вызовы дают тот же результат с точки зрения состояния).
      • Можно кешировать (при правильных заголовках).
    • Примеры:
      • GET /users — получить список пользователей.
      • GET /users/123 — получить пользователя с id=123.
  2. POST

    • Назначение:
      • Создание нового ресурса.
      • Выполнение операций, которые не вписываются в CRUD по конкретному ресурсу (команды, логин, сложные запросы).
    • Характеристики:
      • Не является идемпотентным по определению (повтор может создать несколько ресурсов или повторно выполнить действие).
    • Примеры:
      • POST /users — создать пользователя.
      • POST /auth/login — выполнить логин и получить токен.
  3. PUT

    • Назначение:
      • Полная замена состояния ресурса по указанному URI.
    • Характеристики:
      • Идемпотентен: повторный одинаковый запрос должен приводить к одному и тому же состоянию ресурса.
      • Обычно требует передачу полной репрезентации ресурса.
    • Пример:
      • PUT /users/123
        • тело запроса содержит полное новое представление пользователя;
        • после запроса ресурс должен точно соответствовать переданным данным.
  4. DELETE

    • Назначение:
      • Удаление ресурса.
    • Характеристики:
      • Идемпотентен: повторный DELETE для уже удаленного ресурса обычно возвращает 404/204, но состояние (ресурс отсутствует) не меняется.
    • Пример:
      • DELETE /users/123 — удалить пользователя.
  5. PATCH (важно упомянуть как часть сильного ответа)

Хотя кандидат не обязан был назвать PATCH для признания ответа корректным, для полноты:

  • Назначение:
    • Частичное изменение ресурса.
    • В отличие от PUT, не требует полного состояния ресурса.
  • Примеры:
    • PATCH /users/123 с телом {"name": "New Name"} — изменить только имя.

Практический пример на Go (обработчики основных методов):

func userHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// чтение пользователя
case http.MethodPost:
// создание пользователя
case http.MethodPut:
// полное обновление пользователя
case http.MethodDelete:
// удаление пользователя
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}

Пример SQL-операций, соответствующих методам:

  • GET:
    SELECT id, email, name FROM users WHERE id = $1;
  • POST:
    INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id;
  • PUT/PATCH:
    UPDATE users SET email = $1, name = $2 WHERE id = $3;
  • DELETE:
    DELETE FROM users WHERE id = $1;

Ключевые инженерные акценты:

  • Понимать семантику идемпотентности и "safety":
    • GET — безопасен, не меняет состояние.
    • PUT/DELETE — меняют состояние, но идемпотентны.
    • POST — не идемпотентен.
  • Проектировать API так, чтобы поведение методов соответствовало их стандартной семантике:
    • это упрощает кэширование, ретраи, балансировку, работу клиентов и прокси.
  • Для продуманного REST API важно использовать методы осознанно, а не просто "все через POST".

Вопрос 21. Какие основные элементы библиотеки для REST-тестирования используются (given/when/then) и в чем их назначение?

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

Ответ собеседника: неполный. Говорит, что использовал given, when, then: given описывает данные запроса, when — метод и endpoint, then — проверку ответа и тела; понимание базовое, но не полностью корректное и без деталей.

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

Под "основными элементами библиотеки для REST-тестирования" в таком контексте обычно подразумевают BDD-стиль и fluent-API, как, например, в Rest Assured (Java) или аналогичных инструментах, где используется цепочка:

  • given()
  • when()
  • then()

Это реализация концепции Given-When-Then из BDD (Behavior-Driven Development) и паттерна Arrange-Act-Assert из классического тестирования.

Общая идея:

  • Given — подготовка (контекст, данные, настройки запроса).
  • When — действие (вызов HTTP-метода/endpoint'а).
  • Then — проверка (assert'ы на ответ: статус, заголовки, тело, структура).

Рассмотрим детально.

  1. Given — подготовка запроса (Arrange / Given)

Назначение:

  • Описывает предусловия и конфигурацию HTTP-запроса:
    • базовый URL (если не вынесен глобально),
    • заголовки,
    • аутентификация,
    • query-параметры,
    • path-параметры,
    • тело запроса,
    • тип контента,
    • логирование и др.

Типичные элементы:

  • baseUri, basePath
  • header, cookie
  • auth()
  • queryParam, pathParam
  • body()
  • включение логов для отладки

Пример (Rest Assured, Java):

given()
.baseUri("https://api.example.com")
.basePath("/users")
.header("Authorization", "Bearer " + token)
.contentType("application/json")
.body("{\"email\":\"test@example.com\",\"name\":\"Test User\"}");

Смысл:

  • здесь мы полностью готовим все, что нужно отправить — контекст и данные.
  1. When — выполнение запроса (Act / When)

Назначение:

  • Описывает конкретное действие:
    • какой HTTP-метод вызываем,
    • на какой endpoint.

В BDD-терминах:

  • "Когда пользователь выполняет X".

Пример:

.when()
.post(); // или .get(), .put("/123"), .delete("/123"), и т.п.

Важно:

  • when() не задает данные, только инициирует вызов, используя то, что описано в given().
  • Логика: сначала мы полностью формируем контекст (given), затем выполняем действие (when).
  1. Then — проверка результата (Assert / Then)

Назначение:

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

Включает:

  • assert'ы на:
    • статус (200, 201, 400, 404, 500),
    • значения полей (id, email, error message),
    • структуры.

Пример:

.then()
.statusCode(201)
.contentType("application/json")
.body("id", notNullValue())
.body("email", equalTo("test@example.com"));

Полный пример сценария (Rest Assured, BDD-стиль)

given()
.baseUri("https://api.example.com")
.basePath("/users")
.contentType("application/json")
.body("{\"email\":\"test@example.com\",\"name\":\"Test User\"}")
.when()
.post()
.then()
.statusCode(201)
.body("email", equalTo("test@example.com"))
.body("id", notNullValue());

Связь с принципами тест-дизайна и REST-контрактами:

  • Given:
    • задает предусловия и тестовые данные (preconditions, test data).
  • When:
    • реализует действие по контракту API (HTTP-метод + endpoint).
  • Then:
    • формализует ожидаемый результат (expected result), что является ключевым элементом корректного тест-кейса.

Аналогия с Go (Arrange-Act-Assert для REST-тестов)

Хотя в Go не используется given/when/then в виде аннотаций, тот же подход реализуется явно:

func TestCreateUser(t *testing.T) {
// Given: тестовый сервер и данные
srv := httptest.NewServer(setupRouter())
defer srv.Close()

body := `{"email":"test@example.com","name":"Test User"}`

// When: выполняем POST /users
resp, err := http.Post(srv.URL+"/users", "application/json", strings.NewReader(body))
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()

// Then: проверяем статус и тело
if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected 201, got %d", resp.StatusCode)
}

var res struct {
ID int64 `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatalf("decode response: %v", err)
}
if res.Email != "test@example.com" {
t.Fatalf("unexpected email: %s", res.Email)
}
}

Тут явно прослеживается та же структура:

  • Given: настройка сервера и тела.
  • When: HTTP-вызов.
  • Then: проверки.

Ключевые моменты сильного ответа:

  • Понимать, что given/when/then:
    • не магические слова, а BDD-паттерн структурирования теста.
  • Корректно описать:
    • given — подготовка контекста и запроса,
    • when — выполнение действия (HTTP-метода),
    • then — проверка результата (статус, заголовки, тело, контракт).
  • Упомянуть:
    • это повышает читаемость тестов,
    • помогает явно разделить подготовку, действие и проверку,
    • хорошо ложится на тест-кейсы и API-контракты.

Вопрос 22. Какие существуют группы HTTP-статусов и что они означают?

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

Ответ собеседника: неполный. Называет диапазоны 1xx–5xx и их общее назначение (информационные, успешные, редиректы, ошибки клиента, ошибки сервера), но без четких формулировок и примеров.

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

Коды состояния HTTP делятся на 5 основных классов по первой цифре. Понимание их семантики критично для проектирования корректных REST/gRPC-gateway API, обработки ошибок, ретраев, балансировки и логирования.

  1. 1xx — Informational (Информационные)
  • Предварительные ответы, используемые в специфичных сценариях.
  • Клиент обычно не взаимодействует с ними напрямую.
  • Примеры:
    • 100 Continue:
      • сервер сообщает, что заголовки приняты, клиент может отправлять тело.
    • 101 Switching Protocols:
      • переход на другой протокол (например, WebSocket).
  • В прикладных REST-сервисах используются редко; часто можно игнорировать.
  1. 2xx — Success (Успешные ответы)

Означают, что запрос успешно обработан.

Ключевые:

  • 200 OK:
    • успешный запрос (чаще для GET, успешных операций без создания).
    • Может содержать тело с данными.
  • 201 Created:
    • ресурс успешно создан.
    • Обычно для POST.
    • Желательно возвращать Location и/или тело с созданным ресурсом.
  • 202 Accepted:
    • запрос принят к обработке, но ещё не завершен (async-процессы, очереди).
  • 204 No Content:
    • успешный запрос, но без тела ответа.
    • Часто для DELETE или операций обновления, где тело не нужно.

Практические акценты:

  • 2xx-коды используются как сигнал успеха для клиентов, ретраев и health-check'ов.
  • Важно выбирать корректный код, а не везде 200.
  1. 3xx — Redirection (Перенаправления)

Сообщают клиенту, что ресурс доступен по другому URL или требуется повторный запрос.

Ключевые:

  • 301 Moved Permanently:
    • постоянный редирект; клиентам и поисковикам следует запомнить новый URL.
  • 302 Found / 307 Temporary Redirect:
    • временный редирект; URL может поменяться позже.
  • 304 Not Modified:
    • ресурс не изменился с момента, указанного в заголовках If-Modified-Since / If-None-Match;
    • используется для кеширования.

Практика:

  • В API они используются реже, но:
    • 304 важен для кеширования,
    • корректное поведение с редиректами важно для клиентов и прокси.
  1. 4xx — Client Error (Ошибки клиента)

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

Ключевые:

  • 400 Bad Request:
    • некорректный запрос (битый JSON, невалидные параметры).
  • 401 Unauthorized:
    • требуется аутентификация или неверные креды.
    • Обычно используется вместе с WWW-Authenticate.
  • 403 Forbidden:
    • аутентификация есть, но нет прав на действие.
  • 404 Not Found:
    • ресурс не найден или скрыт по соображениям безопасности.
  • 405 Method Not Allowed:
    • HTTP-метод не поддерживается для этого URL.
  • 409 Conflict:
    • конфликт состояния (например, уникальность, версия ресурса).
  • 422 Unprocessable Entity:
    • семантически неверные данные (часто для валидации).
  • 429 Too Many Requests:
    • превышен лимит запросов (rate limiting).

Практика:

  • 4xx-коды, как правило, не требуют ретраев со стороны клиента без изменения запроса.
  • Важно возвращать информативное тело с описанием ошибки:
    • код ошибки,
    • сообщение,
    • детали валидации.
  1. 5xx — Server Error (Ошибки сервера)

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

Ключевые:

  • 500 Internal Server Error:
    • общая ошибка сервера (panic, необработанное исключение, непредвиденная ситуация).
  • 502 Bad Gateway:
    • проблема на прокси/шлюзе: неверный ответ от upstream-сервиса.
  • 503 Service Unavailable:
    • сервис временно недоступен (перегрузка, maintenance).
  • 504 Gateway Timeout:
    • таймаут при ожидании ответа от upstream-сервиса.

Практические моменты:

  • 5xx-коды:
    • обычно допускают автоматические ретраи (с бэкоффом),
    • требуют мониторинга, алёртов и расследования.
  • Нельзя использовать 5xx, чтобы "маскировать" ошибки клиента — это ломает логику клиентов и балансировщиков.

Инженерные акценты для уверенного ответа:

  • Осознанно выбирать коды:
    • успехи: 200/201/204,
    • валидация и формат: 400/422,
    • аутентификация/авторизация: 401/403,
    • отсутствие ресурса: 404,
    • конфликты: 409,
    • лимиты: 429,
    • внутренние сбои: 5xx.
  • Понимать влияние на:
    • кеширование,
    • retry-политику,
    • балансировщики и API-gateway,
    • мониторинг и алертинг.
  • В сочетании с Go:
    • использовать http.StatusXXX константы,
    • не возвращать 200 при ошибке бизнес-логики.

Пример (Go):

func getUserHandler(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest) // 400
return
}

user, err := svc.GetUser(r.Context(), id)
if errors.Is(err, ErrNotFound) {
http.Error(w, "not found", http.StatusNotFound) // 404
return
}
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError) // 500
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) // 200
_ = json.NewEncoder(w).Encode(user)
}

Кратко:

  • 1xx — информационные;
  • 2xx — успешные;
  • 3xx — редиректы;
  • 4xx — ошибки клиента;
  • 5xx — ошибки сервера.

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

Вопрос 23. Из каких основных частей состоит HTTP-запрос?

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

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

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

HTTP-запрос в классическом виде состоит из четырех ключевых частей:

  1. Стартовая строка (Request Line)
  2. Заголовки (Headers)
  3. Пустая строка (разделитель)
  4. Тело сообщения (Body) — опционально

Важно понимать структуру на низком уровне, потому что это влияет на парсинг, безопасность, диагностику, работу прокси и реализацию HTTP-серверов/клиентов (в том числе в Go).

Разберем по частям.

  1. Стартовая строка (Request Line)

Формат:

  • METHOD SP REQUEST-TARGET SP HTTP-VERSION

Примеры:

  • GET /users HTTP/1.1
  • GET /users/123?verbose=true HTTP/1.1
  • POST /users HTTP/1.1

Состоит из:

  • Метод (HTTP method):
    • GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD и др.
  • Request target (обычно path + query):
    • путь ресурса: /users/123
    • query-параметры: ?page=2&limit=10
  • Версия протокола:
    • HTTP/1.1, HTTP/2 (в HTTP/2 формат на уровне фреймов другой, но логическая модель сохраняется).

Роль:

  • Однозначно определяет действие, ресурс и ожидаемый протокол.
  1. Заголовки (Headers)

Располагаются сразу после стартовой строки, каждый с новой строки.

Формат:

  • Header-Name: value

Назначение:

  • Передача метаданных о запросе, клиенте, теле и контексте:
    • Идентификация и маршрутизация:
      • Host: домен/хост (обязателен в HTTP/1.1).
      • X-Request-ID, Traceparent, X-Correlation-ID.
    • Формат тела:
      • Content-Type: тип содержимого (например, application/json).
      • Content-Length: длина тела.
      • Transfer-Encoding: chunked.
    • Аутентификация и безопасность:
      • Authorization: Bearer <token>.
      • Cookie.
    • Кеширование:
      • If-None-Match, If-Modified-Since, Cache-Control.
    • Client hints, User-Agent и т.д.

Примеры:

Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
Accept: application/json

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

  1. Пустая строка
  • Одна пустая строка отделяет заголовки от тела.
  • Это важный структурный разделитель.
  • После нее (если есть данные) начинается тело запроса.
  1. Тело (Body) — опционально

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

  • в методах, которые передают данные на сервер:
    • POST (создание ресурса),
    • PUT/PATCH (обновление),
    • иногда DELETE (редко, но возможно),
  • для передачи:
    • JSON, XML,
    • форм-данных (application/x-www-form-urlencoded, multipart/form-data),
    • бинарных данных (файлы),
    • Protobuf и других форматов.

Пример тела:

{
"email": "test@example.com",
"name": "Test User"
}

Связь между частями на примере полного HTTP-запроса

POST /users?invite=true HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...
Content-Type: application/json
Accept: application/json

{
"email": "test@example.com",
"name": "Test User"
}
  • Стартовая строка:
    • POST /users?invite=true HTTP/1.1
  • Заголовки:
    • Host, Authorization, Content-Type, Accept
  • Пустая строка:
    • разделяет заголовки и тело
  • Тело:
    • JSON с данными пользователя

Как это выглядит в Go (http.Request)

В Go структура http.Request отражает те же части:

func handler(w http.ResponseWriter, r *http.Request) {
// Стартовая строка / Request Line:
method := r.Method // GET, POST, ...
path := r.URL.Path // /users/123
query := r.URL.Query() // map[string][]string

// Заголовки:
auth := r.Header.Get("Authorization")
contentType := r.Header.Get("Content-Type")

// Тело:
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body error", http.StatusBadRequest)
return
}
defer r.Body.Close()

// Далее — разбор JSON, валидация и т.д.
_ = method
_ = path
_ = query
_ = auth
_ = contentType
_ = bodyBytes
}

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

  • Явно различать:
    • путь (path) и query-параметры,
    • заголовки,
    • тело.
  • Не кодировать критичные данные (пароли, большие JSON) в URL:
    • URL логируется, кешируется, имеет ограничения по длине.
  • Корректно работать с Content-Type, Content-Length, кодировками.
  • Понимать, как эти части влияют на:
    • прокси и балансировщики,
    • кеширование,
    • безопасность (auth, CSRF, CORS).

Кратко:

Основные части HTTP-запроса:

  • стартовая строка (метод, путь/target, версия),
  • заголовки,
  • пустая строка-разделитель,
  • опциональное тело.

Вопрос 24. Что такое Selenium и для чего он используется?

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

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

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

Selenium — это набор инструментов и библиотек для автоматизированного управления браузером, чаще всего используемый для:

  • автоматизированного тестирования веб-интерфейсов (UI),
  • проверки end-to-end (E2E) сценариев,
  • эмуляции действий реального пользователя в браузере.

Ключевые особенности и состав Selenium:

  1. Управление реальными браузерами

Selenium позволяет программно:

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

Это не просто HTTP-запросы к API — это именно эмуляция работы реального пользователя в браузере, с реальным DOM, JS, CSS, куками, редиректами и т.д.

  1. Основные компоненты Selenium-экосистемы
  • Selenium WebDriver

    • Основной API для управления браузерами (Chrome, Firefox, Edge, Safari).
    • Есть биндинги для разных языков: Java, Python, C#, JavaScript и др.
    • Работает через драйверы (chromedriver, geckodriver и т.п.).
  • Selenium Grid

    • Позволяет выполнять тесты параллельно на разных браузерах, версиях и платформах.
    • Используется для масштабирования и cross-browser тестирования.
  • (Исторически) Selenium IDE

    • Расширение для браузера для записи/проигрывания простых сценариев.
  1. Для чего Selenium применяют в реальных проектах
  • E2E и UI-регресс:
    • Проверка ключевых пользовательских сценариев:
      • регистрация, логин, смена пароля;
      • оформление заказа;
      • CRUD-операции через web-интерфейс.
  • Cross-browser тестирование:
    • Убедиться, что функциональность одинаково работает в разных браузерах.
  • Интеграционная проверка frontend + backend:
    • Тест не интересуется внутренним API напрямую;
    • он проверяет систему так, как ее видит конечный пользователь.
  1. Пример типичного сценария (на Java + WebDriver, концептуально)
WebDriver driver = new ChromeDriver();
try {
driver.get("https://example.com/login");

WebElement emailInput = driver.findElement(By.id("email"));
WebElement passwordInput = driver.findElement(By.id("password"));
WebElement submitButton = driver.findElement(By.cssSelector("button[type=submit]"));

emailInput.sendKeys("user@example.com");
passwordInput.sendKeys("secret");
submitButton.click();

WebElement profile = driver.findElement(By.id("profile"));
assertTrue(profile.isDisplayed());
} finally {
driver.quit();
}

Этот тест:

  • открывает браузер,
  • выполняет действия как пользователь,
  • проверяет результат визуального/DOM-состояния.
  1. Важные инженерные акценты
  • Selenium:
    • медленнее и хрупче, чем unit/интеграционные тесты;
    • его стоит использовать точечно:
      • для критичных пользовательских сценариев,
      • как верхний слой над уже надежным API.
  • Хорошая стратегия:
    • максимум логики тестировать на уровне unit и API;
    • Selenium использовать для проверки небольшого набора E2E сценариев:
      • "живая" авторизация,
      • оформление заказа end-to-end,
      • ключевые пользовательские флоу.
  1. Связь с backend/Go-разработкой

Даже если основной стек — Go на backend:

  • Selenium-тесты:
    • помогают убедиться, что frontend корректно использует API,
    • находят проблемы интеграции (CORS, куки, редиректы, авторизация).
  • Backend-разработчику важно:
    • понимать, что Selenium — это инструмент для тестирования системы "снаружи";
    • уметь поддержать такие тесты стабильными контрактами API, корректными статус-кодами, предсказуемым поведением.

Кратко:

Selenium — это инструмент для автоматизации браузера, предназначенный в первую очередь для автоматизированного тестирования UI и E2E сценариев в веб-приложениях, с возможностью работы с реальными браузерами и масштабирования через Grid.

Вопрос 25. Какие основные компоненты Selenium существуют и какова их роль?

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

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

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

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

Основные компоненты Selenium:

  1. Selenium WebDriver

Назначение:

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

Особенности:

  • Работает с реальными браузерами:
    • Chrome (chromedriver),
    • Firefox (geckodriver),
    • Edge, Safari и др.
  • Имеет биндинги для разных языков:
    • Java, Python, C#, JavaScript, Ruby и др.
  • Используется как основной инструмент для написания UI/E2E автотестов.

Пример (концептуально, Java):

WebDriver driver = new ChromeDriver();
driver.get("https://example.com");
WebElement button = driver.findElement(By.id("submit"));
button.click();
driver.quit();
  1. Selenium Server / RemoteWebDriver

Назначение:

  • Позволяет запускать тесты удаленно, подключаясь к браузеру не локально, а через Selenium Server.
  • Используется, когда:
    • нужно запускать тесты в отдельном окружении,
    • CI/CD находится отдельно от браузерной инфраструктуры,
    • нужно масштабирование.

RemoteWebDriver:

  • Клиент, который общается с удаленным Selenium-сервером по протоколу WebDriver.
  1. Selenium Grid

Назначение:

  • Распределенный запуск тестов:
    • параллельно,
    • на разных браузерах,
    • на разных версиях,
    • на разных платформах (Windows, Linux, macOS).
  • Состоит из:
    • Hub (центральный координатор),
    • Nodes (узлы с браузерами).

Зачем нужен:

  • Ускорение прогона больших наборов UI-тестов.
  • Cross-browser и cross-platform тестирование.
  • Интеграция с CI:
    • параллельный запуск,
    • масштабируемость.

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

  • В CI-пайплайне:
    • поднят Selenium Grid,
    • тесты подключаются к нему как к удаленному WebDriver,
    • один сценарий может быть прогнан в Chrome, Firefox, Edge одновременно.
  1. Selenium IDE (исторический и вспомогательный компонент)

Назначение:

  • Расширение для браузера (ранее для Firefox, затем кросс-браузерное):
    • запись действий пользователя,
    • воспроизведение сценариев,
    • экспорт скриптов в код (ограниченно).

Роль:

  • Быстрый прототипинг автотестов,
  • Обучение и демонстрация,
  • Не является основным инструментом для промышленных тестов:
    • мало контроля,
    • сложнее поддерживать, чем код на WebDriver.

Кратко по ролям:

  • WebDriver:
    • ядро, через которое код управляет браузером.
  • Selenium Server / RemoteWebDriver:
    • удаленное управление браузерами.
  • Selenium Grid:
    • параллельный и распределенный запуск тестов на разных браузерах и платформах.
  • Selenium IDE:
    • рекордер/плеер для простых сценариев и быстрых набросков.

Инженерные акценты:

  • В реальных проектах основу обычно составляет:
    • WebDriver + фреймворк тестирования (JUnit/TestNG, pytest, etc.),
    • Selenium Grid для масштабирования в CI.
  • Важно понимать:
    • Selenium — это не только "WebDriver-библиотека",
    • но и инфраструктура (Server/Grid), обеспечивающая распределенный, системный E2E-контур.

Такой ответ демонстрирует не только знание названий, но и понимание архитектуры Selenium и того, как его компоненты применяются в реальных процессах автоматизированного тестирования.

Вопрос 26. Какие типы локаторов используются в Selenium для поиска элементов и каково их назначение?

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

Ответ собеседника: неполный. Упоминает XPath, вскользь CSS Selector и JS, активно использовал только XPath; не называет остальные стандартные типы и даёт неточные формулировки.

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

В Selenium локаторы (селекторы) — это способы однозначно найти элемент на странице. Грамотный выбор локаторов критичен для стабильности и читаемости UI-тестов. Selenium предоставляет стандартный набор стратегий поиска через класс By.

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

  1. По id
  • By.id("elementId")
  • Самый предпочтительный и стабильный способ, если разметка контролируема.
  • Должен быть:
    • уникальным на странице,
    • стабильным (не генерироваться случайно фронтом/фреймворком при каждом билде).

Пример:

WebElement input = driver.findElement(By.id("email"));

Использовать:

  • всегда, когда есть нормальные стабильные id.
  1. По имени (name)
  • By.name("fieldName")
  • Часто используется в формах:
    • <input name="email">
  • Менее надёжен, чем id, т.к. может повторяться.
WebElement input = driver.findElement(By.name("email"));
  1. По имени тега (tagName)
  • By.tagName("input"), By.tagName("button")
  • Обычно используется:
    • в комбинации с другими локаторами,
    • при поиске внутри контейнера.
WebElement form = driver.findElement(By.id("loginForm"));
WebElement firstInput = form.findElement(By.tagName("input"));
  1. По классу (className)
  • By.className("btn-primary")
  • Удобен, если классы стабильны и семантичны.
  • Ограничение:
    • нельзя использовать сложный список классов сразу (только одно имя класса),
    • для сложных случаев лучше CSS.
WebElement button = driver.findElement(By.className("btn-primary"));
  1. По CSS-селектору (cssSelector)
  • By.cssSelector("css-expression")
  • Очень мощный и быстрый способ:
    • поддерживает вложенность, атрибуты, псевдоклассы и т.д.
  • Примеры:
    • By.cssSelector("#email") — по id;
    • By.cssSelector(".btn.primary") — по классам;
    • By.cssSelector("form#login input[name='email']") — контекстный поиск;
    • By.cssSelector("button[data-test='submit']") — по data-атрибутам (очень рекомендовано для тестов).
WebElement button = driver.findElement(By.cssSelector("button[data-test='submit-login']"));

Практический совет:

  • Для UI-тестов хорошей практикой является использование data-testid / data-test атрибутов как стабильных точек:
<button data-test="submit-login">Войти</button>
WebElement button = driver.findElement(By.cssSelector("[data-test='submit-login']"));
  1. По XPath
  • By.xpath("xpath-expression")
  • Очень гибкий и мощный:
    • поиск по структуре,
    • по тексту,
    • по сложным условиям.
  • Примеры:
    • By.xpath("//input[@id='email']")
    • By.xpath("//button[text()='Войти']")
    • By.xpath("//div[@class='user']//span[@data-role='name']")
WebElement loginBtn = driver.findElement(By.xpath("//button[@data-test='submit-login']"));

Рекомендации:

  • Использовать осмысленные, устойчивые XPath, а не:
    • //div[3]/div[2]/span[1] (хрупко),
    • завязку на случайные классы фреймворка.
  • Когда можно — предпочесть CSS/XPath по тестовым атрибутам.
  1. По linkText и partialLinkText (для ссылок)

Используются для элементов <a>.

  • By.linkText("Полный текст ссылки")
  • By.partialLinkText("Часть текста")
WebElement link = driver.findElement(By.linkText("Подробнее"));

Риски:

  • текст может меняться,
  • завязка на язык/локализацию.
  1. Комбинированные стратегии и поиск в контексте

Для устойчивых тестов важно:

  • искать элементы относительно контейнеров:
WebElement form = driver.findElement(By.id("login-form"));
WebElement emailInput = form.findElement(By.name("email"));
  • не тащить длинные глобальные XPath/CSS, если можно локализовать область поиска.

Что НЕ является стандартным локатором Selenium:

  • "JS локатор":
    • В Selenium можно выполнять JavaScript через JavascriptExecutor, но это:
      • не отдельный тип локатора,
      • а сторонний способ, который стоит использовать только при необходимости.
  • Прямой поиск через JS допустим в крайних случаях, когда стандартных стратегий не хватает.

Инженерные рекомендации для стабильных тестов:

  • Приоритет выбора локаторов:
    1. стабильные id или data-test/data-testid атрибуты;
    2. CSS-селекторы по стабильным атрибутам/структуре;
    3. XPath с четкими условиями (по атрибутам, а не по позициям);
    4. name, className, linkText — осторожно, если уверены в стабильности.
  • Избегать:
    • завязки на тексты UI (если важен мультиязычный интерфейс),
    • завязки на технические классы фреймворков (.Mui-..., .ant-...), которые меняются при обновлениях,
    • длинных "хрупких" XPath по индексам.

Кратко:

Основные локаторы Selenium:

  • By.id
  • By.name
  • By.className
  • By.tagName
  • By.cssSelector
  • By.xpath
  • By.linkText
  • By.partialLinkText

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

  • перечисляет эти типы,
  • кратко поясняет каждый,
  • акцентирует важность стабильных, читаемых и не хрупких локаторов (id/data-test/CSS по атрибутам), а не слепой зависимости от XPath "по всему DOM".

Вопрос 27. Как написать XPath для div с определённым классом и заданным текстом?

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

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

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

Для выбора элемента div по классу и тексту важно учитывать две вещи:

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

Базовые корректные варианты XPath.

  1. Точный класс + точный текст

Если у элемента один класс или класс фиксирован и вы хотите точное совпадение текста:

//div[@class='my-class' and text()='Мой текст']

Это сработает, если:

  • class равен строго my-class;
  • текст — ровно Мой текст без лишних пробелов и вложенных тегов.
  1. Класс среди нескольких (рекомендуемый паттерн)

Если class может содержать несколько значений, например:

<div class="my-class active">Мой текст</div>

Используем шаблон с contains и пробелами:

//div[contains(concat(' ', normalize-space(@class), ' '), ' my-class ')
and normalize-space(text()) = 'Мой текст']

Здесь:

  • concat(' ', normalize-space(@class), ' ') и contains(..., ' my-class ') гарантируют, что мы ищем целое имя класса, а не подстроку (чтобы my не совпало с my-class).
  • normalize-space(text()) убирает лишние пробелы и переводы строк вокруг текста.

Проще, если уверены в аккуратном HTML:

//div[contains(@class, 'my-class') and normalize-space(text())='Мой текст']
  1. Частичное совпадение текста (contains)

Если нужно найти div по части текста:

//div[contains(@class, 'my-class') and contains(normalize-space(text()), 'Мой текст')]
  1. Если внутри есть вложенные элементы

Когда div содержит вложенные теги, простой text() может не хватать. Тогда используем .:

//div[contains(@class, 'my-class')
and contains(normalize-space(.), 'Мой текст')]

normalize-space(.) смотрит на весь текст внутри div, включая вложенные элементы.

Примеры для Selenium (Java):

// Точный класс и точный текст:
driver.findElement(By.xpath("//div[@class='my-class' and text()='Мой текст']"));

// Класс среди нескольких и точный текст:
driver.findElement(By.xpath(
"//div[contains(concat(' ', normalize-space(@class), ' '), ' my-class ') " +
"and normalize-space(text())='Мой текст']"
));

// Класс и частичное совпадение текста:
driver.findElement(By.xpath(
"//div[contains(@class, 'my-class') and contains(normalize-space(.), 'Мой текст')]"
));

Практические рекомендации:

  • Для стабильных тестов лучше:
    • использовать data-test/data-testid атрибуты вместо сложных XPath по классу/тексту;
    • избегать завязки на тексты UI (особенно если есть локализации).
  • Если нужно по тексту:
    • применять normalize-space,
    • использовать . вместо text(), если внутри есть вложенные элементы,
    • аккуратно использовать contains (не ловить случайные подстроки).

Краткий правильный пример (самый типичный):

//div[contains(@class, 'my-class') and normalize-space(text())='Мой текст']

Вопрос 28. Какие виды ожиданий есть в Selenium и как они работают?

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

Ответ собеседника: неполный. Называет implicit wait и явное ожидание, но путается в терминах, не раскрывает механику явных ожиданий и других подходов к синхронизации.

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

Ожидания в Selenium — ключевой механизм для стабильных UI-тестов. Веб-приложение асинхронно: элементы появляются не сразу, данные подгружаются, JS дорисовывает DOM. Если тест кликает или ищет элемент "сразу", он становится хрупким и нестабильным.

В Selenium есть несколько типов ожиданий и общих подходов к синхронизации:

  1. Неявное ожидание (Implicit Wait)
  • Настраивается один раз для WebDriver и применяется ко всем операциям поиска элементов.

Суть:

  • Когда мы вызываем findElement, Selenium:
    • не сразу падает с NoSuchElementException,
    • а в течение указанного таймаута периодически пытается найти элемент.
  • Как только элемент найден — ожидание прекращается.

Пример (Java):

driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

Особенности:

  • Действует глобально для driver.
  • Влияет только на поиск элементов (findElement/findElements).
  • Не ждет условий (кликабельности, исчезновения и т.п.) — только появления в DOM.
  • Не рекомендуется сочетать с явными ожиданиями в сложных сценариях: это может приводить к непредсказуемым задержкам.

Когда использовать:

  • Для простых проектов/демо, чтобы "смягчить" момент появления элементов.
  • В продакшн UI-тестах — лучше минимизировать или отключать и использовать явные ожидания.
  1. Явное ожидание (Explicit Wait)

Это основной, правильный инструмент.

Суть:

  • Мы явно указываем:
    • что именно ждем,
    • сколько времени готовы ждать.
  • Используется WebDriverWait + условия (ExpectedConditions).

Пример (Java, Selenium 4+):

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));

WebElement button = wait.until(ExpectedConditions.elementToBeClickable(
By.cssSelector("button[data-test='submit-login']"),
));
button.click();

Типичные условия (ExpectedConditions):

  • visibilityOfElementLocated(locator):
    • элемент существует в DOM и видим.
  • presenceOfElementLocated(locator):
    • элемент есть в DOM (не обязательно видим).
  • elementToBeClickable(locator):
    • элемент видим и enabled.
  • textToBePresentInElementLocated(locator, text):
    • ожидаем появления текста.
  • urlContains, titleIs, frameToBeAvailableAndSwitchToIt
  • invisibilityOfElement, stalenessOf, и др.

Как работает:

  • В течение заданного timeout:
    • Selenium периодически проверяет условие.
    • Если условие выполнено — возвращает результат.
    • Если время вышло — бросает TimeoutException.

Преимущества:

  • Точно контролируем, что ждем:
    • конкретный элемент,
    • его кликабельность,
    • исчезновение спиннера,
    • смену URL.
  • Значительно повышает стабильность тестов.
  1. Fluent Wait (гибкое явное ожидание)

Расширенный вариант явного ожидания.

Суть:

  • Позволяет:
    • задать timeout,
    • задать polling interval (как часто проверять),
    • указать, какие исключения игнорировать.

Пример:

Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(15))
.pollingEvery(Duration.ofMillis(500))
.ignoring(NoSuchElementException.class);

WebElement element = wait.until(d -> d.findElement(By.id("result")));

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

  • Для нестабильных, тяжелых сценариев,
  • Когда нужно тонко управлять частотой проверок и игнорируемыми ошибками.
  1. Жесткие ожидания (Thread.sleep) — как антипаттерн
  • Thread.sleep(3000); — принудительная пауза.

Проблемы:

  • Не учитывает реальное состояние страницы:
    • если элемент появился раньше — всё равно ждём.
    • если не успел появиться — тест все равно падает.
  • Увеличивает общее время прогона.
  • В больших наборах тестов приводит к медленным и нестабильным пайплайнам.

Использовать:

  • Только как временную меру при отладке,
  • В продакшн-тестах — заменять на явные ожидания с условиями.
  1. Практические рекомендации по ожиданиям
  • Основной инструмент — явные ожидания (WebDriverWait + ExpectedConditions).
  • Не полагаться на одни implicit wait:
    • они слишком грубые и только про наличия элемента в DOM.
  • Избегать Thread.sleep, особенно в виде "магических чисел".
  • Использовать стабильные условия:
    • проверять не просто присутствие элемента, а:
      • видимость,
      • кликабельность,
      • завершение загрузки (например, исчезновение loader/spinner),
      • появление конкретного текста или состояния.
  • Не смешивать implicit и explicit wait без понимания:
    • это может привести к неожиданному суммированию задержек и сложным для отладки эффектам.
  1. Связь с надежностью тестов (инженерный взгляд)

Грамотно настроенные ожидания:

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

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

  • В Selenium есть:
    • неявные ожидания (implicit wait) — глобальный таймаут для поиска элементов;
    • явные ожидания (explicit wait) — WebDriverWait + условия на конкретные элементы/состояния;
    • fluent wait — вариация явных ожиданий с гибкими настройками;
    • Thread.sleep — грубый способ, которого стоит избегать.
  • Для стабильных тестов:
    • основа — явные ожидания с четкими условиями (visibility, clickable, исчезновение loader'ов).

Вопрос 29. Что такое Selenoid и для чего он используется?

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

Ответ собеседника: правильный. Описывает Selenoid как инструмент для запуска автотестов в Docker/на сервере без GUI с удалённым запуском браузеров.

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

Selenoid — это высокопроизводительный сервер реализации протокола Selenium, предназначенный для запуска браузеров в Docker-контейнерах. Он решает задачи масштабируемого, изолированного и воспроизводимого запуска UI-автотестов в распределённой инфраструктуре.

Ключевые идеи и преимущества Selenoid:

  1. Запуск браузеров в Docker-контейнерах
  • Каждый тест (или сессия) запускается в отдельном контейнере с нужным браузером и версией:
    • chrome, firefox, opera, edge (при наличии образов),
    • фиксированные версии (например, chrome:121.0).
  • Изоляция окружений:
    • тесты не конфликтуют между собой,
    • конфигурация и версия браузера полностью контролируемы.

Преимущества:

  • Нет зависимости от локального GUI.
  • Быстрое поднятие/удаление окружения.
  • Легко повторить проблемную среду.
  1. Высокая производительность и малый overhead

В отличие от классического Selenium Grid на Java:

  • Selenoid написан на Go:
    • низкое потребление ресурсов,
    • высокая производительность,
    • минимальные накладные расходы.
  • Хорошо подходит для:
    • CI/CD,
    • массового параллельного запуска тестов.
  1. Совместимость с WebDriver
  • Selenoid совместим с Selenium/WebDriver протоколом.
  • С точки зрения тестов:
    • это просто удаленный Selenium endpoint:
      • http://selenoid:4444/wd/hub (пример).
  • Вы можете:
    • использовать существующие тесты на Selenium/WebDriver,
    • просто указывать удаленный адрес Selenoid как хост.

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

DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setVersion("121.0");
caps.setCapability("enableVNC", true);
caps.setCapability("enableVideo", true);

WebDriver driver = new RemoteWebDriver(
new URL("http://selenoid:4444/wd/hub"),
caps
);
  1. Параллельный запуск и масштабирование
  • Позволяет запускать множество браузерных сессий параллельно:
    • на одной машине,
    • или в кластере (в связке с Kubernetes/другим оркестратором).
  • Конфигурация через browsers.json и ограничения ресурсов:
    • максимальное число одновременно работающих контейнеров,
    • поддерживаемые версии.

Плюсы:

  • Существенно ускоряет полный прогон UI/E2E регресса.
  • Подходит для крупных команд и CI-инфраструктуры.
  1. Видео, VNC и отладка

Selenoid предоставляет:

  • Видео-запись тестовых сессий:
    • удобно для анализа падений.
  • Поддержку VNC:
    • можно "подключиться" и посмотреть, что реально происходит в браузере во время теста.

Это критично для:

  • диагностики нестабильных тестов,
  • анализа багов UI или окружения.
  1. Типичный сценарий использования в проекте
  • Есть набор UI-тестов на Selenium/WebDriver (JUnit/TestNG и т.д.).
  • В CI-пайплайне:
    • поднимается (или уже крутится) Selenoid.
    • тесты запускаются против http://selenoid:4444/wd/hub.
    • каждый тест создает сессию с нужными caps (браузер, версия, опции).
  • Selenoid:
    • разворачивает Docker-контейнер с нужным браузером,
    • прогоняет тест,
    • сохраняет видео/логи,
    • завершает контейнер.
  1. Почему это важно понимать инженеру

С инженерной точки зрения Selenoid решает сразу несколько проблем:

  • Детерминированность:
    • фиксированные образы браузеров,
    • предсказуемое окружение.
  • Масштабируемость:
    • простое горизонтальное масштабирование.
  • Наблюдаемость:
    • видео, логи, VNC-доступ.
  • Интеграция с CI/CD:
    • контейнерная модель идеально ложится на современную инфраструктуру.

Кратко:

Selenoid — это легковесный, производительный Selenium/WebDriver-сервер, запускающий браузеры в Docker-контейнерах. Используется для:

  • удаленного, параллельного и воспроизводимого запуска UI/E2E тестов;
  • избавления от ручного управления браузерами и Selenium Grid на Java;
  • интеграции UI-тестов с CI/CD на уровне современной контейнерной инфраструктуры.

Вопрос 30. Как формируется отчёт в Allure и как его настраивать?

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

Ответ собеседника: неполный. Упоминает подключение зависимости Allure, генерацию отчёта через Gradle и просмотр в браузере; не описывает, за счёт чего собираются данные (слушатели, аннотации), и не раскрывает ключевые элементы конфигурации.

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

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

  • как Allure получает данные во время запуска тестов;
  • как из этих данных формируется HTML-отчёт.

Общая схема работы:

  1. Тесты выполняются (JUnit/TestNG/Selenide/Rest-Assured/и т.п.).
  2. Специальные слушатели/интеграции Allure снимают события:
    • старт/завершение теста,
    • шаги,
    • вложения (скриншоты, логи, запросы/ответы),
    • статусы (passed/failed/broken/skipped).
  3. Эти события сохраняются в виде "сырых" результатов:
    • в директорию (обычно allure-results).
  4. Команда allure generate или плагин Gradle/Maven:
    • читает allure-results,
    • строит статический HTML-отчет в директории allure-report.
  5. Команда allure serve или статическая раздача:
    • поднимает веб-сервер, открывает красивый интерактивный отчет в браузере.

Теперь по шагам и практическим деталям.

  1. Подключение Allure к проекту

Для Java-проектов (JUnit/TestNG) обычно используется:

  • dependency + плагин (Maven/Gradle),
  • включение Allure listener.

Пример для Gradle (концептуально):

plugins {
id 'java'
id 'io.qameta.allure' version '2.11.2'
}

allure {
version = '2.27.0'
useJUnit5 {
version = '2.27.0'
}
}

dependencies {
testImplementation 'io.qameta.allure:allure-junit5:2.27.0'
}

Смысл:

  • плагин знает, как перехватить исполнение тестов,
  • результаты (JSON/XML) складываются в build/allure-results.
  1. Формирование "сырых" результатов

Allure интегрируется через:

  • JUnit/TestNG listeners, расширения и аннотации;
  • Selenide/Rest-Assured/иные библиотеки, которые умеют "репортить" в Allure.

При запуске тестов:

  • Для каждого теста фиксируется:
    • имя,
    • класс/метод,
    • статус (passed/failed/broken/skipped),
    • стек ошибки,
    • тайминги.
  • Если используются аннотации Allure:
    • добавляются шаги, описания, ссылки на требования, истории, severity и т.п.
  • Если добавляются вложения:
    • Allure сохраняет файлы (скриншоты, .log, .txt, JSON) и ссылки на них.

Результат:

  • В allure-results появляются файлы:
    • *-result.json (описание тестов),
    • *-container.json (контейнеры/сьюты),
    • файлы аттачей (png, txt, html),
    • categories.json, executor.json и т.п. при доп. конфигурации.
  1. Использование аннотаций Allure (ключевой момент, которого не было в ответе)

Аннотации позволяют структурировать отчёт и сделать его реально полезным.

Основные:

  • @Epic, @Feature, @Story

    • Структурируют тесты по бизнес-измерениям.
    • Пример:
      • Epic: "User Management"
      • Feature: "Registration"
      • Story: "User can register with valid email"
  • @Description

    • Человекочитаемое описание сценария.
  • @Severity

    • Важность теста:
      • BLOCKER, CRITICAL, NORMAL, MINOR, TRIVIAL.
  • @Owner

    • Ответственный за тест.
  • @Issue, @TmsLink

    • Ссылки на задачи/баги/тест-кейсы в внешних системах (Jira, TestRail и т.п.).
  • @Step

    • Оформление шагов внутри теста (или на методах, вызываемых из теста).
    • Позволяет видеть подробный сценарий в отчёте.
  • @Attachment

    • Добавление вложений:
      • скриншоты,
      • лог-файлы,
      • запросы/ответы API,
      • html-дом страницы.

Пример (JUnit 5 + Allure):

import io.qameta.allure.*;

@Epic("User Management")
@Feature("Registration")
class RegistrationTest {

@Test
@Story("User can register with valid data")
@Severity(SeverityLevel.CRITICAL)
@Description("Проверка успешной регистрации нового пользователя через API")
void shouldRegisterUser() {
step("Отправить запрос на регистрацию", () -> {
// вызов API /users
});

step("Проверить, что пользователь создан в БД", () -> {
// проверка в БД
});
}

@Step("Шаг с параметром: {param}")
void someHelperStep(String param) {
// будет отображен как шаг в отчете
}

@Attachment(value = "Response body", type = "application/json")
String attachResponse(String body) {
return body;
}
}
  1. Генерация и просмотр отчёта

После выполнения тестов:

  • сырые результаты лежат в allure-results.

Дальше:

  • allure generate allure-results --clean -o allure-report
    • генерирует HTML-отчет в allure-report.
  • allure serve allure-results
    • одновременно генерирует и запускает временный веб-сервер,
    • автоматически открывает отчет в браузере.

При Gradle/Maven:

  • часто настраивают таски:
    • ./gradlew allureReport
    • ./gradlew allureServe

В CI:

  • сохраняют allure-results как артефакт,
  • генерируют отчет на стороне CI,
  • прикрепляют ссылку в job.
  1. Что видно в отчете Allure

Хорошо настроенный Allure-отчет показывает:

  • Общую статистику:
    • passed/failed/broken/skipped,
    • timeline, duration, flaky-паттерны.
  • Дерево тестов:
    • по пакетам/классам,
    • по параметрам BDD (Epic/Feature/Story).
  • Детали каждого теста:
    • шаги,
    • вложения,
    • лог,
    • скриншоты,
    • стеки исключений.
  • Историю:
    • динамику прохождения теста по билдам.
  • Категории (categories):
    • группировка типов падений (assertion, infra, flaky и т.п.).
  1. Инженерные акценты

Правильная настройка Allure:

  • Это не просто "подключить зависимость":
    • нужно включить listener для JUnit/TestNG;
    • настроить пути allure-results;
    • добавить аннотации, чтобы отчет был структурированным, а не "серым списком тестов".
  • Важно:
    • логировать ключевые шаги (через @Step или programmatic steps),
    • прикладывать важные артефакты:
      • запросы/ответы API,
      • скриншоты из Selenium/Selenide/Selenoid,
      • логи.

Пример связки с Selenium/Selenoid:

  • при падении теста:
    • снять скриншот,
    • приложить в Allure как attachment,
    • при использовании Selenoid — приложить видео сессии.

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

  • Allure формирует отчет на основе "сырых" результатов (allure-results), которые генерируются во время выполнения тестов с помощью интеграции (listeners, аннотации).
  • Я настраиваю:
    • зависимости и плагин (Maven/Gradle),
    • включение Allure listener,
    • аннотации (@Epic, @Feature, @Story, @Severity, @Step, @Attachment) для структурирования отчета,
    • генерацию отчета (allure generate / allure serve) локально и в CI.
  • В итоге получаем детализированный HTML-отчет с шагами, скриншотами, логами и метриками по тестам.

Вопрос 31. Что такое CI/CD и в чём их суть?

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

Ответ собеседника: правильный. Расшифровывает как continuous integration и continuous delivery, описывает CI как автоматическую сборку и запуск тестов с отчётами, CD как автоматическое развёртывание на сервере.

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

CI/CD — это связанный набор практик и процессов, позволяющий быстро, предсказуемо и безопасно доставлять изменения из репозитория в продакшен.

Разберем по частям и чуть глубже, чем формальное определение.

CI — Continuous Integration (Непрерывная интеграция)

Суть:

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

Типичный pipeline CI для backend-сервиса (например, на Go):

  • Триггер:
    • push в репозиторий, merge request / pull request.
  • Ступени:
    • Lint/Static analysis:
      • golangci-lint, go vet, форматирование gofmt.
    • Сборка:
      • go build ./...
    • Unit-тесты:
      • go test ./...
    • Интеграционные тесты:
      • запуск тестовой БД (Postgres в Docker),
      • go test -tags=integration ./...
    • Генерация отчетов:
      • покрытия кода,
      • Allure / JUnit-совместимые репорты для CI.

Результат:

  • Если что-то ломается — билд красный,
  • разработчик сразу получает фидбек,
  • в main/master не попадает нерабочий код.

CD — Continuous Delivery / Continuous Deployment

Две близкие практики, их часто путают, важно различать.

  1. Continuous Delivery
  • Система автоматизирует весь путь до "готового к деплою" артефакта:
    • собранный бинарь / Docker-образ,
    • прогнанные тесты,
    • проверенные миграции.
  • Деплой на продакшен:
    • может требовать ручного подтверждения (manual approval),
    • но технически готов и воспроизводим одной кнопкой или командой.
  1. Continuous Deployment
  • Следующий шаг после Delivery:
    • деплой в нужное окружение происходит автоматически после прохождения всех проверок,
    • без ручного участия, при зеленом pipeline.
  • Обычно с:
    • поэтапным раскатом (canary, blue-green),
    • фиче-флагами,
    • rollback-стратегиями.

Типичный CD-процесс для Go-сервиса:

  • Сборка Docker-образа:
    docker build -t registry.example.com/my-service:${CI_COMMIT_SHA} .
    docker push registry.example.com/my-service:${CI_COMMIT_SHA}
  • Прогон тестов и проверок.
  • Автоматическое обновление окружения:
    • Kubernetes:
      • применение манифестов / Helm-чартов с новым образом,
      • стратегии развёртывания (RollingUpdate, Blue-Green, Canary),
      • health-check и readiness-пробы.
    • Или другой оркестратор/инфраструктура.
  • Постдеплойные проверки:
    • smoke-тесты,
    • health-check,
    • метрики и алерты.

Инженерные акценты:

  • CI/CD — это не только инструменты (GitLab CI, GitHub Actions, Jenkins, Argo CD, etc.), а соглашения и гарантии процесса:
    • любой коммит проверен автоматически,
    • ветка main всегда в деплоебельном состоянии,
    • выпуск новой версии — рутинная операция, а не "героизм ночного релиза".
  • Для сервисов:
    • минимальное требование:
      • CI: линтеры, unit-тесты, интеграционные тесты, сборка образа;
      • CD: автоматизированный деплой в staging, полуавтоматический или автоматический деплой в prod.
  • Для качества и безопасности:
    • в pipeline можно встраивать:
      • security-scans,
      • SAST/DAST,
      • проверку миграций БД,
      • контрактные тесты между сервисами.

Кратко:

  • CI — часто и автоматически интегрируем изменения, сразу проверяем сборку и тесты.
  • CD — автоматически готовим и (в случае continuous deployment) выкатываем изменения в окружения.
  • Цель: быстрые, безопасные, повторяемые релизы без "ручной магии" и "боевых" экспериментов.

Вопрос 32. Какие инструменты CI/CD знаешь и с какими работал?

Таймкод: 00:20:41

Ответ собеседника: неполный. Упоминает только Jenkins и запуск в Docker, без детализации других инструментов и сравнений.

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

Инструменты CI/CD — это системы, которые позволяют автоматически выполнять сборку, тестирование, анализ кода и деплой. Важно знать не только один конкретный (например, Jenkins), но и понимать общие подходы и ключевые игроки на рынке.

Ключевые инструменты и их особенности:

  1. Jenkins
  • Один из самых распространенных и старейших CI/CD-инструментов.
  • Особенности:
    • self-hosted: разворачивается на своих серверах или в Docker/Kubernetes;
    • pipeline как код:
      • Jenkinsfile с декларативным или скриптовым пайплайном;
    • огромная экосистема плагинов:
      • интеграция с Git, Docker, Kubernetes, Allure, Slack и т.д.
  • Типичный пайплайн:
    • checkout кода → сборка (go build / mvn / gradle) → тесты → линтеры → сборка Docker-образа → деплой.
  • Пример упрощенного Jenkinsfile для Go-сервиса:
pipeline {
agent any

stages {
stage('Checkout') {
steps {
git 'https://github.com/org/repo.git'
}
}

stage('Build') {
steps {
sh 'go build ./...'
}
}

stage('Test') {
steps {
sh 'go test ./...'
}
}

stage('Docker Build & Push') {
steps {
sh '''
docker build -t registry.example.com/my-service:${GIT_COMMIT} .
docker push registry.example.com/my-service:${GIT_COMMIT}
'''
}
}

stage('Deploy to Staging') {
steps {
sh 'kubectl set image deploy/my-service my-service=registry.example.com/my-service:${GIT_COMMIT}'
}
}
}
}
  1. GitLab CI/CD
  • Встроен в GitLab.
  • Конфигурация через .gitlab-ci.yml в репозитории.
  • Особенности:
    • тесная интеграция с репозиторием, MR, registry;
    • удобная работа с Docker:
      • Docker-in-Docker,
      • Kubernetes executors;
    • environments, review apps.
  • Подходит для полного цикла:
    • CI (линтеры, тесты),
    • CD (автодеплой в staging/prod),
    • мониторинг статуса.
  1. GitHub Actions
  • Встроенный CI/CD в GitHub.
  • Конфигурация через .github/workflows/*.yml.
  • Особенности:
    • marketplace с готовыми actions,
    • удобная интеграция с репозиторием и PR,
    • поддержка matrix build (разные версии Go, разные OS).
  • Часто используется для:
    • тестирования open-source,
    • сборки и публикации Docker-образов,
    • деплоя в Kubernetes/облака.
  1. Bitbucket Pipelines
  • Аналог GitLab CI/CD для Bitbucket.
  • yaml-конфигурация в репозитории.
  • Подходит для команд, уже живущих в Bitbucket.
  1. TeamCity, Bamboo и др.
  • Более старые/энтерпрайзные решения.
  • Часто используются в крупных компаниях с наследием.
  • Имеют UI-конфигурацию и поддержку pipelines-as-code.
  1. Инструменты для CD / GitOps

Поверх CI-систем часто используют дополнительные сервисы, специализирующиеся на деплое и управлении конфигурациями:

  • Argo CD

    • GitOps-подход:
      • состояние кластера (Kubernetes-манифесты, Helm-чарты, Kustomize) хранится в Git;
      • Argo CD следит за Git и приводит кластер к описанному состоянию.
    • Отлично сочетается с GitLab CI/GitHub Actions/Jenkins:
      • CI собирает и пушит образы,
      • обновляет манифесты в Git,
      • Argo CD автоматически синхронизирует кластер.
  • FluxCD

    • Аналогичный GitOps-инструмент от Weaveworks.

Эти инструменты:

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

Важно уметь:

  • понимать не только конкретный UI Jenkins, но и концепцию:
    • pipeline as code,
    • stages/jobs,
    • артефакты, кеши,
    • триггеры (push, PR/MR, теги, расписание).
  • строить пайплайны, включающие:
    • статический анализ (golangci-lint, go vet),
    • юнит-тесты и интеграционные тесты (go test, docker-compose),
    • сборку Docker-образов,
    • публикацию в registry,
    • деплой (Kubernetes, VM, serverless),
    • генерацию отчетов (Allure, coverage),
    • уведомления (Slack, email).

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

  • Назвать несколько инструментов:
    • Jenkins, GitLab CI, GitHub Actions, Bitbucket Pipelines, TeamCity, Argo CD.
  • Кратко описать:
    • Jenkins — универсальный, extensible, self-hosted.
    • GitLab CI/GitHub Actions — нативно интегрированы с git-платформой, yaml-конфигурация.
    • Argo CD/Flux — GitOps для деплоя.
  • Показать понимание:
    • как через эти инструменты строится полный цикл: от коммита до деплоя, с автоматизацией тестов, проверок и релизов.

Вопрос 33. Для чего используется файл pom.xml в Maven-проекте?

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

Ответ собеседника: правильный. Говорит, что в pom.xml задаются зависимости, версия JDK, настройки сборки и связанные параметры; общее понимание корректное.

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

pom.xml (Project Object Model) — это центральный конфигурационный файл Maven-проекта. Он описывает:

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

Ключевые элементы pom.xml и их роль:

  1. Идентификация проекта

Определяет координаты артефакта в Maven-экосистеме:

  • groupId — группа/организация/домен (например, com.example).
  • artifactId — имя артефакта/модуля (например, user-service).
  • version — версия проекта (например, 1.0.0, 1.0.0-SNAPSHOT).
  • packaging — тип артефакта:
    • jar, war, pom и др.

Пример:

<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
  1. Управление зависимостями (dependencies)

Описывает внешние библиотеки, которые Maven:

  • скачивает из репозиториев (Maven Central, private Nexus/Artifactory);
  • добавляет в classpath при компиляции, тестировании, запуске.

Пример:

<dependencies>
<!-- JUnit для тестов -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>

<!-- Spring Web как пример runtime-зависимости -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>

Особенности:

  • scope:
    • compile (по умолчанию),
    • test,
    • provided,
    • runtime,
    • system.
  • Транзитивные зависимости:
    • Maven автоматически подтягивает зависимости зависимостей;
    • конфликты решаются через механизм dependency management (версии можно переопределить).
  1. Репозитории (repositories, pluginRepositories)

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

  • публичные и приватные Nexus/Artifactory;
  • зеркала для Maven Central.

Если проект использует приватные либы — это настраивается здесь (или в settings.xml).

  1. Плагины и фазы сборки (build, plugins)

Секция build описывает:

  • какие плагины используются,
  • как выполняются фазы Maven lifecycle:
    • validate, compile, test, package, verify, install, deploy.

Типичные плагины:

  • maven-compiler-plugin:
    • версия Java, опции компилятора.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
  • maven-surefire-plugin:
    • запуск unit-тестов.
  • maven-failsafe-plugin:
    • запуск интеграционных тестов.
  • плагины для:
    • сборки Docker-образов,
    • генерации отчетов (например, интеграция с Allure),
    • упаковки fat-jar и т.п.
  1. Профили (profiles)

Позволяют конфигурировать разные режимы сборки:

  • dev / test / prod;
  • с/без определенных модулей;
  • разные БД/URLs и т.п.

Активируются:

  • по параметрам командной строки,
  • по переменным окружения,
  • по условиям.

Пример:

<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
</profiles>
  1. Родительский POM и управление версиями (parent, dependencyManagement)

Для больших проектов:

  • Выносят общие настройки (версии зависимостей, плагины, encoding, java-version) в parent pom.
  • Дочерние модули/проекты наследуют эти настройки.

Пример:

<parent>
<groupId>com.example</groupId>
<artifactId>platform-bom</artifactId>
<version>1.0.0</version>
</parent>

Секция dependencyManagement:

  • позволяет централизованно задать версии зависимостей,
  • в модулях указывать только groupId/artifactId без version.
  1. Взаимосвязь с CI/CD

Для CI/CD pom.xml — точка правды:

  • какие зависимости нужны,
  • как собрать:
    • mvn clean test,
    • mvn package,
    • mvn verify,
  • какие плагины запускать:
    • генерация Allure-результатов,
    • сборка Docker-образа,
    • деплой в репозиторий (Maven repo, Docker registry).

CI-система (Jenkins, GitLab CI, GitHub Actions) по сути просто вызывает команды Maven, которые читают pom.xml и выполняют описанный процесс.

Кратко:

  • pom.xml — декларативное описание Maven-проекта:
    • координаты артефакта,
    • зависимости и их версии,
    • плагины и фазы сборки,
    • профили и репозитории,
    • общая конфигурация для сборки, тестирования и деплоя.
  • Это единый источник истины для билд-системы и важный элемент предсказуемого CI/CD-процесса.

Вопрос 34. Какие основные фазы сборки в Maven существуют и за что они отвечают?

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

Ответ собеседника: неправильный. Не смог назвать фазы.

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

Maven использует модель жизненного цикла (build lifecycle), состоящую из последовательных фаз. Важно понимать, что:

  • фазы — это логические этапы,
  • плагины "подвешиваются" к фазам,
  • запуск mvn <phase> выполняет все фазы до нее включительно,
  • корректная настройка фаз критична для предсказуемой сборки и CI/CD.

Базовый lifecycle "default" (основной для сборки артефакта) включает следующие ключевые фазы (основные, которые стоит уверенно знать):

  1. validate
  • Проверка структуры проекта:
    • корректность pom.xml,
    • наличие необходимых директорий/конфигураций.
  • На этом этапе обычно нет компиляции, только валидация модели.
  1. compile
  • Компиляция исходного кода основного модуля:
    • по умолчанию из src/main/java в target/classes.
  • Здесь используется maven-compiler-plugin.
  • Если код не компилируется — дальше фазы не идут.

Команда:

mvn compile
  1. test
  • Компиляция и запуск модульных (unit) тестов:
    • код тестов из src/test/java компилируется;
    • тесты запускаются, обычно через maven-surefire-plugin.
  • Не создает финальный артефакт.
  • Если тесты падают — билд красный.

Команда:

mvn test
  1. package
  • Упаковка скомпилированного кода в артефакт:
    • jar, war, ear — в зависимости от packaging в pom.xml.
  • Результат:
    • файл в target/, например my-service-1.0.0.jar.

Команда:

mvn package
  1. verify
  • Дополнительные проверки качества:
    • интеграционные тесты (через maven-failsafe-plugin),
    • проверка контрактов, схем, статического анализа и т.п.
  • Часто используется в более зрелых пайплайнах:
    • unit-тесты на test,
    • интеграционные/системные тесты на verify.

Команда:

mvn verify
  1. install
  • Установка собранного артефакта в локальный Maven-репозиторий (~/.m2/repository).
  • Нужен для:
    • использования текущего артефакта другими локальными проектами,
    • ускорения разработки и локальных интеграций.

Команда:

mvn install
  1. deploy
  • Публикация артефакта во внешний (удаленный) Maven-репозиторий:
    • Nexus, Artifactory, internal repo.
  • Используется на CI/CD:
    • после успешных тестов/проверок артефакт выкладывается как официально доступный для остальных сервисов/модулей.

Команда:

mvn deploy

Важные моменты:

  • Если запустить:

    • mvn package:

      • будут последовательно выполнены: validate → compile → test → package.
    • mvn install:

      • validate → compile → test → package → verify → install.
    • mvn deploy:

      • весь цикл до deploy включительно.
  • Жизненные циклы:

    • default — для сборки (выше).
    • clean — очистка (mvn clean → удаляет target).
    • site — генерация документации.

    Часто в CI используется комбинация:

    mvn clean verify
    # или
    mvn clean package

Инженерные акценты:

  • Грамотная привязка плагинов к фазам позволяет:
    • получать предсказуемый билд-процесс;
    • логично встроить:
      • unit-тесты (test),
      • интеграционные (обычно verify),
      • генерацию отчётов (site/verify),
      • упаковку Docker-образов (package/verify),
      • деплой (deploy).
  • В CI/CD:
    • фазы Maven становятся "контрактом":
      • test не должен деплоить,
      • deploy не должен запускаться локально "просто так" без настроенного репозитория.

Краткий ответ, уместный на интервью:

  • Основные фазы: validate, compile, test, package, verify, install, deploy.
  • При вызове фазы выполняются все предыдущие.
  • Они задают стандартный жизненный цикл сборки, к которому привязываются плагины и шаги CI/CD.

Вопрос 35. Какие базовые и продвинутые SQL-операции важно уверенно знать и использовать?

Таймкод: 00:22:09

Ответ собеседника: неполный. Лично почти не работал; знает SELECT и FROM, может выбирать столбцы из одной таблицы; не умеет делать JOIN и другие более сложные операции.

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

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

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

Ниже — системный обзор того, что необходимо знать.

Базовые операции (обязательный минимум)

  1. SELECT: выборка данных
  • Базовый синтаксис:
SELECT column1, column2
FROM table_name;
  • Выбор всех колонок:
SELECT * FROM users;
  • Псевдонимы:
SELECT u.id AS user_id, u.email
FROM users AS u;
  1. WHERE: фильтрация строк
SELECT id, email
FROM users
WHERE active = true
AND created_at >= '2024-01-01';

Операторы:

  • =, <>, <, >, <=, >=
  • AND, OR, NOT
  • IN (...), BETWEEN, LIKE, ILIKE (PostgreSQL)
  1. ORDER BY: сортировка
SELECT id, email
FROM users
WHERE active = true
ORDER BY created_at DESC, id ASC;
  1. LIMIT / OFFSET: пагинация
SELECT id, email
FROM users
ORDER BY id
LIMIT 20 OFFSET 40; -- страница 3 по 20 записей

Но важно понимать, что OFFSET на больших таблицах дорогой; лучше keyset pagination.

Модификация данных

  1. INSERT: вставка
INSERT INTO users (email, name)
VALUES ('test@example.com', 'Test User');

Для получения id (PostgreSQL):

INSERT INTO users (email, name)
VALUES ('test@example.com', 'Test User')
RETURNING id;
  1. UPDATE: обновление
UPDATE users
SET name = 'New Name'
WHERE id = 123;

Всегда с осмысленным WHERE, иначе измените все строки.

  1. DELETE: удаление
DELETE FROM users
WHERE id = 123;

Опять же, без WHERE получите truncate по незнанию.

Соединения таблиц (JOIN) — критически важно

  1. INNER JOIN

Возвращает только записи, которые есть в обеих таблицах (по условию соединения).

SELECT
o.id,
o.total,
u.email
FROM orders o
JOIN users u ON u.id = o.user_id;

Используется, когда нужны только "связанные" данные.

  1. LEFT JOIN

Включает все строки из левой таблицы + совпадения из правой; если совпадения нет — NULL.

SELECT
u.id,
u.email,
o.id AS order_id,
o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;

Используется для выборок вида:

  • "все пользователи и их заказы, если есть";
  • "найти пользователей без заказов":
SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
  1. RIGHT JOIN / FULL OUTER JOIN

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

  • RIGHT JOIN — симметрия LEFT JOIN.
  • FULL OUTER JOIN — все строки с обеих сторон, где нет совпадений — NULL.

Часто можно переписать через LEFT JOIN для читаемости.

Агрегации и группировка

  1. GROUP BY и агрегатные функции

Функции: COUNT, SUM, AVG, MIN, MAX.

SELECT
user_id,
COUNT(*) AS orders_count,
SUM(total) AS total_spent
FROM orders
GROUP BY user_id;
  1. HAVING

Фильтрация по агрегированным значениям (после GROUP BY):

SELECT user_id, COUNT(*) AS orders_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5;

Работа с NULL

  1. Понимание NULL
  • NULL — отсутствие значения.
  • = NULL не работает; нужно IS NULL / IS NOT NULL.
SELECT *
FROM users
WHERE deleted_at IS NULL;

Подзапросы и EXISTS

  1. Подзапросы

В SELECT:

SELECT
u.id,
u.email,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS orders_count
FROM users u;

В WHERE:

SELECT id, email
FROM users
WHERE id IN (SELECT user_id FROM orders);
  1. EXISTS (предпочтительно для "есть ли связанные записи")
SELECT u.id, u.email
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.id
);

Транзакции

  1. BEGIN / COMMIT / ROLLBACK

Транзакции обеспечивают атомарность и согласованность при нескольких операциях.

BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;

Если что-то пошло не так:

ROLLBACK;

Важны для:

  • переводов денег,
  • сложных изменений,
  • миграций.

Использование в Go (database/sql) — базовый пример

type User struct {
ID int64
Email string
Name string
}

func GetActiveUsers(ctx context.Context, db *sql.DB, limit, offset int) ([]User, error) {
rows, err := db.QueryContext(ctx, `
SELECT id, email, name
FROM users
WHERE active = true
ORDER BY id
LIMIT $1 OFFSET $2
`, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()

var res []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email, &u.Name); err != nil {
return nil, err
}
res = append(res, u)
}
return res, rows.Err()
}

Почему это важно для сильного разработчика:

  • Вы должны:
    • уверенно писать SELECT/INSERT/UPDATE/DELETE;
    • уметь делать JOIN'ы;
    • использовать GROUP BY и агрегаты;
    • понимать NULL и EXISTS;
    • мыслить транзакциями там, где критична целостность.
  • Это нужно:
    • для реализации бизнес-логики,
    • для написания интеграционных тестов,
    • для анализа и оптимизации проблемных запросов.

Краткий "must have" список для интервью:

  • SELECT, WHERE, ORDER BY, LIMIT/OFFSET.
  • INSERT, UPDATE, DELETE.
  • INNER JOIN, LEFT JOIN.
  • GROUP BY, HAVING, агрегаты.
  • Работа с NULL (IS NULL / IS NOT NULL).
  • Подзапросы, EXISTS.
  • Базовое понимание транзакций.

Все, что меньше этого, — уровень "читал про SQL", а не инструментально рабочий навык.

Вопрос 36. Какие существуют основные типы баз данных и в чем их различия?

Таймкод: 00:22:44

Ответ собеседника: правильный. Называет реляционные и нереляционные БД: реляционные — табличная структура, нереляционные — хранение в формате ключ-значение/JSON.

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

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

Крупные группы:

  1. Реляционные (SQL)
  2. Документные
  3. Key-Value хранилища
  4. Колончатые (Column-family / Column-oriented)
  5. Графовые
  6. Специализированные (time-series, search и др.)

Разберем кратко и по существу.

  1. Реляционные базы данных (RDBMS)

Примеры:

  • PostgreSQL, MySQL, MariaDB, Oracle, SQL Server.

Модель:

  • Данные в виде таблиц (строки/колонки).
  • Связи между таблицами через внешние ключи (FOREIGN KEY).
  • Язык запросов — SQL.

Ключевые свойства:

  • Строгая схема (schema-on-write):
    • типы колонок,
    • ограничения (NOT NULL, UNIQUE, CHECK),
    • внешние ключи.
  • ACID-транзакции:
    • атомарность,
    • согласованность,
    • изолированность,
    • долговечность.

Когда использовать:

  • Финансы, биллинг, заказы, пользователи:
    • строгая целостность данных,
    • сложные JOIN'ы,
    • отчетность и аналитика по транзакциям.
  • Микросервисы, где важна:
    • предсказуемость схемы,
    • надежная транзакционная модель.

Пример схемы и запроса:

CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);

CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total NUMERIC(10,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now()
);

SELECT u.email, SUM(o.total) AS total_spent
FROM users u
JOIN orders o ON o.user_id = u.id
GROUP BY u.email;
  1. Документные базы данных

Примеры:

  • MongoDB, Couchbase.

Модель:

  • Документы (обычно JSON/BSON).
  • Гибкая схема (schema-on-read):
    • разные документы в одной коллекции могут иметь разные поля.

Особенности:

  • Хорошо ложатся на вложенные структуры.
  • Меньше ceremony при изменении структуры данных.
  • Запросы:
    • собственные диалекты (Mongo Query Language),
    • фильтры/агрегации по полям документа.

Когда использовать:

  • Схема часто меняется.
  • Объекты по сути документные:
    • профили пользователей,
    • настройки, события.
  • Нужны вложенные структуры без сложных JOIN'ов.

Важно:

  • Целостность и транзакции ограничены (у MongoDB есть транзакции, но это уже сложнее).
  • Связи между сущностями часто реализуются на уровне приложения.
  1. Key-Value хранилища

Примеры:

  • Redis, Memcached, DynamoDB (в простейшем режиме), Etcd.

Модель:

  • Ключ → значение.
  • Значение — непрозрачный blob (строка, бинарь, иногда структуры).

Особенности:

  • Очень быстрое чтение/запись.
  • Часто данные в памяти.
  • Ограниченные возможности фильтрации и запросов — обычно нужен ключ.

Когда использовать:

  • Кэш (сессии, токены, результаты запросов).
  • Хранение конфигурации.
  • Лимитинг (rate limit counters), очереди, блокировки.

Пример (Redis, концептуально):

  • SET session:123 "{}"
  • GET session:123

В связке с Go:

  • использовать для кэша поверх PostgreSQL.
  1. Колончатые / Column-family базы

Важно разделять:

  • Column-family (NoSQL в духе Bigtable):
    • Cassandra, HBase.
  • Column-oriented аналитические:
    • ClickHouse, Apache Parquet + Presto/Trino, Vertica.

Column-family (Cassandra):

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

Колончатые аналитические (ClickHouse):

  • Данные хранятся по колонкам, а не по строкам.
  • Очень эффективны для аналитики:
    • агрегации,
    • сканы по большим таблицам.
  • Используются для:
    • логов,
    • метрик,
    • BI-отчетов.

Пример (ClickHouse):

SELECT
user_id,
count(*) AS hits
FROM events
WHERE event_time >= now() - INTERVAL 1 DAY
GROUP BY user_id;
  1. Графовые базы данных

Примеры:

  • Neo4j, JanusGraph, Amazon Neptune.

Модель:

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

Когда использовать:

  • Социальные графы,
  • Рекомендательные системы,
  • Сложные связи и пути.

Пример запроса (Cypher для Neo4j):

MATCH (u:User {id: 1})-[:FRIEND_OF]->(f:User)
RETURN f;
  1. Специализированные: Time-series, Search и др.
  • Time-series:

    • InfluxDB, TimescaleDB, VictoriaMetrics, Prometheus TSDB.
    • Оптимизированы под метрики, события по времени, downsampling.
  • Search:

    • Elasticsearch, OpenSearch, Solr.
    • Полнотекстовый поиск, сложные запросы по тексту, релевантность.
  • Message storage:

    • Kafka, NATS JetStream — лог событий/сообщений.

Различия в ключевых аспектах

  1. Модель данных:
  • Реляционные:
    • таблицы, связи, строгая схема.
  • NoSQL:
    • документы, ключ-значение, графы, колонки;
    • гибче, но ответственность за консистентность часто на приложении.
  1. Транзакционность и целостность:
  • RDBMS:
    • ACID — сильные гарантии.
  • Многие NoSQL:
    • eventual consistency,
    • ограниченные транзакции,
    • фокус на доступности и масштабировании (CAP).
  1. Гибкость схемы:
  • RDBMS:
    • schema-on-write — изменения требуют миграций.
  • Документные:
    • schema-on-read — легко добавлять новые поля, но следить сложнее.
  1. Масштабирование:
  • RDBMS:
    • традиционно вертикально (scale up), но есть шардинг/репликация.
  • NoSQL:
    • изначально ориентированы на горизонтальное масштабирование (scale out).

Практический инженерный вывод:

  • Не существует "лучшей" БД вообще, есть подходящая под конкретный use-case.

Типичные комбинации:

  • Транзакционный контур:
    • PostgreSQL / MySQL.
  • Кэш:
    • Redis.
  • Логи и события:
    • ClickHouse / Elasticsearch / Kafka.
  • Конфиг/синхронизация:
    • Etcd/Consul.
  • Сложные связи:
    • Графовая БД (иногда).

Для сильного ответа на интервью важно:

  • Уметь четко разделить:
    • реляционные vs нереляционные не только по формату хранения, но и по транзакционности, схеме, масштабируемости.
  • Назвать конкретные типы NoSQL:
    • document, key-value, column-family, graph.
  • Понимать, в каких сценариях каждый тип оправдан, и как он будет использоваться совместно с остальными компонентами системы.

Вопрос 37. На каком языке ты обучался и какие группы типов данных в нём существуют?

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

Ответ собеседника: неполный. Указывает Java, перечисляет int, boolean, String и коллекции; с подсказкой вспоминает деление на примитивные и ссылочные типы, но неуверенно и без системности.

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

Речь о Java. В Java типы данных делятся на две принципиальные группы:

  • примитивные типы (primitive types);
  • ссылочные типы (reference types).

Важно не только перечислить, но и понимать различия по памяти, поведению и влиянию на код.

Примитивные типы

Примитивы хранят "сырые" значения (значимые типы), не являются объектами (до автоупаковки) и размещаются либо на стеке (в локальных переменных), либо внутри объектов как поля.

Основные примитивы:

  1. boolean
  • Логическое значение: true или false.
  1. byte
  • 8-битное целое число со знаком.
  • Диапазон: -128..127.
  1. short
  • 16-битное целое со знаком.
  • Диапазон: -32768..32767.
  1. int
  • 32-битное целое со знаком.
  • Основной рабочий тип для целых.
  • Диапазон: -2^31..2^31-1.
  1. long
  • 64-битное целое со знаком.
  • Для больших чисел, идентификаторов, таймстэмпов.
  1. float
  • 32-битное число с плавающей точкой (одинарная точность).
  1. double
  • 64-битное число с плавающей точкой (двойная точность).
  • Основной тип для вещественных вычислений (с оговоркой о неточности для денег).
  1. char
  • 16-битный символ (UTF-16 code unit).

Особенности примитивов:

  • Не могут быть null (кроме случая автоупаковки/оберток).
  • Имеют значения по умолчанию в полях:
    • 0, 0.0, false, '\u0000'.
  • Участвуют в арифметике без накладных расходов (без выделения объектов).

Ссылочные типы

Ссылочные типы хранят ссылку на объект в памяти (heap). Переменная такого типа указывает на объект или содержит null.

К ссылочным типам относятся:

  1. Классы (class)
  • Например:
    • String
    • Integer, Long (обертки над примитивами)
    • Пользовательские классы (User, Order и т.д.)
  1. Интерфейсы (interface)
  • Определяют контракты поведения.
  • Переменные типа интерфейса указывают на объект, реализующий этот интерфейс.
  1. Массивы
  • Например:
    • int[], String[], User[].
  • Всегда ссылочный тип, даже если элементы — примитивы.
  1. Enum
  • Перечисления, по реализации — особый вид классов.
  1. Коллекции и обобщенные типы
  • List<String>, Map<Long, User>, Set<Integer> и т.д.
  • Все это ссылочные типы, хранят ссылки на элементы.

Ключевые различия между примитивами и ссылочными типами

  1. Хранение и передача:
  • Примитивы:
    • хранят значение напрямую,
    • при передаче в метод копируются по значению.
  • Ссылочные:
    • переменная хранит ссылку;
    • при передаче в метод копируется ссылка (тоже "по значению"), но обе переменные указывают на один объект.
  1. null:
  • Примитивы:
    • не могут быть null.
  • Ссылочные:
    • могут быть null → риск NullPointerException, если не проверять.
  1. Автоупаковка (autoboxing) и обертки:
  • Для каждого примитива есть wrapper-класс:
    • int → Integer,
    • long → Long,
    • boolean → Boolean и т.д.
  • Автоупаковка/распаковка:
    • Integer x = 1; // int → Integer
    • int y = x; // Integer → int
  • Важно:
    • Wrapper может быть null → при распаковке NPE.
    • Есть накладные расходы по памяти и производительности.
  1. Поведение в коллекциях:
  • Коллекции (List, Map, Set) работают только с объектами (ссылочными типами).
  • При использовании примитивов происходит автоупаковка:
    • List<Integer>, а не List<int>.

Инженерные акценты:

  • Правильный выбор:
    • примитивы:
      • для счетчиков, флагов, чисел, где null не нужен;
    • обертки:
      • когда значение может отсутствовать (null),
      • при работе с коллекциями и generic-API.
  • Осознанное отношение к null:
    • не использовать null без необходимости;
    • для опциональности в современных подходах — использовать явные объекты, результаты, либо аналоги Optional (где уместно).

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

  • Язык: Java.
  • Типы делятся на:
    • примитивные (8 штук: boolean, byte, short, int, long, float, double, char);
    • ссылочные (классы, интерфейсы, массивы, enum, коллекции и т.п.).
  • Примитивы хранят значения напрямую, ссылочные — адрес объекта.
  • Примитивы не могут быть null, ссылочные могут; это влияет на NPE, использование в коллекциях и автопакетирование.

Вопрос 38. Какие примитивные типы данных есть в Java?

Таймкод: 00:24:09

Ответ собеседника: неполный. Называет только char и boolean, без полного списка.

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

В Java существует ровно 8 примитивных типов данных. Они хранят "сырые" значения (а не ссылки на объекты) и являются фундаментом для эффективной работы с памятью и производительностью.

Список примитивных типов:

  1. boolean
  • Логический тип: true или false.
  • Используется для условий, флагов.
  1. byte
  • 8-битное целое со знаком.
  • Диапазон: -128..127.
  • Применение:
    • работа с бинарными данными,
    • экономия памяти в больших массивах.
  1. short
  • 16-битное целое со знаком.
  • Диапазон: -32768..32767.
  • Сейчас используется редко; иногда для экономии памяти.
  1. int
  • 32-битное целое со знаком.
  • Диапазон: примерно -2.1e9..2.1e9.
  • Основной тип для целых чисел в Java-коде.
  1. long
  • 64-битное целое со знаком.
  • Диапазон: очень большое целое.
  • Используется для:
    • идентификаторов,
    • временных меток (timestamp),
    • счетчиков, которые могут выйти за пределы int.
  1. float
  • 32-битное число с плавающей точкой (одинарная точность).
  • Используется редко; когда важна компактность, а не точность.
  1. double
  • 64-битное число с плавающей точкой (двойная точность).
  • Основной тип для вещественных чисел.
  • Важно:
    • не подходит для денежных расчетов из-за ошибок округления; для денег лучше BigDecimal.
  1. char
  • 16-битное значение (code unit UTF-16).
  • Представляет символ или единицу кодировки.
  • Используется для работы с символами на низком уровне; в современном коде чаще работают со String.

Ключевые моменты, которые важно понимать:

  • Примитивы:

    • не являются объектами;
    • не могут быть равны null;
    • хранятся непосредственно как значения;
    • быстро обрабатываются виртуальной машиной.
  • Для каждого примитивного типа есть соответствующий ссылочный wrapper-тип:

    • boolean → Boolean
    • byte → Byte
    • short → Short
    • int → Integer
    • long → Long
    • float → Float
    • double → Double
    • char → Character

Автоупаковка/распаковка (autoboxing/unboxing) работает между примитивами и их обертками, но:

  • обертки могут быть null (риск NullPointerException при распаковке),
  • имеют накладные расходы по памяти и производительности.

Уверенное перечисление всех 8 примитивных типов — базовый минимум, ожидаемый при работе с Java.

Вопрос 39. Что относится к ссылочным типам данных в Java?

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

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

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

В Java все типы делятся на:

  • примитивные (8 штук),
  • ссылочные (reference types).

К ссылочным типам относятся ВСЕ типы, не являющиеся примитивами. Переменная ссылочного типа хранит не само значение объекта, а ссылку (адрес) на объект в heap-памяти (или null).

К основным категориям ссылочных типов относятся:

  1. Классы (class)
  • Любой пользовательский или стандартный класс:
    • String
    • BigDecimal
    • LocalDateTime
    • ArrayList, HashMap
    • Любой ваш User, Order, ServiceConfig и т.д.
  • Переменная типа класса содержит ссылку на объект или null.

Примеры:

String s = "hello";           // s — ссылка на объект String
User user = new User(); // user — ссылка на объект User
Integer x = Integer.valueOf(5); // ссылка на объект-обертку
  1. Интерфейсы (interface)
  • Интерфейс сам по себе — ссылочный тип.
  • Переменная интерфейсного типа хранит ссылку на объект, реализующий этот интерфейс.
Runnable r = () -> System.out.println("run"); // r — ссылка на объект, реализующий Runnable
  1. Массивы
  • Любой массив в Java — ссылочный тип, даже если элементы — примитивные.
    • int[] — ссылочный тип,
    • String[] — ссылочный тип.
int[] arr = new int[10];      // arr — ссылка на массив
String[] names = new String[5]; // names — ссылка на массив ссылок на String
  1. Enum
  • Перечисления реализуются как особый вид класса.
  • Enum-типы — ссылочные.
  • Каждое значение enum — это объект указанного enum-класса (однотонные синглтоны).
enum Status { NEW, PAID, SHIPPED }

Status st = Status.NEW; // st — ссылка на объект enum-константы
  1. Обобщенные типы и коллекции (generics, collections)
  • Любые generics работают только со ссылочными типами.
  • Коллекции (List, Set, Map) — всегда ссылочные типы, их элементы — ссылки на объекты (включая обертки над примитивами).
List<String> list = new ArrayList<>();
Map<Long, User> users = new HashMap<>();
  1. Классы-обертки над примитивами (wrapper types)
  • Integer, Long, Boolean, Double и т.д. — это ссылочные типы.
  • Они:
    • хранятся по ссылке,
    • могут быть null,
    • используются в коллекциях и generics.
Integer n = null; // допустимо (в отличие от int)

Ключевые отличия ссылочных типов от примитивов:

  • Могут быть null:
    • попытка обращения к методу/полю при nullNullPointerException.
  • Передача в методы:
    • копируется ссылка, а не объект;
    • несколько переменных могут указывать на один и тот же объект.
  • Использование в коллекциях и generics:
    • коллекции не работают с примитивами напрямую, только с ссылочными типами.
  • По памяти и производительности:
    • объекты (включая обертки) дороже примитивов:
      • заголовок объекта,
      • аллокация в heap,
      • GC.

Типичное инженерное понимание (как надо отвечать):

  • К ссылочным типам относятся:
    • все классы (включая String и wrapper-типы),
    • интерфейсы,
    • массивы,
    • enum-типы,
    • любые коллекции и объекты.
  • Переменная ссылочного типа хранит ссылку на объект или null, в отличие от примитивов, которые хранят значение напрямую.

Вопрос 40. Что представляет собой тип данных String в Java?

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

Ответ собеседника: неполный. Говорит, что это строка и с ней можно выполнять операции (склеивание, изменение), но не раскрывает ключевые свойства: неизменяемость, ссылочный тип, особенности хранения и работы.

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

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

Основные свойства String:

  1. Ссылочный тип, а не примитив
  • String — это класс в пакете java.lang.

  • Переменная типа String хранит ссылку на объект, а не сами символы "внутри переменной".

  • Пример:

    String s = "hello"; // s — ссылка на объект String
  1. Неизменяемость (immutability)

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

  • Объекты String неизменяемы:
    • Любая операция "изменения" строки (конкатенация, replace, substring и т.п.) создает НОВЫЙ объект String.
    • Исходный объект остается неизменным.

Пример:

String s1 = "Hello";
String s2 = s1 + " World";

System.out.println(s1); // "Hello"
System.out.println(s2); // "Hello World"
  • s1 не изменился; s2 указывает на новый объект.

Последствия:

  • Безопасность:
    • Строки можно безопасно использовать как ключи в Map, в качестве параметров, логов и т.д.
    • Подходят для констант и идентификаторов (имена таблиц, полей, ролей и т.д.).
  • Потокобезопасность:
    • Один и тот же объект String можно читать из разных потоков без синхронизации.
  • Но:
    • Частые конкатенации в цикле могут приводить к созданию множества временных объектов и бить по производительности.
  1. String Pool (строковый пул)
  • Литералы строк, указанные в коде, попадают в специальный пул (string pool).
  • Если две строки являются одинаковыми строковыми литералами, они, как правило, указывают на один и тот же объект в пуле.

Пример:

String a = "test";
String b = "test";

System.out.println(a == b); // true (один и тот же объект из пула)
System.out.println(a.equals(b)); // true
  • == сравнивает ссылки, equals — содержимое.
  • Через new String("test") вы создадите новый объект вне пула:
String c = new String("test");
System.out.println(a == c); // false
System.out.println(a.equals(c)); // true
  1. Основные операции со строками

Хотя String неизменяем, над ним есть множество удобных методов, каждый из которых возвращает новую строку:

  • length()
  • charAt(int index)
  • substring(int begin, int end)
  • toUpperCase(), toLowerCase()
  • trim()
  • replace(...), replaceAll(...)
  • split(...)
  • contains(...), startsWith(...), endsWith(...)
  • equals(...), equalsIgnoreCase(...)
  • format(...)
  • конкатенация:
    • оператор +
    • String.concat(...)
    • String.join(...)

Важно:

  • Использование + в простых выражениях компилятор оптимизирует через StringBuilder.
  • В циклах или при сборке больших строк лучше явно использовать StringBuilder:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();
  1. Связь с обертками и коллекциями
  • String часто используется как:
    • ключ в Map,
    • значение в конфигурациях,
    • идентификатор в доменных моделях.
  • Благодаря неизменяемости:
    • безопасен как ключ в хэш-структурах (HashMap, HashSet),
    • хэш-код можно кэшировать (что и делает String).
  1. Инженерные акценты (как правильно понимать на интервью)

Правильное, "сильное" объяснение String в Java включает:

  • это ссылочный тип (класс), находящийся в java.lang;
  • строки неизменяемы:
    • любые изменения создают новый объект;
    • это важно для безопасности и потокобезопасности;
  • существует string pool:
    • литералы переиспользуются;
  • сравнение:
    • содержимое — через equals,
    • == — только для проверки ссылок (т.е. очень аккуратно и осознанно).

Формулировка, которой достаточно:

String в Java — это неизменяемый ссылочный тип (класс), представляющий последовательность символов. Объекты String хранятся в куче, строковые литералы — в пуле. Любые операции "изменения" возвращают новый объект, что делает строки безопасными для использования в многопоточности и в качестве ключей в коллекциях, но требует внимательного отношения к производительности при частых конкатенациях.

Вопрос 41. Является ли String в Java изменяемым или неизменяемым типом?

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

Ответ собеседника: неправильный. Не может ответить и не понимает суть вопроса.

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

String в Java — неизменяемый (immutable) ссылочный тип.

Это значит:

  • После создания объекта String его содержимое (последовательность символов) нельзя изменить.
  • Любая операция, которая выглядит как "изменение строки" (конкатенация, replace, substring и т.п.), на самом деле возвращает НОВЫЙ объект String, оставляя исходный без изменений.

Примеры:

String s1 = "Hello";
String s2 = s1 + " World";

System.out.println(s1); // "Hello"
System.out.println(s2); // "Hello World"
  • s1 не изменился, s2 — новая строка.
String s = "test";
s.toUpperCase();
System.out.println(s); // по-прежнему "test"

s = s.toUpperCase();
System.out.println(s); // теперь "TEST"

Метод toUpperCase() вернул новую строку; чтобы "изменение" отразилось в переменной, мы присваиваем результат обратно.

Почему String сделали неизменяемым:

  • Безопасность:

    • Строки часто используются для:
      • ключей в Map,
      • значений в кэше,
      • путей, имен классов, идентификаторов,
      • параметров безопасности (логины, роли).
    • Если бы их можно было менять "по ссылке", это ломало бы инварианты и позволяло бы неожиданно менять поведение других частей системы.
  • Потокобезопасность:

    • Один и тот же объект String можно безопасно использовать в разных потоках без синхронизации.
  • Эффективность string pool:

    • Строковые литералы переиспользуются (String Pool).
    • Это возможно именно потому, что строки не меняются:
      • один объект "test" может безопасно использоваться везде.

Практическое следствие:

  • Частые конкатенации в цикле:
    • создают много временных String-объектов;
    • нужно использовать StringBuilder/StringBuffer (они изменяемые):
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();

Кратко:

  • String в Java — неизменяемый ссылочный тип.
  • "Изменение" строки всегда создает новый объект; это важно понимать для корректного, безопасного и производительного кода.

Вопрос 42. Что такое String Pool в Java?

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

Ответ собеседника: неправильный. Говорит, что не знает.

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

String Pool (intern pool, пул строк) в Java — это специальная область памяти, где хранятся уникальные экземпляры строк, предназначенная для оптимизации памяти и ускорения работы со строковыми литералами.

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

  1. Зачем нужен String Pool
  • Строки используются повсюду:
    • литералы в коде,
    • ключи в map,
    • имена полей, путей, параметров.
  • Без пула:
    • каждый литерал "test" везде создавал бы новый объект,
    • память тратилась бы на множество идентичных строк.
  • Пул позволяет:
    • переиспользовать один экземпляр одинаковых строк,
    • уменьшить расход памяти,
    • ускорить сравнение (в некоторых случаях достаточно сравнить ссылки).
  1. Как работает String Pool с литералами

Когда в коде пишут:

String a = "hello";
String b = "hello";

Происходит следующее:

  • "hello" как литерал помещается в String Pool (если его там еще нет).
  • Переменные a и b указывают на один и тот же объект в пуле.
  • Поэтому:
System.out.println(a == b);      // true (одна и та же ссылка)
System.out.println(a.equals(b)); // true (одинаковое содержимое)

Важно:

  • == для строк безопасно использовать только тогда, когда заведомо знаем, что обе — из пула / interned.
  • В общем случае — только equals для сравнения содержимого.
  1. new String(...) и отличие от литералов

Если явно создать строку через new:

String a = "hello";
String c = new String("hello");

System.out.println(a == c); // false — разные объекты
System.out.println(a.equals(c)); // true — содержимое одинаковое
  • Литерал "hello" — в пуле.
  • new String("hello") создает новый объект вне пула, даже если такая строка уже есть в пуле.
  1. Метод intern()

String.intern() возвращает ссылку на строку из пула.

  • Если в пуле уже есть строка с таким содержимым:
    • возвращается ссылка на существующий объект.
  • Если нет:
    • строка добавляется в пул и возвращается ссылка на нее.

Пример:

String a = "hello";
String c = new String("hello");

String d = c.intern();

System.out.println(a == c); // false
System.out.println(a == d); // true: d теперь указывает на строку из пула

Применение:

  • Можно явно использовать intern для уменьшения числа дубликатов строк (особенно часто встречающихся ключей, статусов, идентификаторов).
  • Но злоупотреблять не стоит:
    • пул — глобальный,
    • чрезмерное добавление строк может привести к увеличению памяти.
  1. Связь со свойством неизменяемости String

String Pool возможен именно потому, что строки неизменяемы:

  • Если бы строку можно было изменить после помещения в пул:
    • изменение в одном месте сломало бы все остальные, кто использует ту же ссылку;
    • это разрушило бы саму идею пула и безопасность.

Неизменяемость гарантирует:

  • одна interned-строка может безопасно использоваться многими частями программы.
  1. Практические выводы для уверенного разработчика
  • Понимать разницу:
    • "hello" — пул,
    • new String("hello") — новый объект.
  • Для сравнения строк:
    • использовать equals, а не ==, если только вы не работаете осознанно с interned-строками.
  • Понимать влияние на память:
    • String Pool уменьшает дублирование литералов;
    • intern() можно использовать точечно для повторяющихся значений (например, статусы, типы), но не для всего подряд.

Краткая формулировка:

String Pool — это глобальный пул уникальных строк, в котором хранятся строковые литералы и interned-значения. Он существует для экономии памяти и ускорения работы с часто повторяющимися строками и опирается на неизменяемость типа String.

Вопрос 43. Для чего используется ключевое слово final в Java?

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

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

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

Ключевое слово final в Java — это средство ограничения изменения. В зависимости от места применения (переменная, метод, класс) оно накладывает разные, но концептуально родственные ограничения. Понимание final важно для корректной архитектуры, потокобезопасности и читаемости кода.

Основные применения final:

  1. final-переменные: нельзя менять ссылку/значение
  2. final-методы: нельзя переопределять
  3. final-классы: нельзя наследовать

Разберем по пунктам.

  1. final для переменных

Семантика:

  • Значение переменной (или ссылка) присваивается один раз и не может быть изменено позже.
  • Это не "глубокая неизменяемость объекта", а запрет на повторное присваивание.

Примеры:

  • Локальные переменные и параметры:
final int x = 10;
// x = 20; // ошибка компиляции

void process(final String id) {
// id нельзя переназначить: id = "other"; // ошибка
}
  • Поля класса (часто для констант и immutable-объектов):
class User {
private final String email; // инициализируется в конструкторе, потом неизменен

User(String email) {
this.email = email;
}

public String getEmail() {
return email;
}
}
  • Статические константы (по convention: public static final + UPPER_SNAKE_CASE):
public static final int MAX_RETRY_COUNT = 3;

Важно отличать:

  • Для примитивов:
    • final int x — числовое значение зафиксировано.
  • Для ссылочных типов:
    • final User user — нельзя переназначить ссылку user,
    • но сам объект, на который она указывает, МОЖЕТ быть изменяемым.
final List<String> list = new ArrayList<>();
list.add("a"); // ОК — изменяем содержимое списка
// list = new ArrayList<>(); // ошибка — нельзя переназначить ссылку

Чтобы объект был реально неизменяемым, нужна:

  • неизменяемая внутренняя структура,
  • отсутствие сеттеров,
  • final-поля,
  • отсутствие "утечек" внутренних изменяемых ссылок наружу.
  1. final для методов

Семантика:

  • final-метод нельзя переопределить в подклассе.

Пример:

class Base {
public final void log(String msg) {
System.out.println(msg);
}
}

class Child extends Base {
// @Override
// public void log(String msg) { ... } // так нельзя — компилятор выдаст ошибку
}

Когда полезно:

  • Фиксация критичного поведения:
    • логика безопасности,
    • инварианты, которые не должны ломаться наследованием.
  • Стабильность API:
    • защита от несанкционированного override со стороны пользователей библиотеки.
  1. final для классов

Семантика:

  • final-класс нельзя наследовать.

Пример:

public final class String {
// ...
}
final class MyService {
// ...
}

// class ExtendedService extends MyService {} // ошибка компиляции

Когда это уместно:

  • Класс спроектирован как завершенный:
    • неизменяемые value-объекты,
    • утилитарные классы,
    • классы безопасности.
  • Хотим:
    • упростить reasoning о поведении (нет сюрпризов через override),
    • позволить JVM лучше оптимизировать (в т.ч. inlining).
  1. Инженерные аспекты использования final

Почему final — не просто "синтаксис", а инструмент дизайна:

  • Явная неизменяемость:
    • final-поля + отсутствие сеттеров → упрощают понимание кода.
    • меньше состояний → меньше багов.
  • Потокобезопасность:
    • корректно инициализированные final-поля видны всем потокам после завершения конструктора (happens-before гарантии Java Memory Model).
    • Это ключевой момент при проектировании immutable-объектов для concurrent-кода.
  • Прозрачность намерений:
    • final показывает читателю и компилятору, что:
      • эта ссылка не будет переназначена;
      • этот метод не должен переопределяться;
      • этот класс не предназначен для наследования.
  1. Типичные ошибки и заблуждения
  • "final делает объект неизменяемым" — нет:
    • final ограничивает переназначение переменной/поля;
    • изменяемость/неизменяемость объекта определяется его внутренней реализацией.
  • Игнорирование final там, где важны инварианты:
    • поля конфигурации, id, константы домена лучше делать final.
  • Наследование от классов, которые не были спроектированы для наследования:
    • если класс не задуман для расширения, разумно сделать его final, чтобы избежать "хрупкого" наследования.

Краткая формулировка для ответа:

  • final в Java:
    • для переменных — запрет на повторное присваивание;
    • для полей — основа для констант и immutable-объектов;
    • для методов — запрет переопределения в наследниках;
    • для классов — запрет наследования.
  • Используется для:
    • усиления предсказуемости кода,
    • безопасности и инкапсуляции,
    • корректной работы с многопоточностью,
    • устранения нежелательных вариаций поведения через наследование.

Вопрос 44. Для чего используется ключевое слово static в Java и как ведёт себя статическая переменная?

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

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

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

Ключевое слово static в Java означает принадлежность не объекту, а классу. Оно влияет на то, как переменные и методы существуют в памяти и как к ним обращаться.

Важно разделять:

  • static-поля (статические переменные),
  • static-методы,
  • static-блоки и вложенные классы.
  1. Общая идея static

Если упростить:

  • Нестатические члены (поля, методы):

    • принадлежат конкретному объекту (экземпляру класса);
    • для доступа нужен объект: obj.method(), obj.field.
  • Статические члены:

    • принадлежат самому классу;
    • существуют в единственном (или фиксированном) экземпляре на JVM/класс-лоадер;
    • доступны без создания объекта: ClassName.method(), ClassName.field.
  1. Статические переменные (static fields)

Статическое поле:

  • Объявляется с ключевым словом static внутри класса.
  • Общее для всех экземпляров класса:
    • все объекты видят одно и то же статическое поле;
    • изменение значения отражается для всех.

Пример:

public class Counter {
public static int total; // статическая переменная

public Counter() {
total++; // считаем, сколько раз создавали объект
}
}

Counter c1 = new Counter();
Counter c2 = new Counter();
System.out.println(Counter.total); // 2

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

  • static НЕ означает "только внутри класса":
    • область видимости определяется модификаторами доступа:
      • public / protected / default / private.
  • static НЕ означает "нельзя изменять":
    • изменяемость определяется final, а не static.
    • public static int x можно свободно менять:
      • ClassName.x = 42;

Чтобы сделать "настоящую константу" в Java, обычно используют:

public static final int MAX_SIZE = 100;

Здесь:

  • static — принадлежит классу;
  • final — нельзя переназначить;
  • по convention: UPPER_SNAKE_CASE.
  1. Статические методы (static methods)

Статический метод:

  • Не привязан к конкретному объекту;
  • Не имеет доступа к нестатическим полям/методам напрямую, т.к. у него нет this;
  • Вызывается по имени класса.

Пример:

public class MathUtil {
public static int add(int a, int b) {
return a + b;
}
}

int sum = MathUtil.add(2, 3);

Правила:

  • Внутри static-метода можно:
    • обращаться к другим static-полям/методам,
    • нельзя обращаться к полям экземпляра без объекта.
  • Типичный паттерн:
    • утилитарные функции,
    • фабричные методы,
    • вспомогательные константы.
  1. static-блоки и статические вложенные классы (кратко)
  • static-блок инициализации:
public class Config {
public static final Map<String, String> SETTINGS = new HashMap<>();

static {
SETTINGS.put("env", "prod");
SETTINGS.put("region", "eu-central");
}
}
  • выполняется один раз при загрузке класса.

  • static nested class:

class Outer {
static class Inner {
// не требует экземпляра Outer
}
}
  1. Типичные использования static
  • Константы:
public class HttpStatus {
public static final int OK = 200;
public static final int NOT_FOUND = 404;
}
  • Утилитарные классы:
public class Strings {
public static boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}
}
  • Глобальные счетчики, реестры (но с осторожностью):
    • статическое состояние = shared mutable state,
    • требует аккуратной синхронизации в многопоточной среде.
  1. Инженерные акценты и анти-паттерны
  • static — это глобальное состояние:

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

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

    • static-поля должны быть потокобезопасны:
      • immutable-объекты (final + неизменяемые поля),
      • или синхронизация / concurrent-структуры.
  1. Краткое корректное резюме
  • static:
    • привязывает поле или метод к классу, а не к экземпляру.
  • Статическая переменная:
    • одна на все экземпляры (per classloader),
    • может быть изменяемой или нет (это задает final, а не static),
    • видимость определяется модификаторами доступа, а не самим static.
  • Статические методы:
    • вызываются без создания объекта,
    • не работают с this и нестатическими полями/методами напрямую.

Вопрос 45. Что означает тип возвращаемого значения void у метода?

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

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

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

В Java (и ряде других языков) ключевое слово void в сигнатуре метода означает, что метод:

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

Примеры:

void logMessage(String msg) {
System.out.println(msg);
}

void updateUserName(User user, String newName) {
user.setName(newName); // меняем состояние переданного объекта
}

Особенности:

  • В методе с void можно (но не обязательно) использовать оператор return без значения:
    • для раннего выхода из метода.
void process(int value) {
if (value < 0) {
return; // просто выйти из метода
}
// ...
}
  • Попытка вернуть значение из void-метода — ошибка компиляции.

Связь с архитектурой и тестированием:

  • Методы с void часто выполняют побочные эффекты:
    • меняют состояние полей,
    • пишут в лог,
    • отправляют события, делают внешние вызовы.
  • Для удобства тестирования и ясности логики обычно лучше:
    • по возможности возвращать результат (новое состояние, объект, статус),
    • минимизировать скрытые побочные эффекты.
  • Но для явно "процедурных" действий (логирование, нотификация, fire-and-forget вызовы) void естественен.

Вопрос 46. Каково назначение интерфейсов в Java?

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

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

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

Интерфейс в Java — это контракт, который определяет, что тип "умеет делать" (набор методов), не задавая, как именно это реализовано. Интерфейсы — ключевой инструмент для:

  • абстракции;
  • полиморфизма;
  • снижения связанности;
  • тестируемости и расширяемости системы.

Главные идеи и назначение интерфейсов:

  1. Контракт без реализации

Интерфейс описывает только публичное поведение:

  • сигнатуры методов (имя, параметры, тип возвращаемого значения);
  • до Java 8 — без реализации;
  • в современных версиях возможны default/static методы, но базовая идея — контракт.

Пример:

public interface Notifier {
void notify(String message);
}

Это означает:

  • "любой, кто реализует Notifier, обязан уметь выполнить notify(message)".
  1. Сокрытие деталей реализации

Код, который использует интерфейс, не зависит от конкретного класса:

public class EmailNotifier implements Notifier {
@Override
public void notify(String message) {
// отправка email
}
}

public class SmsNotifier implements Notifier {
@Override
public void notify(String message) {
// отправка SMS
}
}

public class AlertService {
private final Notifier notifier;

public AlertService(Notifier notifier) {
this.notifier = notifier;
}

public void sendAlert(String msg) {
notifier.notify(msg);
}
}

Здесь:

  • AlertService работает с абстракцией Notifier.
  • Ему не важно, email это, SMS, push или логгер.
  • Мы можем подставить любую реализацию, не меняя AlertService.

Это и есть:

  • снижение связанности,
  • реализация принципа Dependency Inversion (DIP),
  • удобство для тестирования.
  1. Полиморфизм

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

void notifyAll(List<Notifier> notifiers, String msg) {
for (Notifier n : notifiers) {
n.notify(msg); // полиморфный вызов
}
}

Каждый элемент списка может быть:

  • EmailNotifier,
  • SmsNotifier,
  • MockNotifier для тестов,
  • и т.д.

Выбор реализации — деталь, контракт один.

  1. Тестируемость и подмена реализаций

Интерфейсы позволяют легко подставить фейковую/мок-реализацию в тестах:

class MockNotifier implements Notifier {
List<String> sent = new ArrayList<>();
@Override
public void notify(String message) {
sent.add(message);
}
}

@Test
void testAlertService() {
MockNotifier mock = new MockNotifier();
AlertService svc = new AlertService(mock);

svc.sendAlert("Test");

assertEquals(1, mock.sent.size());
assertEquals("Test", mock.sent.get(0));
}

Это:

  • убирает зависимость теста от реальных внешних сервисов;
  • позволяет тестировать только бизнес-логику.
  1. Интерфейсы и архитектура

Интерфейсы — ключевой инструмент разделения слоёв:

  • Интерфейсы репозиториев:
public interface UserRepository {
User findById(long id);
void save(User user);
}
  • Реализации:
    • JdbcUserRepository (PostgreSQL/SQL),
    • InMemoryUserRepository (для тестов),
    • CachedUserRepository (с кэшем поверх основного).

Код сервисного слоя зависит от UserRepository, а не от конкретного JDBC/ORM-кода. Это:

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

Интерфейс:

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

Это сознательная конструкция:

  • коду "снаружи" достаточно знать интерфейс;
  • всё, что не включено в интерфейс, можно свободно менять, не ломая пользователей.
  1. Связь с Go (важна для мышления)

В Go интерфейсы играют аналогичную роль:

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

Этот опыт легко переносится на Java и наоборот:

  • интерфейсы везде — способ проектировать по контракту, а не по конкретному типу.

Краткая формулировка для интервью:

  • Интерфейс в Java — это контракт, задающий набор методов без конкретной реализации.
  • Нужен для:
    • абстракции и сокрытия реализации,
    • полиморфизма (работы с разными реализациями через общий тип),
    • снижения связанности,
    • удобства тестирования (подмена реализаций на моки/фейки),
    • реализации принципов SOLID (особенно DIP и ISP).
  • Интерфейс не про "магический доступ", а про чётко определенное публичное поведение компонента.

Вопрос 47. Какие коллекции в Java существуют и чем они отличаются?

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

Ответ собеседника: неполный. Называет List и Map, сомневается, относится ли Map к коллекциям; упоминает LinkedList без уверенности, опирается на старые знания.

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

В Java коллекции — это набор стандартных структур данных из пакета java.util, построенных вокруг нескольких базовых интерфейсов. Важно:

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

Ключевой момент: интерфейс Collection и интерфейс Map — разные ветви. Формально Map не наследует Collection, но в широком смысле Map тоже относят к "коллекциям" Java.

Базовые интерфейсы и их назначение

  1. Collection<E>
  • Общий интерфейс для коллекций элементов:
    • List<E>, Set<E>, Queue<E> и др.
  • Описывает базовые операции:
    • добавление, удаление, проверка размера, contains, итерация.
  1. List<E>
  • Упорядоченная коллекция, допускающая дубликаты.
  • Доступ по индексу.
  • Индексированный последовательный список.

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

  • ArrayList

    • Динамический массив.
    • Быстрый случайный доступ: O(1).
    • Быстрая добавка в конец (амортизированно O(1)).
    • Вставка/удаление из середины — O(n).
    • Типичный "список по умолчанию".
  • LinkedList

    • Двусвязный список.
    • Быстрая вставка/удаление при наличии ссылки на узел (O(1)).
    • Случайный доступ по индексу — O(n).
    • Использовать, когда нужно много вставок/удалений внутри, но не для рандомного доступа.

Примеры:

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
String v = list.get(0); // "a"
  1. Set<E>
  • Множество: уникальные элементы, без дубликатов.

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

  • HashSet

    • Основан на хэш-таблице.
    • Нет гарантированного порядка.
    • Операции add/contains/remove обычно O(1).
    • Использовать как "множество по умолчанию".
  • LinkedHashSet

    • Сохраняет порядок вставки.
    • Чуть больше накладных расходов.
  • TreeSet

    • Отсортированное множество (по натуральному порядку или компаратору).
    • Основан на красно-черном дереве.
    • Операции O(log n).
    • Подходит, когда важно упорядочивание.

Пример:

Set<String> set = new HashSet<>();
set.add("a");
set.add("a"); // дубликат не добавится
  1. Map<K, V>
  • Отображение (ассоциативный массив): ключ → значение.
  • Не наследует Collection, но является основной коллекционной структурой Java.

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

  • HashMap

    • Самая распространенная реализация.
    • Нет порядка ключей.
    • Операции get/put/remove обычно O(1).
    • Требует корректной реализации hashCode() и equals() у ключей.
  • LinkedHashMap

    • Сохраняет порядок вставки или порядок доступа.
    • Удобен для:
      • предсказуемой итерации,
      • LRU-кешей (через access-order).
  • TreeMap

    • Отсортирован по ключу (натуральный порядок или компаратор).
    • O(log n) для операций.
    • Полезен, когда нужны диапазонные операции и сортировка.

Пример:

Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
Integer v = map.get("a"); // 1
  1. Queue<E> и Deque<E>
  • Queue — для работы по принципу FIFO (очередь).
  • Deque — двусторонняя очередь (push/pop с обоих концов).

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

  • ArrayDeque
    • Быстрая реализация Deque без синхронизации.
    • Используется как стек или очередь.
  • LinkedList
    • Реализует и List, и Deque.

Примеры:

Queue<String> q = new ArrayDeque<>();
q.add("a");
q.add("b");
q.poll(); // "a"
  1. Специализированные и потокобезопасные коллекции

Для многопоточных систем и высоких нагрузок важно знать:

  • Collections.synchronizedXXX:
    • оболочки над обычными коллекциями, добавляющие synchronized.
  • java.util.concurrent:
    • ConcurrentHashMap — потокобезопасная Map с высокой производительностью.
    • CopyOnWriteArrayList, ConcurrentLinkedQueue, ConcurrentSkipListMap/Set и др.
  • EnumSet / EnumMap:
    • эффективные коллекции для enum-ключей/значений.

Инженерные акценты (как выбирать)

  • List:

    • ArrayList — по умолчанию.
    • LinkedList — только для специфичных кейсов (много вставок в середину, работа как Deque).
  • Set:

    • HashSet — по умолчанию.
    • LinkedHashSet — если важен порядок вставки.
    • TreeSet — если важна сортировка.
  • Map:

    • HashMap — по умолчанию.
    • LinkedHashMap — когда нужен порядок.
    • TreeMap — для отсортированных ключей.
  • Queue/Deque:

    • ArrayDeque — для очередей/стеков.
  • Многопоточность:

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

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

  • Знаю основные интерфейсы:
    • Collection, List, Set, Map, Queue, Deque.
  • Использовал:
    • List (ArrayList, LinkedList),
    • Set (HashSet),
    • Map (HashMap, LinkedHashMap),
    • Queue/Deque (ArrayDeque).
  • Понимаю различия:
    • List — упорядоченная последовательность, допускает дубликаты.
    • Set — множество уникальных элементов.
    • Map — пары ключ-значение.
    • Hash* — хэш-структуры, O(1) в среднем.
    • Tree* — отсортированные, O(log n).
    • LinkedHash* — сохраняют порядок вставки.

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