12k
All articles

Интеграционные тесты vs сквозные тесты

Интеграционные и end-to-end тесты для веб-приложений: четкие определения, JS-инструменты и когда использовать Vitest, MSW, Playwright или Cypress.

OpenReplay Team
OpenReplay Team
Интеграционные тесты vs сквозные тесты

Интеграционные тесты проверяют, что ваши собственные компоненты и модули корректно работают вместе — они выполняются в CI за миллисекунды и имитируют внешние системы; сквозные тесты управляют всем запущенным приложением через реальный UI, как это делает пользователь, запускаются против задеплоенной или staging-сборки и ничего не имитируют. Это единственное различие снимает большую часть путаницы, однако скрывает вторую проблему, специфичную для frontend-разработки: слово «интеграционный» означает разные вещи для JavaScript-разработчика и backend QA-инженера, а граница между «интеграционным» и «E2E» в браузере действительно размыта.

Эта статья проводит чёткую границу в терминах веб-стека. Здесь вы найдёте точные определения с конкретными примерами, сравнение по критериям, которые реально влияют на выбор типа теста, место каждого из них относительно юнит-тестов, JavaScript-инструменты, соответствующие каждому типу (Vitest, Jest, Testing Library, MSW, Playwright, Cypress), и однозначную рекомендацию по распределению тестового бюджета — без расплывчатого «используйте оба».

Ключевые выводы

  • Интеграционные тесты рендерят ваши собственные модули совместно с замоканной сетью и выполняются в CI за миллисекунды; сквозные тесты управляют задеплоенным приложением в реальном браузере и ничего не мокают.
  • В JavaScript-приложении «интеграционный тест» обычно означает Vitest или Jest плюс Testing Library и MSW; «сквозной тест» обычно означает Playwright или Cypress против вашего реального URL.
  • Граница между интеграционными и E2E-тестами во frontend — это спектр, а не стена: где её провести — вопрос бюджета, а не закон.
  • Пишите преимущественно интеграционные тесты ради оптимального соотношения уверенности и затрат, поддерживайте небольшой набор из трёх-пяти E2E-тестов для наиболее важных пользовательских сценариев и не пытайтесь покрыть E2E-тестами всё подряд.
  • Баг в продакшене — это, по определению, сценарий, который ваши тесты никогда не проверяли; именно поэтому воспроизведение сессии при реальном сбое — кратчайший путь к недостающему тесту.

Интеграционные тесты vs сквозные тесты: краткий обзор

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

КритерийИнтеграционный тестСквозной (E2E) тест
ОхватНесколько собственных модулей совместно (например, компонент + слой данных)Всё запущенное приложение, от frontend до backend
СкоростьМиллисекунды — единицы секундСекунды — минуты на тест
Среда запускаЛокально и в CI, в Node или jsdomПротив задеплоенной или staging-сборки, в реальном браузере
Что имитируетсяВнешние системы — сеть, сторонние APIНичего (или как можно меньше)
НестабильностьНизкая — нет реальной сети, нет реальных таймингов браузераВыше — зависит от стабильности всей системы
Стоимость / поддержкаНиже при написании и поддержке в рабочем состоянииВыше; наборы деградируют по мере изменения приложения
Уникально обнаруживаетНарушения контрактов API, некорректные ответы, проблемы связывания состоянияАутентификацию, редиректы, конфигурацию окружения, сторонние скрипты, межстраничное состояние

Что такое интеграционный тест: веб-пример

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

Конкретный пример: компонент LoginForm, связанный с хуком аутентификации и запросом профиля. Интеграционный тест рендерит форму, заполняет поля, отправляет её и проверяет, что отображается состояние приветствия — при этом вызовы POST /api/login и GET /api/profile перехватываются и обрабатываются моком. Вы тестируете связывание между формой, хуком, слоем запросов и итоговым рендером. Вы не тестируете реальный backend, провайдер аутентификации или CDN.

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

Что такое сквозной тест: веб-пример

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

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

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

Место в иерархии: пирамида и трофей

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

Современной альтернативой служит тестовый трофей, популяризированный Кентом К. Доддсом, который расширяет интеграционный слой, поскольку интеграционные тесты обеспечивают наилучшее соотношение уверенности и затрат для веб-приложений — близко к реальному использованию, но без нестабильности и времени выполнения полноценных E2E. Эта идея опирается на руководящий принцип Testing Library: чем больше ваши тесты напоминают реальное использование программного обеспечения, тем больше уверенности они дают. Интеграционный тест, рендерящий реальное дерево компонентов и проверяющий видимое пользователем поведение, гораздо ближе к реальному использованию, чем юнит-тест изолированного редьюсера — и при этом выполняется за миллисекунды. Воспринимайте трофей как широко принятый современный подход для веб-приложений, а не как универсальную замену пирамиде.

