Перейти к содержанию
Фронтенд

10 вопросов по теме «Frontend: Тулинг и тестирование» на собеседовании

В этом материале — 10 вопросов из русской колоды RecallDeck по теме «Frontend: Тулинг и тестирование». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

9 мин чтения10 подробных ответовПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

10 подробных ответов

01

Что делает бандлер и чем Vite отличается от webpack? Почему Vite быстрый в dev-режиме?

Короткий ответ: Бандлер разрешает граф зависимостей и собирает модули (JS, CSS, ассеты) в оптимизированные файлы для браузера. webpack бандлит весь граф и в dev, и в prod; Vite в dev не бандлит — раздаёт исходники через нативный ESM-сервер, транспилируя каждый модуль по запросу на esbuild, а для прода собирает бандл через Rollup.

Подробно:

  1. Зачем бандлер — браузеру нужно мало запросов и валидный код: бандлер склеивает модули, прогоняет CSS/ассеты через лоадеры, делает code splitting и минификацию.
  2. Почему Vite быстр в dev — нет предварительной сборки всего проекта: dev-сервер раздаёт ESM, браузер сам запрашивает нужные модули, а esbuild (на Go) транспилирует в разы быстрее Babel.
  3. HMR — Vite инвалидирует только изменённый модуль, поэтому горячая замена почти мгновенна и не зависит от размера проекта.
webpack Vite
Dev бандлит весь граф нативный ESM, без бандла
Транспиляция Babel / ts-loader esbuild
Prod-сборка webpack Rollup
Старт dev-сервера растёт с проектом почти постоянный

⚠️ Частая ошибка: считать, что Vite вообще не бандлит. В проде он собирает бандл через Rollup — без бандлинга прод страдал бы от водопада сетевых запросов.

02

Что такое транспиляция? Зачем нужны Babel/SWC/tsc и как таргетировать конкретные браузеры?

Короткий ответ: Транспиляция — это перевод современного JS/TS в синтаксис, понятный целевым браузерам. Babel и SWC понижают синтаксис, tsc дополнительно проверяет типы; набор целевых браузеров задаётся через browserslist, а недостающие API добавляются полифилами (core-js).

Подробно:

  1. Зачем транспилировать — новый синтаксис (optional chaining, классы) и TypeScript не исполняются в старых движках; транспилятор понижает синтаксис до поддерживаемого.
  2. Синтаксис ≠ API — транспиляция меняет только синтаксис; новые API (Promise, fetch, Array.flat) нужно добавлять полифилами, иначе в старом браузере будет ReferenceError.
  3. Babel vs SWC vs tsc — Babel гибок (плагины и пресеты), SWC написан на Rust и кратно быстрее, tsc медленнее, но единственный проверяет типы.
{
  "browserslist": [
    ">0.5%",
    "last 2 versions",
    "not dead"
  ]
}

⚠️ Частая ошибка: думать, что транспиляция сама добавит полифилы. Понижение синтаксиса и полифилы API — это разные шаги; без core-js/таргета в browserslist код упадёт на отсутствующем методе.

03

Какие оптимизации делает прод-сборка? Объясните tree-shaking, code splitting, минификацию и content-hash.

Короткий ответ: Прод-сборка удаляет мёртвый код (tree-shaking), бьёт бандл на части (code splitting), сжимает код (минификация) и подставляет хеш контента в имена файлов для долгого кеширования. Tree-shaking работает только с ESM и требует честно проставленного sideEffects.

Подробно:

  1. Tree-shaking — статический анализ import/export выкидывает неиспользуемые экспорты; нужен ESM (не CJS) и поле sideEffects, чтобы бандлер знал, какие модули безопасно удалять.
  2. Code splitting — динамический import() выносит код в отдельные чанки, грузящиеся по требованию (роуты, тяжёлые виджеты).
  3. Минификация — Terser/esbuild убирают пробелы, сокращают имена, удаляют недостижимый код.
  4. Content-hashapp.[hash].js: при неизменном содержимом имя стабильно, поэтому можно ставить Cache-Control: immutable, а при изменении хеш меняется и кеш сбрасывается.
{
  "name": "ui-kit",
  "type": "module",
  "sideEffects": ["*.css"],
  "exports": { ".": "./dist/index.mjs" }
}

⚠️ Частая ошибка: ставить "sideEffects": false при наличии импортов с побочными эффектами (CSS, полифилы, регистрация) — бандлер вырежет их, и в проде пропадут стили или сломается инициализация.

04

Чем отличаются dependencies, devDependencies и peerDependencies? Что значат semver-диапазоны и зачем lock-файл?

