Какие виды тестирования существуют?

Похожие вопросы:
  • Что такое пирамида тестирования?
  • unit vs integration vs e2e?
 
🎯 Зачем спрашивают
  • Проверить, понимает ли кандидат цели и уровни тестирования, а не просто знает инструменты.
  • Убедиться, что кандидат умеет оценивать баланс между скоростью, надёжностью и стоимостью тестов (понимает, зачем нужны unit, integration и E2E, и когда какой писать).
  • Понять, насколько кандидат знаком с современными практиками фронтенд-тестирования: react-testing-library, playwright, cypress, визуальные и контрактные тесты.
  • Оценить инженерную зрелость: воспринимает ли тесты как часть CI/CD и культуры качества, а не как «обязанность QA».
  • Проверить, умеет ли кандидат говорить о качестве кода через тестирование — то есть связывает тесты, архитектуру и надёжность продукта.
 
📝 Ответ
Выделяют 2 подхода к тестированию:
Ручное тестирование
Специалист (тестировщик, разработчик) вручную тестирует код.
Если мы говорим о тестировании приложения, то ручное тестирование осуществляется через прогон пользовательских сценариев и сравнение текущего результата с ожидаемым.
Если мы говорим про ручное тестирование кода, (например, тестирование API), то мы можем вызывать методы с определенным набором данными и сравнивать результаты.
Минусы данного подхода очевидны:
  • Дорогостоящая по времени операция. Каждый раз нужно производить вручную огромное количество проверок;
  • Человеческий фактор. Какой-то сценарий можно забыть (если тестовые сценарии нигде не фиксируются).
Автоматизированные тесты
Автотесты — вид тестирования, при котором вы описываете с помощью кода тестовые сценарии.
  1. Переиспользуемость. Автоматизированный тест пишется один раз и далее его можно прогонять сколько душе угодно (если не изменяются требования к коду);
  1. Модульность. Можно тестировать компоненты системы по отдельности;
  1. Гибкость. Под конкретный тест-сценарий можно воссоздать определенные условия. Отсюда следует, что с помощью автоматизированных тестов можно покрыть множество сценариев поведения кода;
 

Виды автоматизированного тестирования

Автоматизированное тестирование делится на несколько подтипов:
Пирамида качества
Пирамида качества
Пирамида тестирования
Пирамида тестирования
Данная схема чаще упоминается в различных ресурсах как “Пирамида тестирования” и не включает первую ступень.
На самом же деле, это пирамида обеспечения качества.
Оно и понятно, ведь качество обеспечивается не только тестами.
 
💡
Для фронтенда есть другая версия данной пирамиды. Ее называют “трофей тестирования”, где упор идет на static и integration уровни
Данный подход к организации тестов считается более практичным, потому что фокус на интеграционных тестах позволяет тестировать сценарии в среде, приближенной к реальной. Проще написать несколько сценариев в интеграционных тестах, чем покрывать большой объем функционала десятками unit тестов.
Давайте разберем эту схему подробнее.

Unit tests

Тестирование небольших атомарных кусочков программы (функция, класс, модуль, компонент) в изоляции от всей системы.
Представьте, что вы пишете калькулятор. В этой программе будет функция сложения 2х чисел. Эта функция — unit. Ее вполне можно протестировать в отрыве от всей системы.
Пример 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

Изолированные тесты компонентов, проверяющие их поведение в реальном DOM-окружении, но без внешних интеграций (стора, API, роутинга).
Пример теста на компонент
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> ); }
CounterWithStore.jsx
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 }, });
store.js
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'); }); });
CounterWithStore.test.jsx
Данные тест является интеграционным потому что:
  • тестирует связку компонента с реальной логикой стора.
  • Не мокаются useSelector или useDispatch.
Так же к интеграционным тестам относятся тесты, написанные на playwright/cypress. Данные технологии позволяют запустить ваше приложение целиком, прогнать сценарии, эмулируя поведение пользователя.
В данном случае вы проверяете интеграцию следующих компонентов:
  • ваш сборщик
  • приложение и 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
