Основы тестирования
Тестирование — процесс проверки того, что код работает так, как задумано, и продолжает работать после изменений.
Зачем нужно
Без тестов каждое изменение в коде — лотерея: заработает или сломается? Тесты дают уверенность при рефакторинге, документируют поведение системы и ловят баги до того, как их найдут пользователи. Команды с тестами выпускают фичи быстрее, потому что не боятся менять код.
Где используется
В любом серьёзном проекте: от 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('пользователь активен по умолчанию', () => { /* ... */ });
Практика
- Напиши функцию
isPalindrome(str)и 5 тестов для неё (пустая строка, одна буква, палиндром, не палиндром, с пробелами) - Напиши тесты для функции
calculateDiscount(price, percent)с граничными значениями (0%, 100%, отрицательная цена) - Создай тестовый файл с
describe-блоками для модуля калькулятора - Намеренно напиши хрупкий тест и перепиши его правильно