Короткий ответ: dependencies нужны в рантайме, devDependencies — только для сборки и тестов, peerDependencies — то, что предоставляет хост-приложение (типично для библиотек, например React). Semver-диапазон (^, ~) задаёт допустимые версии, а lock-файл фиксирует точное дерево для воспроизводимости.

Подробно:

  1. Где что лежитdependencies едут в прод-бандл; devDependencies (vite, vitest, eslint, типы) не нужны в рантайме; peerDependencies нельзя дублировать — библиотека требует, но не ставит React сама.
  2. Semver^1.4.0 разрешает minor+patch (<2.0.0), ~1.4.0 — только patch (<1.5.0), точная 1.4.0 — без обновлений.
  3. Lock-файлpackage-lock.json / pnpm-lock.yaml пинит конкретные версии всего дерева; коммитится в репозиторий, иначе у CI и коллег будут разные сборки.
{
  "dependencies": { "react": "^18.3.0" },
  "devDependencies": { "vite": "~5.4.0", "vitest": "^2.0.0" },
  "peerDependencies": { "react": ">=18" }
}

pnpm экономит диск через симлинки из общего store; npm и yarn копируют пакеты в каждый node_modules.

⚠️ Частая ошибка: класть @types/* или сборочные пакеты в dependencies, или не коммитить lock-файл — тогда «работает у меня» расходится с CI.

05

ESLint и Prettier — зачем нужны оба и как форсить их в CI и pre-commit?

Короткий ответ: ESLint ловит ошибки и проблемные паттерны (корректность кода), Prettier приводит код к единому форматированию (стиль). Это разные задачи, поэтому держат оба; чтобы не конфликтовали, форматирование целиком отдают Prettier. Форсят их в CI и через pre-commit-хук (husky + lint-staged).

Подробно:

  1. ESLint = корректность — находит no-unused-vars, react-hooks/exhaustive-deps, потенциальные баги; часть правил чинится --fix.
  2. Prettier = форматирование — отступы, кавычки, переносы; убирает споры о стиле в ревью, форматируя детерминированно.
  3. Связкаeslint-config-prettier отключает стилистические правила ESLint, чтобы они не воевали с Prettier.
  4. Enforcement — pre-commit (lint-staged прогоняет только staged-файлы) + жёсткая проверка в CI с --max-warnings=0 и prettier --check.
ESLint Prettier
Цель корректность, баги форматирование
Пример неиспользуемая переменная отступы, кавычки
Конфигурируемость много правил почти нет опций
В CI --max-warnings=0 --check

⚠️ Частая ошибка: заставлять ESLint форматировать код (стилистические правила) — он конфликтует с Prettier; правила форматирования нужно отключить через eslint-config-prettier.

06

Что такое пирамида тестирования во фронтенде? Чем различаются unit-, integration- и e2e-тесты?

Короткий ответ: Пирамида тестирования — это рекомендация по балансу: много дешёвых и быстрых юнит-тестов в основании, меньше интеграционных в середине и совсем немного медленных дорогих e2e на вершине. Чем выше, тем больше уверенность, но тем дороже и медленнее тест.

Подробно:

  1. Unit — изолированная функция/хук/компонент без сети; миллисекунды, запускаются на каждое сохранение.
  2. Integration — несколько модулей вместе (компонент + стор + замоканный API); главный слой во фронтенде, ловит реальные баги взаимодействия.
  3. E2E — реальный браузер проходит сценарий целиком; максимальная уверенность, но медленно и более хрупко.
        /\
       /e2e\        мало · медленно · дорого
      /------\
     / интегра-\     среднее число
    / ционные   \
   /-------------\
  /     юнит       \  много · быстро · дёшево
 /-----------------\

⚠️ Частая ошибка: «перевёрнутая пирамида» (ice-cream cone) — куча медленных e2e и мало юнит-тестов; набор становится медленным и flaky, и ему перестают доверять.

07

Как тестировать компоненты с Vitest/Jest + React Testing Library? Что значит «тестировать поведение, а не реализацию»?

Короткий ответ: RTL рендерит компонент и проверяет то, что видит и делает пользователь, а не внутренности (state, имена методов). Элементы ищут по доступной роли (getByRole), взаимодействие имитируют через user-event, а затем проверяют видимый результат.

Подробно:

  1. Поведение, а не реализация — не лезьте в state/инстанс; проверяйте, что на экране после действия. Тогда рефактор внутренностей не ломает тест.
  2. Запросы по ролиgetByRole('button', { name: ... }) отражает доступность; getByTestId — крайняя мера.
  3. user-event вместо fireEvent — он точнее моделирует реальные действия (фокус, ввод, клавиатуру).
  4. Async — после клика используйте findBy*/await, чтобы дождаться обновления.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'