Данный вид тестов часто путают с E2E тестированием. Главное отличие интеграционных тестов от E2E — контроль данных. Предпочтительно мокать ответы от вашего API, а не использовать настоящий backend для воспроизводимости и атомарности тестов.
E2E тестирование предполагает тестирование пользовательских сценариев на production или stage окружениях, с использованием полноценного API. Поэтому это и есть end to end тестирование. Playwright и Cypress можно использовать и для интеграционных, и для E2E-тестов — всё зависит от того, что вы тестируете: мокнутый API (интеграция) или реальное окружение (E2E).
✅ Плюсы
❌ Минусы
Позволяет протестировать приложение в приближенной к боевой среде
Если интеграционных тестов много и не используются средства оптимизации (распараллеливание), то такие тесты могут выполнятся часами
Тесты могут писать сами разработчики. Тесты ближе к коду, а значит, ошибки могут обнаруживаться раньше
Не все проекты удобно тестировать. Например, приложение-игра на canvas.
Некоторые инструменты (cypress) дают возможность параллелить тесты только через платную фичу.
Нестабильные тесты. Могут быть ложные срабатывания из-за технических особенностей инструментов
 

 

E2E

Цель: проверить весь пользовательский путь через всю систему: UI + API + база + сторонние сервисы.
 
Характерные признаки:
  • Никаких моков
  • Запускается реальный фронтенд против реального бэкенда (часто на staging).
  • Проверяются реальные сценарии пользователя: логин, создание заказа, обработка ошибок, logout и т. д.
  • Часто требуют тестовых данных / сидирования БД / очистки окружения.
  • Дольше выполняются, менее стабильны, но дают уверенность “всё работает вместе”.
 
Технологии для написания тестов:
  • playwright
  • cypress
  • Puppeteer
  • Selenium / WebDriverIO
 

Screenshot tests

Цель: проверить визуальную составляющую компонента/фичи/приложения в некотором сценарии.
 
Делается 2 скриншота и сравниваются друг с другом. Если обнаруживается разница, то выводится тепловая карта различий.
notion image
 
 
При низком threshold может находится множество “ошибок”, заметных только машине, но не человеческому глазу. Типичные причины «шума»:
  • шрифты
  • субпиксельный рендер
  • анимации
 
Технологии:
  • Percy
  • Applitools
  • Chromatic
  • pixelmatch
  • reg-suit
  • Loki
 
✅ Плюсы
❌ Минусы
Удобно детектить ошибки/неточности интерфейса
Восприимчивы к среде выполнения. На разных устройствах, браузерах и операционных системах могут быть разные результаты
Хорошо подходят для UI-китов
При низком treshhold может находится множество “ошибок”, заметных только машине, но не человеческому глазу
Большинство коробочных решений платные
POV: ты, пытающийся увидеть diff между двумя скриншотами в упавшем тесте
POV: ты, пытающийся увидеть diff между двумя скриншотами в упавшем тесте
 
 
 
⚖️ Компромиссы
Уровень
Скорость
Надёжность
Стоимость поддержки
Основной риск
Unit
⚡ Высокая
Средняя
Низкая
Тест реализаций, а не поведения
Component
⚡ Высокая
Средняя
Низкая
Тест реализаций, а не поведения
Integration
🐢 Средняя (прямая зависимость от кол-ва тестов)
Высокая
Средняя
Flaky, нестабильность Дорогие в поддержке
E2E
🐢 Низкая (прямая зависимость от кол-ва тестов)
Очень высокая
Высокая
Flaky, нестабильность Дороги в поддержке
 
🔎 Встречные вопросы
  • Чем отличаются unit- и snapshot-тесты?
  • Что такое mocking и stub, зачем нужны?
  • Что такое fixtures?
  • Что такое seeds?
  • Когда можно не писать тесты?
  • Как бороться с flaky-тестами?
 
🚩 Красные флаги
  • Тесты нужны только QA
  • Unit-тесты не нужны, если есть E2E
  • Не различает integration vs E2E.
 
🛠 Практика
TODO
 
📚 Источники / ссылки
  • YouTubeYouTubeUnit tests are useless. Change my mind. Дмитрий Коваленко. JS Fest 2019 Autumn
  • YouTubeYouTubeПочему E2E? - Тестирование