Размытая граница во frontend

В frontend-разработке граница между интеграционными и E2E-тестами — это спектр, а не стена: тест на Testing Library, рендерящий поддерево компонентов, — это «интеграционный тест»; тест на Playwright, загружающий ваш реальный URL, — это «E2E»; а где именно провести границу — вопрос бюджета, а не закон. Именно эта размытость является главным источником путаницы, поскольку одно и то же слово означает разные вещи в разных контекстах.

В backend QA-литературе интеграционное тестирование делится на стратегии bottom-up, top-down и big-bang — эта таксономия реальна, но представляет собой серверный жаргон, который редко помогает разработчику, работающему с Vitest и Playwright. Игнорируйте её. Для веб-разработчика важно практическое соответствие: «интеграционный тест» почти всегда означает рендеринг дерева компонентов с замоканной сетью, а «E2E» почти всегда означает управление задеплоенным приложением в реальном браузере. Серая зона посередине — например, тест на Playwright, направленный на локальный dev-сервер с перехватом сети через MSW, — вполне допустима; просто заранее решите, из какой части бюджета он финансируется.

JS-инструментарий для каждого типа с примерами кода

В JavaScript-приложении «интеграционный тест» обычно означает рендеринг дерева компонентов и проверку его поведения с замоканной сетью — как правило, Vitest или Jest плюс Testing Library и MSW — тогда как «сквозной тест» означает управление задеплоенным приложением в реальном браузере с помощью Playwright или Cypress.

Mock Service Worker (MSW) — ключевой элемент, делающий интеграционные тесты реалистичными без реального backend. Он перехватывает запросы на сетевом уровне в обеих средах — через Service Worker API в браузере (setupWorker) и через низкоуровневый перехват запросов в Node (setupServer, используемый в тестах Vitest или Jest, которому не нужен файл service worker). Поскольку перехват происходит на границе, а не через патчинг fetch, одни и те же обработчики работают в интеграционных тестах, на dev-сервере и даже в запусках Playwright; в документации это сформулировано прямо: «Представьте, что вы используете одни и те же API-моки при разработке, интеграционном и сквозном тестировании, а также в Storybook».

Ниже представлена та же функциональность входа и получения профиля в виде интеграционного теста. Обратите внимание на API MSW v2 http + HttpResponse, заменивший паттерн res/ctx из v1:

// login.integration.test.tsx — Vitest 4 + @testing-library/react 16 + MSW 2
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { LoginForm } from './LoginForm'

