Какие уровни тестирования есть во frontend и какими инструментами их покрывают?
Вопрос
Что такое пирамида тестирования применительно к frontend и какие инструменты используются на каждом уровне?
Короткий ответ
Пирамида тестирования — это соотношение количества тестов по уровням: много быстрых дешёвых unit-тестов (Jest/Vitest) в основании, меньше компонентных тестов, проверяющих поведение (Testing Library), и совсем немного медленных, но самых реалистичных E2E-тестов (Playwright/Cypress) на вершине.
Подробный ответ
- Unit-тесты (Jest / Vitest) — тестируют изолированные функции и модули: утилиты, хелперы, редьюсеры, композаблы без DOM. Запускаются в памяти (jsdom/happy-dom при необходимости DOM-заглушки), выполняются за миллисекунды, легко пишутся в большом количестве. Ловят логические ошибки в изолированном коде, но ничего не знают про то, как модули работают вместе.
- Компонентные тесты (Testing Library —
@testing-library/vue,@testing-library/react) — рендерят компонент и проверяют его поведение с точки зрения пользователя: что видно на экране, что происходит по клику, — а не внутренние детали реализации (не "у компонента state.count === 1", а "после клика на экране написано '1'"). Философия Testing Library намеренно ограничивает доступ к внутренностям компонента (нет прямого доступа к instance/state), чтобы тесты не ломались при рефакторинге реализации и ломались только при реальном изменении поведения. Это медленнее unit-тестов (нужен DOM), но покрывает интеграцию компонента с шаблоном/стилями/событиями. - E2E-тесты (Playwright / Cypress) — запускают приложение целиком в настоящем (или near-real) браузере, кликают по реальному UI, ходят по реальным сетевым запросам (или мокают их на уровне сети). Проверяют полный пользовательский сценарий — логин, оформление заказа — через все слои: фронт, API, иногда БД. Самые медленные и самые дорогие в поддержке (флаки из-за таймингов, сети), но и самые близкие к реальности: ловят баги интеграции, которые unit- и компонентные тесты в принципе не видят.
Компромисс пирамиды: чем выше уровень, тем дороже (время выполнения, время написания, стабильность) и тем ближе к реальному пользовательскому опыту тест. Поэтому основную массу проверок стараются покрыть unit- и компонентными тестами, а E2E оставляют для критичных сквозных сценариев (checkout, авторизация), а не для каждой мелочи.
Отдельная категория — визуальное регрессионное тестирование (screenshot testing): инструмент (Playwright, Chromatic для Storybook, Percy) делает скриншот компонента/страницы и сравнивает попиксельно с эталоном, чтобы ловить непреднамеренные визуальные изменения (сдвинулась кнопка, съехали отступы), которые обычные assertion-тесты не замечают.
Пример
// sum.test.js — unit-тест (Vitest)
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum', () => {
it('складывает два числа', () => {
expect(sum(2, 3)).toBe(5)
})
})
// Counter.test.js — компонентный тест (Testing Library)
import { render, screen, fireEvent } from '@testing-library/vue'
import Counter from './Counter.vue'
test('увеличивает счётчик по клику', async () => {
render(Counter)
const button = screen.getByRole('button', { name: /increment/i })
await fireEvent.click(button)
// проверяем то, что видит пользователь, а не внутренний state
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
// checkout.spec.ts — E2E-тест (Playwright)
import { test, expect } from '@playwright/test'
test('пользователь может оформить заказ', async ({ page }) => {
await page.goto('/cart')
await page.getByRole('button', { name: 'Оформить заказ' }).click()
await page.getByLabel('Email').fill('user@example.com')
await page.getByRole('button', { name: 'Оплатить' }).click()
await expect(page.getByText('Заказ оформлен')).toBeVisible()
})
Дополнительные вопросы
- Почему Testing Library намеренно не даёт лёгкого доступа к internal state компонента?
- Как бороться с "флакающими" (нестабильными) E2E-тестами?
- В каких случаях компонентный тест стоит заменить на E2E, и наоборот?