Виды тестов
Разные виды тестов проверяют разные аспекты системы — от отдельных функций до полного пользовательского сценария.
Зачем нужно
Понимание видов тестов помогает выбрать правильный инструмент для задачи. Unit-тест не поймает проблему интеграции, а E2E-тест не подходит для проверки граничных значений функции. Грамотная стратегия тестирования комбинирует разные виды.
Где используется
В любом проекте с тестами. CI/CD обычно запускает unit и интеграционные тесты на каждый коммит, E2E — перед релизом, smoke — после деплоя.
Предпосылки
Unit-тесты (Модульные)
Проверяют одну функцию или класс в изоляции. Зависимости заменяются моками.
// formatPrice.js
function formatPrice(cents) {
if (typeof cents !== 'number' || cents < 0) {
throw new Error('Цена должна быть положительным числом');
}
return `$${(cents / 100).toFixed(2)}`;
}
// formatPrice.test.js
describe('formatPrice', () => {
test('форматирует центы в доллары', () => {
expect(formatPrice(1999)).toBe('$19.99');
});
test('форматирует ноль', () => {
expect(formatPrice(0)).toBe('$0.00');
});
test('добавляет ведущий ноль для малых сумм', () => {
expect(formatPrice(5)).toBe('$0.05');
});
test('выбрасывает ошибку для отрицательных чисел', () => {
expect( => formatPrice(-100)).toThrow('положительным числом');
});
test('выбрасывает ошибку для не-числа', () => {
expect( => formatPrice('abc')).toThrow;
});
});
Характеристики:
- Быстрые (миллисекунды)
- Изолированные от внешних зависимостей
- Легко писать и поддерживать
- Составляют основу пирамиды тестов
Интеграционные тесты
Проверяют взаимодействие нескольких модулей вместе. Реальные зависимости, но без UI.
// userService.test.js — проверяем сервис + репозиторий вместе
const { UserService } = require('./userService');
const { Database } = require('./database');
describe('UserService интеграция', () => {
let db;
let userService;
beforeEach(async () => {
db = new Database(':memory:'); // реальная БД, но в памяти
await db.migrate;
userService = new UserService(db);
});
afterEach(async () => {
await db.close();
});
test('создаёт пользователя и находит его по email', async () => {
await userService.create({
name: 'Алиса',
email: 'alice@test.com',
});
const user = await userService.findByEmail('alice@test.com');
expect(user.name).toBe('Алиса');
expect(user.email).toBe('alice@test.com');
});
test('не позволяет создать дубликат email', async () => {
await userService.create({ name: 'A', email: 'dup@test.com' });
await expect(
userService.create({ name: 'B', email: 'dup@test.com' })
).rejects.toThrow('уже существует');
});
});
Характеристики:
- Медленнее unit-тестов
- Ловят ошибки на стыках модулей
- Могут использовать реальные БД (в памяти), файловую систему
E2E-тесты (End-to-End)
Проверяют полный пользовательский сценарий через реальный браузер.
// login.e2e.test.js (Playwright)
const { test, expect } = require('@playwright/test');
test('пользователь логинится и видит дашборд', async ({ page }) => {
// Пользователь заходит на страницу логина
await page.goto('http://localhost:3000/login');
// Вводит данные
await page.fill('[name="email"]', 'alice@test.com');
await page.fill('[name="password"]', 'secret123');
await page.click('button[type="submit"]');
// Проверяет, что попал на дашборд
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toHaveText('Добро пожаловать, Алиса');
});
Характеристики:
- Самые медленные и дорогие
- Тестируют реальный UX
- Хрупкие — ломаются при смене UI
- Мало, но покрывают критические сценарии
Smoke-тесты
Быстрая проверка — работает ли система вообще. Запускаются после деплоя.
// smoke.test.js
describe('Smoke Tests', () => {
test('главная страница отвечает 200', async () => {
const res = await fetch('https://myapp.com');
expect(res.status).toBe(200);
});
test('API health-check', async () => {
const res = await fetch('https://myapp.com/api/health');
const data = await res.json();
expect(data.status).toBe('ok');
});
test('БД доступна', async () => {
const res = await fetch('https://myapp.com/api/health/db');
expect(res.ok).toBe(true);
});
});
Регрессионные тесты
Проверяют, что исправленный баг не вернулся. Каждый баг = новый тест.
// regression.test.js
describe('Регрессия', () => {
// BUG-123: formatDate возвращал "NaN" для однозначных месяцев
test('BUG-123: formatDate корректно работает с январём', () => {
const date = new Date('2025-01-05');
expect(formatDate(date)).toBe('05.01.2025');
});
// BUG-456: корзина теряла товары при пустом количестве
test('BUG-456: addToCart с quantity=0 не добавляет товар', () => {
const cart = new Cart();
cart.addItem({ id: 1, quantity: 0 });
expect(cart.items).toHaveLength(0);
});
});
Acceptance-тесты (Приёмочные)
Проверяют бизнес-требования. Написаны на языке, понятном заказчику.
// Стиль BDD (Behaviour-Driven Development)
describe('Оформление заказа', () => {
describe('Когда в корзине товары на сумму > 5000₽', () => {
test('должен применить бесплатную доставку', () => {
const cart = createCart([
{ name: 'Ноутбук', price: 50000 },
]);
const order = checkout(cart);
expect(order.deliveryPrice).toBe(0);
expect(order.deliveryLabel).toBe('Бесплатная доставка');
});
});
describe('Когда корзина пуста', () => {
test('должен отклонить оформление', () => {
const cart = createCart();
expect( => checkout(cart)).toThrow('Корзина пуста');
});
});
});
Snapshot-тесты
Сохраняют «снимок» вывода и сравнивают с предыдущим. Ловят неожиданные изменения.
// UserCard.test.js
const { render } = require('@testing-library/react');
const UserCard = require('./UserCard');
test('UserCard рендерится корректно', () => {
const { container } = render(
<UserCard name="Алиса" role="admin" />
);
expect(container).toMatchSnapshot;
});
// Первый запуск: создаёт файл __snapshots__/UserCard.test.js.snap
// Последующие: сравнивает с сохранённым
// Обновить: jest --updateSnapshot
Когда полезны:
- Компоненты UI
- Сложные объекты конфигурации
- Сериализованные данные
Проблема: легко «автоматически» обновлять снапшоты не глядя.
Визуальные тесты
Сравнивают скриншоты пиксель в пиксель. Ловят визуальные регрессии.
// visual.test.js (Playwright)
test('кнопка выглядит правильно', async ({ page }) => {
await page.goto('/components/button');
await expect(page.locator('.btn-primary')).toHaveScreenshot('button.png');
});
// Первый запуск: сохраняет эталонный скриншот
// Последующие: сравнивает пиксель в пиксель
// Допуск: toHaveScreenshot({ maxDiffPixels: 100 })
Сравнительная таблица
| Вид теста | Скорость | Стоимость | Что ловит | Инструменты |
|---|---|---|---|---|
| Unit | ms | Низкая | Баги в логике | Jest, Vitest |
| Integration | секунды | Средняя | Ошибки на стыках | Jest, Supertest |
| E2E | десятки секунд | Высокая | UX-проблемы | Playwright, Cypress |
| Smoke | секунды | Низкая | Система не работает | curl, Jest |
| Regression | ms-секунды | Низкая | Возврат багов | Любой фреймворк |
| Snapshot | ms | Низкая | Неожиданные изменения | Jest |
| Visual | секунды | Средняя | UI-регрессии | Playwright, Chromatic |
Частые ошибки
1. Только unit-тесты
100% coverage unit-тестов не гарантирует работу системы. Модули могут работать по отдельности, но ломаться вместе.
2. Только E2E-тесты
Медленные, хрупкие, дорогие. Если что-то сломалось — непонятно, где именно.
3. Неправильный уровень теста
// ПЛОХО: E2E-тест для проверки валидации email
// (лучше unit-тест для функции validateEmail)
test('показывает ошибку при невалидном email', async ({ page }) => {
await page.goto('/register');
await page.fill('#email', 'not-an-email');
await page.click('#submit');
await expect(page.locator('.error')).toBeVisible;
});
// ХОРОШО: unit-тест
test('отклоняет невалидный email', () => {
expect(validateEmail('not-an-email')).toBe(false);
});
Практика
- Для функции
calculateShipping(weight, distance)напиши unit-тесты с граничными значениями - Напиши интеграционный тест для связки «валидация + сохранение в БД»
- Составь список из 3 smoke-тестов для своего (или воображаемого) проекта
- Напиши snapshot-тест для простого React-компонента
- Определи, какой вид теста подойдёт для каждого кейса: проверка формулы расчёта налога, авторизация пользователя, стили кнопки