const server = setupServer(
  http.post('/api/login', () =>
    HttpResponse.json({ token: 'abc' })
  ),
  http.get('/api/profile', () =>
    HttpResponse.json({ name: 'Ada' })
  )
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

test('logs in and shows the profile name', async () => {
  render(<LoginForm />)
  await userEvent.type(screen.getByLabelText(/email/i), 'ada@example.com')
  await userEvent.type(screen.getByLabelText(/password/i), 'hunter2')
  await userEvent.click(screen.getByRole('button', { name: /sign in/i }))
  expect(await screen.findByText(/welcome, ada/i)).toBeInTheDocument()
})

Браузер не запускается, сервер не поднимается, а сеть полностью контролируется — поэтому тест выполняется быстро и детерминированно. Та же функциональность в виде E2E-теста запускается против задеплоенного приложения и ничего не мокает. Playwright управляет Chromium, Firefox и WebKit через единый API с автоматическим ожиданием и web-first-утверждениями:

// login.e2e.spec.ts — Playwright 1.6x
import { test, expect } from '@playwright/test'

test('user logs in and sees their profile', async ({ page }) => {
  await page.goto('/login')
  await page.getByLabel('Email').fill('ada@example.com')
  await page.getByLabel('Password').fill('hunter2')
  await page.getByRole('button', { name: 'Sign in' }).click()
  await expect(
    page.getByRole('heading', { name: /welcome, ada/i })
  ).toBeVisible()
})

Утверждения выглядят похоже; различается всё, что находится под ними. Спецификация Playwright доказывает, что реальный endpoint аутентификации, редирект и сессионный куки работают в реальном браузере — и платит за это временем выполнения и нестабильностью. Один практический нюанс, заслуживающий внимания: поскольку асинхронные React Server Components — новая технология, Jest пока не поддерживает их, и Next.js рекомендует E2E-тесты для асинхронных компонентов — конкретный случай, когда интеграционный уровень ещё не справляется и E2E оправдывает своё место.

Правило выбора по сценарию

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

  • Новое взаимодействие модулей или контракт frontend-backend → интеграционный тест. Связывание компонента с новым endpoint, нового редьюсера с новым селектором, формы со слоем валидации: рендерите дерево, мокайте сеть через MSW, проверяйте поведение.
  • Критический пользовательский сценарий через весь стек → один E2E-тест. Вход, регистрация, оформление заказа — тот единственный сценарий, поломка которого стоит денег или репутации.
  • Всё остальное → опирайтесь на интеграционные тесты и не покрывайте это E2E.

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

Почему E2E-наборы деградируют и как их стабилизировать

Наборы E2E-тестов деградируют, потому что каждый тест зависит от стабильности всей системы; решение — держать их немногочисленными, проверять видимые пользователю результаты, а не детали реализации, и удалять те, что больше не защищают доход. Переименованный тестовый идентификатор, замедленный staging-деплой, нестабильный сторонний виджет или изменившийся редирект могут окрасить прошедший набор в красный без какой-либо реальной регрессии — а набор, кричащий «волк», в итоге игнорируют. Конкретно:

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

Однако ни один набор не покрывает сценарии, которые вы не предусмотрели — а баг в продакшене, по определению, является путём, который ваши интеграционные и E2E-тесты никогда не проверяли. Именно здесь воспроизведение сессии оправдывает своё применение: запись реального сбоя показывает точный непротестированный сценарий, который привёл к поломке — состояние DOM, сетевые вызовы и ошибки консоли в момент сбоя. Воспроизведения сессий для сценариев входа и оформления заказа нередко выявляют режимы отказа, которые не воспроизводились ни в одном локальном тесте — например, специфичный для окружения редирект или сторонний скрипт, заблокированный CSP. Профессиональный подход — превратить это воспроизведение в недостающий тест: поломка на уровне сценария становится новой спецификацией Playwright, поломка компонента или контракта — новым интеграционным тестом на Testing Library + MSW, и тот же баг не сможет регрессировать.

Заключение

Разделение, которое работает в продакшене: преимущественно интеграционные тесты, несколько высокоценных E2E и юнит-тесты в основании для чистой логики — интеграционные потому, что они обеспечивают максимальную уверенность на единицу времени выполнения; E2E потому, что некоторые баги существуют только в реально задеплоенном браузере. Начните с написания интеграционных тестов на Vitest или Jest, Testing Library и MSW для границ модулей вашей следующей фичи, зарезервируйте Playwright для нескольких сценариев, которые вы не можете позволить себе сломать, и позвольте реальным продакшен-сбоям подсказать вам, какой тест вы забыли написать.

Часто задаваемые вопросы

Можно ли с помощью одного инструмента — Playwright или Cypress — писать как интеграционные, так и сквозные тесты?

Да, но различие определяется тем, как вы ограничиваете тест, а не инструментом. Playwright и Cypress могут монтировать компонент в изоляции с замоканной сетью — это фактически интеграционный тест — или загружать ваш задеплоенный URL и задействовать весь стек, что является сквозным тестом. В обоих инструментах существуют режимы тестирования компонентов, однако Vitest или Jest с Testing Library остаются более распространённым путём для интеграционного тестирования в экосистеме JavaScript.

Нужен ли файл mockServiceWorker.js для MSW в интеграционных тестах на Vitest или Jest?

Нет. Файл mockServiceWorker.js требуется только для браузерного пути с использованием setupWorker, который опирается на Service Worker API. Интеграционные тесты на основе Node работают через setupServer, который перехватывает запросы с помощью низкоуровневого расширения классов, а не через service worker, поэтому генерируемый файл не нужен. Скрипт воркера генерируется только при мокировании запросов в реальном браузере — например, при локальной разработке или браузерных запусках тестов.

Когда следует писать сквозной тест вместо интеграционного?

Пишите сквозной тест, когда предотвращаемый баг существует только в реальном деплое — аутентификация, редиректы, конфигурация окружения, сессионные куки, сторонние скрипты, нарушения Content Security Policy или межстраничное состояние. Всё это не воспроизводится при замоканной сети. Для всего, что находится между вашими собственными модулями — нарушения контрактов API, некорректные ответы или проблемы связывания состояния — интеграционный тест быстрее, дешевле и стабильнее. Резервируйте сквозные тесты для наиболее ценных сценариев, приносящих доход.

Почему мои сквозные тесты проходят локально, но падают в CI?

Наборы E2E-тестов зависят от стабильности всей системы, поэтому различия между локальной средой и CI ломают их без какой-либо реальной регрессии. Распространённые причины: фиксированные задержки вместо автоматического ожидания, хрупкие CSS-селекторы, меняющиеся между сборками, более медленный деплой в CI, общие или неизолированные тестовые данные и нестабильные сторонние виджеты. Предпочитайте селекторы по роли и метке, используйте автоматическое ожидание Playwright, изолируйте тестовые данные для каждого запуска и держите набор достаточно маленьким для обеспечения надёжности.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.