Виды тестов

Разные виды тестов проверяют разные аспекты системы — от отдельных функций до полного пользовательского сценария.

Зачем нужно

Понимание видов тестов помогает выбрать правильный инструмент для задачи. 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);
});

Практика

  1. Для функции calculateShipping(weight, distance) напиши unit-тесты с граничными значениями
  2. Напиши интеграционный тест для связки «валидация + сохранение в БД»
  3. Составь список из 3 smoke-тестов для своего (или воображаемого) проекта
  4. Напиши snapshot-тест для простого React-компонента
  5. Определи, какой вид теста подойдёт для каждого кейса: проверка формулы расчёта налога, авторизация пользователя, стили кнопки

Связанные темы

Ресурсы