Основы тестирования

Тестирование — процесс проверки того, что код работает так, как задумано, и продолжает работать после изменений.

Зачем нужно

Без тестов каждое изменение в коде — лотерея: заработает или сломается? Тесты дают уверенность при рефакторинге, документируют поведение системы и ловят баги до того, как их найдут пользователи. Команды с тестами выпускают фичи быстрее, потому что не боятся менять код.

Где используется

В любом серьёзном проекте: от npm-пакетов до крупных SPA. CI/CD-пайплайны запускают тесты автоматически перед деплоем. Open-source библиотеки без тестов не вызывают доверия.

Предпосылки

Введение в JavaScript, базовое понимание функций и модулей

Зачем писать тесты

Без тестов

Написал код → Открыл браузер → Покликал → Вроде работает → Задеплоил → Пользователь нашёл баг

С тестами

Написал тест → Написал код → Тест зелёный → Рефакторинг → Тесты всё ещё зелёные → Уверенный деплой

Пирамида тестирования

         /\
        /  \          E2E тесты
       / E2E\         (мало, дорогие, медленные)
      /------\
     /Интегр. \       Интеграционные тесты
    /----------\      (средне)
   /   Unit     \     Юнит-тесты
  /--------------\    (много, дешёвые, быстрые)
Уровень Количество Скорость Стоимость Что проверяет
Unit Много (70%) Быстро Низкая Отдельные функции/классы
Integration Средне (20%) Средне Средняя Взаимодействие модулей
E2E Мало (10%) Медленно Высокая Полный пользовательский сценарий

Анатомия теста

Каждый тест состоит из трёх частей — AAA-паттерн:

// test: сложение двух чисел
test('складывает 1 + 2 и возвращает 3', () => {
  // Arrange (Подготовка) — настраиваем данные
  const a = 1;
  const b = 2;

  // Act (Действие) — вызываем тестируемый код
  const result = add(a, b);

  // Assert (Проверка) — проверяем результат
  expect(result).toBe(3);
});

Arrange — Подготовка

Создаём данные, моки, конфигурации, всё что нужно для запуска.

Act — Действие

Вызываем тестируемую функцию или метод. Обычно одна строка.

Assert — Проверка

Сравниваем фактический результат с ожидаемым.

Что делает тест хорошим

// ПЛОХО: тест ничего не говорит
test('работает', () => {
  expect(processData(data)).toBeTruthy();
});

// ХОРОШО: тест документирует поведение
test('processData возвращает отсортированный массив уникальных значений', () => {
  const input = [3, 1, 2, 1, 3];
  const result = processData(input);
  expect(result).toEqual([1, 2, 3]);
});

Принципы хороших тестов — FIRST

Буква Принцип Значение
F Fast Быстрые — миллисекунды, не секунды
I Isolated Изолированные — не зависят друг от друга
R Repeatable Повторяемые — одинаковый результат при каждом запуске
S Self-validating Самопроверяемые — pass/fail без ручной проверки
T Timely Своевременные — пишутся рядом с кодом

Структура тестового файла

// math.test.js
const { add, subtract, multiply } = require('./math');

describe('Math модуль', () => {
  describe('add', () => {
    test('складывает два положительных числа', () => {
      expect(add(2, 3)).toBe(5);
    });

    test('складывает отрицательные числа', () => {
      expect(add(-1, -2)).toBe(-3);
    });

    test('складывает ноль', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('subtract', () => {
    test('вычитает числа', () => {
      expect(subtract(5, 3)).toBe(2);
    });
  });
});

describe — группировка тестов

Объединяет связанные тесты. Можно вкладывать.

test (или it) — отдельный тест-кейс

Описывает одно конкретное поведение.

expect — утверждение

Проверяет, что результат соответствует ожиданию.

Когда писать тесты

Ситуация Нужны тесты?
Бизнес-логика (расчёты, валидация) Обязательно
Утилитарные функции Обязательно
API-эндпоинты Да
Компоненты UI Да (ключевые)
Прототип/хакатон Не обязательно
Простые геттеры/сеттеры Не стоит
Приватные методы Через публичный API

Частые ошибки

1. Тестирование реализации, а не поведения

// ПЛОХО: тестируем КАК работает
test('использует Array.filter внутри', () => {
  const spy = jest.spyOn(Array.prototype, 'filter');
  getActiveUsers(users);
  expect(spy).toHaveBeenCalled(); // Хрупкий тест!
});

// ХОРОШО: тестируем ЧТО возвращает
test('возвращает только активных пользователей', () => {
  const users = [
    { name: 'Алиса', active: true },
    { name: 'Боб', active: false },
  ];
  expect(getActiveUsers(users)).toEqual([{ name: 'Алиса', active: true }]);
});

2. Тесты зависят друг от друга

// ПЛОХО: второй тест зависит от первого
let counter = 0;
test('инкремент', () => { counter++; expect(counter).toBe(1); });
test('ещё инкремент', () => { counter++; expect(counter).toBe(2); });

// ХОРОШО: каждый тест независим
test('инкремент с нуля', () => {
  let counter = 0;
  counter++;
  expect(counter).toBe(1);
});

3. Слишком много проверок в одном тесте

// ПЛОХО: один тест проверяет всё
test('пользователь', () => {
  expect(user.name).toBe('Алиса');
  expect(user.age).toBe(25);
  expect(user.isActive).toBe(true);
  expect(user.posts.length).toBe(3);
  // Если первый fail — остальные не проверяются
});

// ХОРОШО: отдельные аспекты
test('имя пользователя корректно', () => { /* ... */ });
test('пользователь активен по умолчанию', () => { /* ... */ });

Практика

  1. Напиши функцию isPalindrome(str) и 5 тестов для неё (пустая строка, одна буква, палиндром, не палиндром, с пробелами)
  2. Напиши тесты для функции calculateDiscount(price, percent) с граничными значениями (0%, 100%, отрицательная цена)
  3. Создай тестовый файл с describe-блоками для модуля калькулятора
  4. Намеренно напиши хрупкий тест и перепиши его правильно

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

Ресурсы