- Что такое пирамида тестирования?
- unit vs integration vs e2e?
🎯 Зачем спрашивают
- Проверить, понимает ли кандидат цели и уровни тестирования, а не просто знает инструменты.
- Убедиться, что кандидат умеет оценивать баланс между скоростью, надёжностью и стоимостью тестов (понимает, зачем нужны unit, integration и E2E, и когда какой писать).
- Понять, насколько кандидат знаком с современными практиками фронтенд-тестирования:
react-testing-library,playwright,cypress, визуальные и контрактные тесты.
- Оценить инженерную зрелость: воспринимает ли тесты как часть CI/CD и культуры качества, а не как «обязанность QA».
- Проверить, умеет ли кандидат говорить о качестве кода через тестирование — то есть связывает тесты, архитектуру и надёжность продукта.
📝 Ответ
Ручное тестирование
- Дорогостоящая по времени операция. Каждый раз нужно производить вручную огромное количество проверок;
- Человеческий фактор. Какой-то сценарий можно забыть (если тестовые сценарии нигде не фиксируются).
Автоматизированные тесты
- Переиспользуемость. Автоматизированный тест пишется один раз и далее его можно прогонять сколько душе угодно (если не изменяются требования к коду);
- Модульность. Можно тестировать компоненты системы по отдельности;
- Гибкость. Под конкретный тест-сценарий можно воссоздать определенные условия. Отсюда следует, что с помощью автоматизированных тестов можно покрыть множество сценариев поведения кода;
Виды автоматизированного тестирования
Оно и понятно, ведь качество обеспечивается не только тестами.
Для фронтенда есть другая версия данной пирамиды. Ее называют “трофей тестирования”, где упор идет на static и integration уровни
Unit tests
Пример unit теста на утилиту
import { getCookie } from '../cookies'; const mockDocumentCookie = (cookies: string) => { Object.defineProperty(document, 'cookie', { get: () => cookies, configurable: true }); }; describe('getCookie', () => { beforeEach(() => { mockDocumentCookie(''); }); test('возвращает undefined когда кука не найдена', () => { mockDocumentCookie('existing=value'); expect(getCookie('nonexistent')).toBeUndefined(); }); test('возвращает правильное значение для существующей куки', () => { mockDocumentCookie('test=123; another=abc'); expect(getCookie('test')).toBe('123'); expect(getCookie('another')).toBe('abc'); }); test('возвращает правильное значение для закодированной куки', () => { mockDocumentCookie('encoded=hello%20world'); expect(getCookie('encoded')).toBe('hello world'); }); test('обрабатывает куки с лишним = в значении', () => { mockDocumentCookie('complex=key=value'); expect(getCookie('complex')).toBe('key=value'); }); test('тримит отступы у значений и ключей', () => { mockDocumentCookie(' spaced = value ; another=test'); expect(getCookie('spaced')).toBe('value'); expect(getCookie('another')).toBe('test'); }); test('возвращает первое совпадение при наличии дубликата', () => { mockDocumentCookie('duplicate=first; duplicate=second'); expect(getCookie('duplicate')).toBe('first'); }); });
- jest
- vitest
- mochajs
✅ Плюсы | ❌ Минусы |
Очень быстрые.
Затраты по времени и ресурсам для компьютера минимальны. Для запуска данных тестов системе требуются миллисекунды или секунды (все зависит от кол-ва тестов). Можно запускать параллельно. | Много моков и стабов
При написании unit тестов приходится мокать внешнее API, мокать методы, мокать окружение. Мок моком погоняет |
Просты в написании.
Так как вы тестируете конкретный элемент в системе, достаточно просто подготовить для него тестовые данные или эмулировать среду. | Создается впечатление, что ты тестируешь реализацию — а не поведение. |
Component tests
Пример теста на компонент
import { render, screen, fireEvent } from '@testing-library/react'; import Counter from './Counter'; describe('Counter', () => { test('показывает начальное значение', () => { render(<Counter initial={5} />); expect(screen.getByTestId('count')).toHaveTextContent('Счётчик: 5'); }); test('увеличивает значение при клике на кнопку "Увеличить"', () => { render(<Counter initial={0} />); fireEvent.click(screen.getByText('Увеличить')); expect(screen.getByTestId('count')).toHaveTextContent('Счётчик: 1'); }); test('уменьшает значение при клике на кнопку "Уменьшить"', () => { render(<Counter initial={2} />); fireEvent.click(screen.getByText('Уменьшить')); expect(screen.getByTestId('count')).toHaveTextContent('Счётчик: 1'); }); });
- Jest/Vitest + react-testing-library
- playwright (component testing режим)
- cypress (component testing режим)
Пример кода
import { render, fireEvent } from '@testing-library/react'; import { Counter } from './Counter'; test('увеличивает счётчик при клике', () => { const { container } = render(<Counter />); // ❌ Тест зависит от структуры DOM. Поменяется реализация -> тест упадет const button = container.querySelector('.wrapper > .increase-btn'); const title = container.querySelector('h2'); fireEvent.click(button!); // ❌ Проверяем конкретный текст — даже пробел сломает тест expect(title?.textContent).toBe('Count value: 1'); });
- Использовать методы поиска элементы, приближенных к «пользовательскому» опыту:
- поиск по семантической роли: кнопки, ссылки, заголовки (
getByRoleметод из react-testing-library) - поиск по тексту (
getByTextметод из react-testing-library)
- Проверять результат действий, а не детали DOM.
- Избегать прямых моков внутренних хуков.
Пример кода
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Counter } from './Counter'; test('увеличивает счётчик при клике', async () => { render(<Counter />); // ✅ Находим элементы по смыслу, как это сделал бы пользователь const button = screen.getByRole('button', { name: '+1' }); const title = screen.getByRole('heading', { level: 2 }); await userEvent.click(button); // ✅ Проверяем поведение, а не структуру expect(title).toHaveTextContent(/1/); });
✅ Плюсы | ❌ Минусы |
Очень быстрые.
Затраты по времени и ресурсам для компьютера минимальны. Для запуска данных тестов системе требуются миллисекунды или секунды (все зависит от кол-ва тестов). Можно запускать параллельно. | Много моков и стабов
При написании тестов приходится мокать внешнее API, мокать методы, мокать окружение. Мок моком погоняет |
Просты в написании.
Так как вы тестируете конкретный компонент в системе, достаточно просто подготовить для него тестовые данные или эмулировать среду. | Тесты становятся хрупкими и зависят от внутренней реализации компонента, а не от пользовательского поведения. |
ㅤ | ㅤ |
Integration
Пример интеграционного теста компонента
import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; export default function CounterWithStore() { const count = useSelector((state) => state.counter.value); const dispatch = useDispatch(); return ( <div> <p data-testid="count">Счётчик: {count}</p> <button onClick={() => dispatch({ type: 'counter/increment' })}> Увеличить </button> </div> ); }
import { configureStore, createSlice } from '@reduxjs/toolkit'; const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { increment: (state) => { state.value += 1; }, }, }); export const store = configureStore({ reducer: { counter: counterSlice.reducer }, });
import { render, screen, fireEvent } from '@testing-library/react'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import counterReducer from './store'; import CounterWithStore from './CounterWithStore'; function renderWithStore(ui, { initialState } = {}) { const store = configureStore({ reducer: { counter: counterReducer.reducer }, preloadedState: { counter: initialState || { value: 0 } }, }); return render(<Provider store={store}>{ui}</Provider>); } describe('CounterWithStore', () => { test('отображает значение из Redux и изменяет его при клике', () => { renderWithStore(<CounterWithStore />, { initialState: { value: 5 } }); expect(screen.getByTestId('count')).toHaveTextContent('Счётчик: 5'); fireEvent.click(screen.getByText('Увеличить')); expect(screen.getByTestId('count')).toHaveTextContent('Счётчик: 6'); }); });
- тестирует связку компонента с реальной логикой стора.
- Не мокаются
useSelectorилиuseDispatch.
- ваш сборщик
- приложение и UI компоненты
- логика
- конкретный браузер и форм фактор (телефон, планшет, десктоп)
Пример интеграционного теста на playwright
import { test, expect } from '@playwright/test'; test.describe('Login page (integration)', () => { test('успешный логин при корректных данных', async ({ page }) => { await page.route('**/api/login', async route => { const body = await route.request().postDataJSON(); if (body.email === 'user@example.com' && body.password === '123456') { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ token: 'mock-token-123' }), }); } else { await route.fulfill({ status: 401, body: JSON.stringify({ message: 'Unauthorized' }), }); } }); await page.goto('/login'); await page.getByPlaceholder('Email').fill('user@example.com'); await page.getByPlaceholder('Пароль').fill('123456'); await page.getByRole('button', { name: 'Войти' }).click(); await expect(page.getByText('Добро пожаловать!')).toBeVisible(); }); test('показывает ошибку при неверном логине или пароле', async ({ page }) => { await page.route('**/api/login', async route => { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ message: 'Unauthorized' }), }); }); await page.goto('/login'); await page.getByPlaceholder('Email').fill('wrong@example.com'); await page.getByPlaceholder('Пароль').fill('wrongpass'); await page.getByRole('button', { name: 'Войти' }).click(); await expect(page.getByText('Неверный логин или пароль')).toBeVisible(); }); });
- playwright
- cypress
Integration vs E2E
✅ Плюсы | ❌ Минусы |
Позволяет протестировать приложение в приближенной к боевой среде | Если интеграционных тестов много и не используются средства оптимизации (распараллеливание), то такие тесты могут выполнятся часами |
Тесты могут писать сами разработчики. Тесты ближе к коду, а значит, ошибки могут обнаруживаться раньше | Не все проекты удобно тестировать. Например, приложение-игра на canvas.
|
ㅤ | Некоторые инструменты (cypress) дают возможность параллелить тесты только через платную фичу. |
ㅤ | Нестабильные тесты. Могут быть ложные срабатывания из-за технических особенностей инструментов |
E2E
- Никаких моков
- Запускается реальный фронтенд против реального бэкенда (часто на staging).
- Проверяются реальные сценарии пользователя: логин, создание заказа, обработка ошибок, logout и т. д.
- Часто требуют тестовых данных / сидирования БД / очистки окружения.
- Дольше выполняются, менее стабильны, но дают уверенность “всё работает вместе”.
- playwright
- cypress
- Puppeteer
- Selenium / WebDriverIO
Screenshot tests
- шрифты
- субпиксельный рендер
- анимации
- Percy
- Applitools
- Chromatic
- pixelmatch
- reg-suit
- Loki
✅ Плюсы | ❌ Минусы |
Удобно детектить ошибки/неточности интерфейса | Восприимчивы к среде выполнения. На разных устройствах, браузерах и операционных системах могут быть разные результаты |
Хорошо подходят для UI-китов | При низком treshhold может находится множество “ошибок”, заметных только машине, но не человеческому глазу
|
ㅤ | Большинство коробочных решений платные |
⚖️ Компромиссы
Уровень | Скорость | Надёжность | Стоимость поддержки | Основной риск |
Unit | ⚡ Высокая | Средняя | Низкая | Тест реализаций, а не поведения |
Component | ⚡ Высокая | Средняя | Низкая | Тест реализаций, а не поведения |
Integration | 🐢 Средняя
(прямая зависимость от кол-ва тестов) | Высокая | Средняя | Flaky, нестабильность
Дорогие в поддержке |
E2E | 🐢 Низкая
(прямая зависимость от кол-ва тестов) | Очень высокая | Высокая | Flaky, нестабильность
Дороги в поддержке |
🔎 Встречные вопросы
- Чем отличаются unit- и snapshot-тесты?
- Что такое mocking и stub, зачем нужны?
- Что такое fixtures?
- Что такое seeds?
- Когда можно не писать тесты?
- Как бороться с flaky-тестами?
🚩 Красные флаги
- Тесты нужны только QA
- Unit-тесты не нужны, если есть E2E
- Не различает integration vs E2E.
🛠 Практика
📚 Источники / ссылки
kentcdoddsThe Testing Trophy and Testing Classifications
The Testing Trophy and Testing Classifications
How to interpret the testing trophy for optimal clarity
YouTubeUnit tests are useless. Change my mind. Дмитрий Коваленко. JS Fest 2019 Autumn
Unit tests are useless. Change my mind. Дмитрий Коваленко. JS Fest 2019 Autumn
The talk from JS Fest conference 2019 Autumn in Kyiv, Ukraine. Практически каждый разработчик в своей жизни сталкивался с тестированием. Наверное каждый из нас слышал о разных типах тестирования и «пирамиде тестирования». И конечно же каждый знает о важности юнит-тестирования. Но действительно ли юнит-тесты это самый продуктивный подход к тестированию? Presentation: http://bit.ly/2pRSTAU Fb: https://www.facebook.com/JSFestua/ Website: https://jsfest.com.ua Upcoming JS Conference: JS Fest 2020 - 30-31st of October, Kyiv, Ukraine Details and tickets: https://bit.ly/3bBxiiK
YouTubeПочему E2E? - Тестирование
Почему E2E? - Тестирование
Видео создано благодаря подписчикам проекта на нашем Patreon. Хотите получать контент на 3 месяца раньше остальных? Присоединяйтесь! https://patreon.com/javascriptninja