test('увеличивает счётчик по клику', async () => {
  const user = userEvent.setup()
  render(<Counter />)
  await user.click(screen.getByRole('button', { name: /добавить/i }))
  expect(screen.getByText('Счёт: 1')).toBeInTheDocument()
})

⚠️ Частая ошибка: проверять внутреннее состояние или имена приватных методов вместо видимого результата — тест ломается при рефакторинге, хотя поведение не изменилось.

08

Когда оправданы e2e-тесты и как бороться с flaky-тестами в Playwright/Cypress?

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

Подробно:

  1. Когда оправданы — сквозные критичные сценарии, которые юнит/интеграция не покрывают; не дублируйте в e2e то, что дешевле проверить ниже.
  2. Авто-ожиданиеexpect(locator).toBeVisible() ретраится до таймаута; не нужны sleep.
  3. Изоляция — каждый тест со своим состоянием (свежий контекст, сид данных), чтобы порядок не влиял.
  4. Мок сетиpage.route стабилизирует ответы и убирает зависимость от бэкенда.
import { test, expect } from '@playwright/test'

test('пользователь логинится', async ({ page }) => {
  await page.route('**/api/me', r => r.fulfill({ json: { name: 'Аня' } }))
  await page.goto('/login')
  await page.getByLabel('Email').fill('a@b.com')
  await page.getByRole('button', { name: 'Войти' }).click()
  // авто-ожидание: ретраит, пока не появится
  await expect(page.getByText('Привет, Аня')).toBeVisible()
})

⚠️ Частая ошибка: page.waitForTimeout(...) — фиксированные паузы главный источник flaky; используйте web-first assertions, которые сами ретраятся.

09

Как выглядит CI/CD-пайплайн для фронтенда и зачем нужны preview-окружения?

Короткий ответ: Типичный пайплайн на каждый PR прогоняет шаги по порядку: установка зависимостей → линт → проверка типов → тесты → сборка → деплой preview. Падение любого шага блокирует merge, а preview-окружение разворачивает ветку на уникальном URL, чтобы ревьюер кликал живую сборку, а не читал диф.

Подробно:

  1. Порядок и fail-fast — дешёвые проверки (линт, типы) раньше дорогих (e2e), чтобы быстро падать.
  2. Воспроизводимость — ставьте из lock-файла (pnpm install --frozen-lockfile) и кешируйте node_modules/store, чтобы CI был быстрым и детерминированным.
  3. Preview-деплой — каждый PR получает свой URL (Vercel/Netlify/свой стенд); QA и дизайн смотрят реальное поведение до merge.
  4. CD — после merge в main та же сборка едет в прод.
push / PR


[install] → [lint] → [typecheck] → [test] → [build] → [preview deploy]
  pnpm i    eslint      tsc         vitest    vite      уникальный URL

⚠️ Частая ошибка: гонять CI без зафиксированного lock-файла или без кеша — сборки невоспроизводимы и медленны; всегда ставьте через --frozen-lockfile.

10

Чем отличаются ESM, CJS и UMD, как они резолвятся в браузере и Node, и как правильно публиковать библиотеку?

Короткий ответ: ESM (import/export) — статичный стандарт, который умеет tree-shaking; CJS (require/module.exports) — исторический формат Node; UMD работает и в браузере (<script>), и в Node. Современную библиотеку публикуют с полем exports, отдавая ESM и CJS варианты по условиям резолвинга.

Подробно:

  1. ESM — статические импорты, асинхронная загрузка, нативен в браузере и Node; основа tree-shaking.
  2. CJS — синхронный require, динамичен, плохо шейкается; до сих пор в экосистеме Node.
  3. UMD — обёртка-универсал, цепляется к window; нужен для подключения через <script> без бандлера.
  4. Публикация — поле exports с условиями import/require/types; так потребитель на ESM и на CJS получит правильный файл.
ESM CJS UMD
Синтаксис import/export require обёртка
Среда браузер + Node Node браузер + Node
Tree-shaking да нет нет
Загрузка статич./async синхронная глобал
{
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

⚠️ Частая ошибка: указать только main/module без exports или перепутать условия import/require — потребитель на CJS получит ESM-файл и упадёт с ERR_REQUIRE_ESM.

Источники

Источники и редакционная политика

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

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS