📘 TypeScript

اختبار الكود في TypeScript: دليل شامل

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — шеф-повар в ресторане. Ты создал новое блюдо — «Пасту с морепродуктами». Прежде чем подать её гостю, ты обязательно пробуешь её: проверяешь, достаточно ли соли, не переварены ли макароны, свежие ли креветки. Если ты этого не сделаешь, гость может получить пересоленную или сырую еду, и ресторан потеряет репутацию.

Тестирование кода — это точно такой же процесс «пробования», только для программ. Ты пишешь код, а потом запускаешь специальные проверки, которые автоматически говорят: «Всё работает отлично!» или «Вот здесь ошибка, исправь!».

Зачем это нужно? Давай перечислим главные причины:

  • Экономия времени. Ручная проверка каждой кнопки в приложении после каждого изменения занимает часы. Автотесты делают это за секунды.
  • Уверенность. Когда ты меняешь старый код, ты можешь случайно сломать что-то, что работало раньше. Тесты сразу покажут: «Эй, ты сломал функцию входа!».
  • Документация. Тесты — это живая инструкция к твоему коду. Другой программист (или ты сам через месяц) смотрит тесты и понимает, как должна работать каждая функция.
  • Профессионализм. В любой серьёзной компании (Google, Яндекс, Тинькофф) код без тестов не принимают. Это признак мастерства.
💡 Важно: Тестирование не гарантирует, что в коде нет ошибок. Оно гарантирует, что известные тебе ошибки не появятся снова. Думай об этом как о страховке: ты платишь немного времени сейчас, чтобы не потерять много времени потом.

Теория: как это работает изнутри

Давай заглянем под капот. В TypeScript (и JavaScript) тестирование строится на трёх китах: описание (describe), проверка (it или test) и утверждение (expect).

Представь, что ты пишешь инструкцию для робота-помощника:

  1. Describe (Опиши группу) — «Робот, сейчас мы будем проверять работу микроволновки».
  2. It/Test (Проверь конкретный случай) — «Проверь, что микроволновка греет еду за 30 секунд».
  3. Expect (Утверди результат) — «Я ожидаю, что температура еды станет 70 градусов. Если это не так — кричи об ошибке».

Самый популярный инструмент для этого в мире TypeScript — Jest. Он создан компанией Facebook (Meta) и считается стандартом индустрии. Jest сам находит тесты, запускает их и выдаёт красивый отчёт.

Как это выглядит технически? Jest подключается к твоему проекту и следит за файлами, которые заканчиваются на .test.ts или .spec.ts. Когда ты запускаешь команду npm test, Jest:

  1. Находит все тестовые файлы.
  2. Запускает твой код в изолированной среде (песочнице).
  3. Сравнивает реальные результаты с ожидаемыми.
  4. Показывает зелёные галочки (успех) или красные крестики (провал).
🔬 Интересный факт: Jest использует фишку под названием «mock» (заглушка). Если твоя функция отправляет письма или работает с базой данных, Jest может «подменить» эти сложные операции, чтобы тест работал быстро и не зависел от интернета.

Синтаксис в TypeScript: все формы и варианты

Давай разберём все основные конструкции, которые ты будешь использовать в тестах. Мы будем использовать синтаксис Jest, так как это стандарт.

1. Базовая структура теста

// Самый простой тест
test('название теста', () => {
  // здесь идёт проверка
  expect(2 + 2).toBe(4);
});

// Альтернативный синтаксис (более читаемый)
it('должно складывать два числа', () => {
  expect(2 + 2).toBe(4);
});

2. Группировка тестов

// Группируем связанные тесты
describe('Калькулятор', () => {
  
  test('сложение', () => {
    expect(1 + 1).toBe(2);
  });

  test('вычитание', () => {
    expect(5 - 3).toBe(2);
  });

  // Вложенные группы тоже можно
  describe('Умножение', () => {
    test('положительные числа', () => {
      expect(2 * 3).toBe(6);
    });
  });
});

3. Основные методы expect (утверждения)

Метод Что проверяет Пример
toBe(value) Строгое равенство (===) expect(1).toBe(1)
toEqual(value) Глубокое равенство (для объектов и массивов) expect({a:1}).toEqual({a:1})
toBeTruthy() Значение истинно (не null, undefined, 0, false) expect(1).toBeTruthy()
toBeFalsy() Значение ложно expect(0).toBeFalsy()
toContain(item) Массив или строка содержит элемент expect([1,2,3]).toContain(2)
toThrow() Функция выбрасывает ошибку expect(() => { throw new Error() }).toThrow()
not Отрицание любого утверждения expect(1).not.toBe(2)

4. Подготовка и очистка (хуки)

describe('Работа с базой данных', () => {
  // Выполняется перед каждым тестом
  beforeEach(() => {
    // Создаём тестовые данные
    console.log('Готовлю данные для теста...');
  });

  // Выполняется после каждого теста
  afterEach(() => {
    // Удаляем тестовые данные
    console.log('Очищаю после теста...');
  });

  // Выполняется один раз перед всеми тестами
  beforeAll(() => {
    console.log('Подключаюсь к тестовой базе...');
  });

  // Выполняется один раз после всех тестов
  afterAll(() => {
    console.log('Отключаюсь от базы...');
  });

  test('сохранение пользователя', () => {
    // тест использует данные из beforeEach
  });
});

Примеры кода — от простого к сложному

Теперь перейдём к практике. Каждый пример — это реальный сценарий, который ты встретишь в работе.

Пример 1: Проверка простой функции

// Функция, которую будем тестировать
function sum(a: number, b: number): number {
  return a + b;
}

// Тест
test('сумма 2 и 3 должна быть 5', () => {
  // Вызываем функцию с тестовыми данными
  const result = sum(2, 3);
  
  // Проверяем, что результат совпадает с ожидаемым
  expect(result).toBe(5);
});

Пример 2: Проверка работы со строками

// Функция приветствия
function greet(name: string): string {
  return `Привет, ${name}!`;
}

describe('Функция greet', () => {
  test('должна приветствовать пользователя по имени', () => {
    expect(greet('Анна')).toBe('Привет, Анна!');
  });

  test('должна работать с пустым именем', () => {
    expect(greet('')).toBe('Привет, !');
  });

  // Используем not для проверки отрицания
  test('не должна возвращать пустую строку', () => {
    expect(greet('Максим')).not.toBe('');
  });
});

Пример 3: Проверка объектов (toEqual)

// Функция создания пользователя
function createUser(name: string, age: number) {
  return {
    id: Date.now(), // уникальный ID
    name: name,
    age: age,
    isActive: true
  };
}

describe('createUser', () => {
  test('должна создавать пользователя с правильными полями', () => {
    const user = createUser('Иван', 25);
    
    // toBe не сработает для объектов, используем toEqual
    expect(user).toEqual({
      id: expect.any(Number), // ID может быть любым числом
      name: 'Иван',
      age: 25,
      isActive: true
    });
  });

  test('должна создавать активного пользователя', () => {
    const user = createUser('Мария', 30);
    expect(user.isActive).toBeTruthy();
  });
});

Пример 4: Проверка массивов

// Функция фильтрации чисел
function getEvenNumbers(numbers: number[]): number[] {
  return numbers.filter(num => num % 2 === 0);
}

describe('getEvenNumbers', () => {
  test('должна возвращать только чётные числа', () => {
    const result = getEvenNumbers([1, 2, 3, 4, 5, 6]);
    expect(result).toEqual([2, 4, 6]);
  });

  test('должна содержать число 4', () => {
    const result = getEvenNumbers([1, 2, 3, 4, 5]);
    expect(result).toContain(4);
  });

  test('не должна содержать нечётные числа', () => {
    const result = getEvenNumbers([1, 2, 3, 4]);
    expect(result).not.toContain(3);
  });

  test('пустой массив должен вернуть пустой массив', () => {
    expect(getEvenNumbers([])).toEqual([]);
  });
});

Пример 5: Проверка ошибок (toThrow)

// Функция деления с проверкой
function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Деление на ноль запрещено!');
  }
  return a / b;
}

describe('divide', () => {
  test('должна делить числа', () => {
    expect(divide(10, 2)).toBe(5);
  });

  test('должна выбрасывать ошибку при делении на ноль', () => {
    // Важно: оборачиваем вызов в функцию, иначе ошибка упадёт до expect
    expect(() => {
      divide(10, 0);
    }).toThrow('Деление на ноль запрещено!');
  });

  test('должна выбрасывать любую ошибку', () => {
    expect(() => divide(10, 0)).toThrow();
  });
});

Пример 6: Асинхронные тесты (Promise)

// Асинхронная функция получения данных
async function fetchUserData(userId: number): Promise<{name: string, id: number}> {
  // Имитация запроса к серверу
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: 'Пользователь ' + userId, id: userId });
    }, 100);
  });
}

describe('fetchUserData', () => {
  // Способ 1: async/await (рекомендуется)
  test('должна возвращать данные пользователя (async/await)', async () => {
    const data = await fetchUserData(1);
    expect(data).toEqual({ name: 'Пользователь 1', id: 1 });
  });

  // Способ 2: return Promise
  test('должна возвращать данные пользователя (return)', () => {
    return fetchUserData(2).then(data => {
      expect(data.name).toBe('Пользователь 2');
    });
  });

  // Способ 3: resolves
  test('должна успешно завершаться (resolves)', async () => {
    await expect(fetchUserData(3)).resolves.toEqual({
      name: 'Пользователь 3',
      id: 3
    });
  });
});

Пример 7: Mock (заглушки) для сложных зависимостей

// Представь, что у нас есть сервис отправки email
const emailService = {
  send: (to: string, message: string): boolean => {
    // В реальности тут сложный код с SMTP
    console.log(`Отправляю письмо на ${to}: ${message}`);
    return true;
  }
};

// Функция, которая использует emailService
function notifyUser(email: string): string {
  const result = emailService.send(email, 'Добро пожаловать!');
  if (result) {
    return 'Уведомление отправлено';
  }
  return 'Ошибка отправки';
}

describe('notifyUser с моками', () => {
  test('должна вызывать emailService.send', () => {
    // Создаём заглушку для emailService.send
    const mockSend = jest.fn();
    // Подменяем реальную функцию на заглушку
    emailService.send = mockSend;
    
    // Вызываем тестируемую функцию
    notifyUser('test@test.com');
    
    // Проверяем, что send был вызван с правильными аргументами
    expect(mockSend).toHaveBeenCalledWith(
      'test@test.com',
      'Добро пожаловать!'
    );
    
    // Проверяем, что send был вызван ровно 1 раз
    expect(mockSend).toHaveBeenCalledTimes(1);
  });
});

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Давай сравним два подхода.

Характеристика TypeScript (Jest) Python (pytest)
Установка npm install --save-dev jest pip install pytest
Запуск npm test или npx jest pytest
Синтаксис теста test('описание', () => { ... }) def test_описание(): ...
Утверждение expect(x).toBe(y) assert x == y
Группировка describe('группа', () => { ... }) Классы или вложенные функции
Проверка ошибок expect(fn).toThrow() with pytest.raises(Error): fn()
Фикстуры (подготовка) beforeEach, beforeAll @pytest.fixture
Типизация Встроенная (TypeScript) Опционально (mypy)

Главное отличие: В Python тесты — это просто функции с особыми именами. В TypeScript тесты — это вызовы функций с колбэками. Python использует assert (встроенная проверка), а TypeScript — цепочку expect().toBe(), которая читается как английское предложение: «Я ожидаю, что это будет равно тому».

🎯 Совет: Если ты знаешь Python, представь, что expect(x).toBe(y) — это то же самое, что assert x == y, только более наглядно и с кучей дополнительных возможностей (проверка типов, глубокое сравнение и т.д.).

Типичные ошибки и подводные камни

Даже опытные разработчики иногда допускают эти ошибки. Давай разберём их, чтобы ты не наступал на те же грабли.

Ошибка 1: Забыл обернуть вызов в функцию при проверке ошибок

// ❌ НЕПРАВИЛЬНО: Ошибка упадёт до того, как сработает expect
test('проверка ошибки', () => {
  expect(divide(10, 0)).toThrow(); // Ошибка здесь!
});

// ✅ ПРАВИЛЬНО: Оборачиваем в стрелочную функцию
test('проверка ошибки', () => {
  expect(() => divide(10, 0)).toThrow();
});

Ошибка 2: Использование toBe для объектов

// ❌ НЕПРАВИЛЬНО: toBe использует ===, объекты разные
test('сравнение объектов', () => {
  expect({a: 1}).toBe({a: 1}); // Провалится!
});

// ✅ ПРАВИЛЬНО: toEqual делает глубокое сравнение
test('сравнение объектов', () => {
  expect({a: 1}).toEqual({a: 1}); // Пройдёт
});

Ошибка 3: Забыл про асинхронность

// ❌ НЕПРАВИЛЬНО: Тест завершится до выполнения Promise
test('асинхронный тест', () => {
  fetchUserData(1).then(data => {
    expect(data.name).toBe('Пользователь 1');
  });
  // Тест закончится, Promise не выполнится
});

// ✅ ПРАВИЛЬНО: Возвращаем Promise или используем async/await
test('асинхронный тест', async () => {
  const data = await fetchUserData(1);
  expect(data.name).toBe('Пользователь 1');
});

Ошибка 4: Тест зависит от другого теста

// ❌ НЕПРАВИЛЬНО: Тесты не должны зависеть друг от друга
let counter = 0;

test('первый тест', () => {
  counter = 1;
  expect(counter).toBe(1);
});

test('второй тест', () => {
  // Этот тест может провалиться, если первый не выполнился
  expect(counter).toBe(1); // Зависимость!
});

// ✅ ПРАВИЛЬНО: Каждый тест готовит свои данные
test('первый тест', () => {
  const localCounter = 1;
  expect(localCounter).toBe(1);
});

test('второй тест', () => {
  const localCounter = 1;
  expect(localCounter).toBe(1);
});

Ошибка 5: Слишком много проверок в одном тесте

// ❌ НЕПРАВИЛЬНО: Если первая проверка упадёт, мы не узнаем о второй
test('всё сразу', () => {
  expect(sum(1, 1)).toBe(2);
  expect(sum(2, 2)).toBe(4);
  expect(sum(3, 3)).toBe(6);
  // Если первая упадёт, остальные не выполнятся
});

// ✅ ПРАВИЛЬНО: Разделяем на отдельные тесты
test('сумма 1+1', () => expect(sum(1, 1)).toBe(2));
test('сумма 2+2', () => expect(sum(2, 2)).toBe(4));
test('сумма 3+3', () => expect(sum(3, 3)).toBe(6));

Лучшие практики и советы профессионалов

Вот правила, которые используют разработчики в топовых IT-компаниях. Запомни их — они сэкономят тебе сотни часов.

1. Правило одной проверки (AAA)

Каждый тест должен состоять из трёх частей:

  • Arrange (Подготовка) — создаём данные, настраиваем окружение.
  • Act (Действие) — вызываем тестируемую функцию.
  • Assert (Проверка) — проверяем результат.
test('правило AAA', () => {
  // Arrange
  const a = 5;
  const b = 3;
  
  // Act
  const result = sum(a, b);
  
  // Assert
  expect(result).toBe(8);
});

2. Названия тестов должны быть понятными

Плохое название: test('test1', ...)
Хорошее название: test('должна возвращать ошибку при отрицательном возрасте', ...)

Представь, что через год ты вернёшься к проекту. Название теста должно сразу говорить, что именно проверяется и какой ожидается результат.

3. Тестируй граничные случаи

Не проверяй только «нормальные» данные. Обязательно проверь:

  • Пустые значения (null, undefined, пустая строка)
  • Крайние значения (0, максимальное число, отрицательные числа)
  • Некорректные типы (если функция ожидает число, передай строку)

4. Не тестируй чужой код

Не нужно писать тесты для стандартных библиотек TypeScript или фреймворков. Разработчики React, Express и других библиотек уже сделали это за тебя. Тестируй только свой код.

5. Используй describe для логической группировки

describe('Калькулятор', () => {
  describe('Сложение', () => {
    test('положительные числа', () => { /* ... */ });
    test('отрицательные числа', () => { /* ... */ });
  });
  
  describe('Деление', () => {
    test('обычное деление', () => { /* ... */ });
    test('деление на ноль', () => { /* ... */ });
  });
});

6. Поддерживай тесты в чистоте

Тесты — это тоже код. Они должны быть:

  • Простыми — без сложной логики и циклов.
  • Быстрыми — если тест выполняется дольше 100 мс, его редко запускают.
  • Изолированными — один тест не должен влиять на другой.

Вопросы для самопроверки

Проверь, как ты усвоил материал. Попробуй ответить на вопросы, прежде чем смотреть ответы.

  1. Вопрос: Какая команда запускает тесты Jest в проекте?

    Показать ответ

    npm test или npx jest. Обычно в package.json прописана команда "test": "jest".

  2. Вопрос: Чем отличается toBe от toEqual?

    Показать ответ

    toBe использует строгое сравнение (===) и подходит для примитивов (числа, строки). toEqual делает глубокое сравнение объектов и массивов.

  3. Вопрос: Как проверить, что функция выбрасывает ошибку?

    Показать ответ

    Нужно обернуть вызов в стрелочную функцию: expect(() => myFunction()).toThrow().

  4. Вопрос: Что такое mock (заглушка) и зачем он нужен?

    Показать ответ

    Mock — это замена реальной функции на «пустышку», которая записывает, как её вызывали. Нужен, чтобы тестировать код, не завися от внешних сервисов (базы данных, API, отправка писем).

  5. Вопрос: Как сгруппировать несколько связанных тестов?

    Показать ответ

    Использовать describe('Название группы', () => { ... }). Внутри можно вкладывать другие describe и test.

  6. Вопрос: Что такое хуки beforeEach и afterEach?

    Показать ответ

    Это функции, которые выполняются перед каждым тестом (beforeEach) и после каждого теста (afterEach). Используются для подготовки данных и очистки.

  7. Вопрос: Почему тесты должны быть независимыми друг от друга?

    Показать ответ

    Если тесты зависят друг от друга, изменение порядка их запуска или сбой одного теста может сломать другие. Это делает отладку кошмаром.

  8. Вопрос: Как в Python называется аналог expect().toBe()?

    Показать ответ

    assert x == y. В Python используется встроенное ключевое слово assert.

  9. Вопрос: Что такое AAA в тестировании?

    Показать ответ

    Arrange (подготовка), Act (действие), Assert (проверка). Это структура идеального теста.

  10. Вопрос: Как проверить, что массив содержит определённый элемент?

    Показать ответ

    expect([1, 2, 3]).toContain(2). Метод toContain работает и для строк.

Итог и что изучить дальше

Поздравляю! Ты только что прошёл полноценный курс по тестированию кода на TypeScript. Давай подведём итоги:

  • Тестирование — это автоматическая проверка твоего кода, которая экономит время и нервы.
  • Jest — главный инструмент для тестирования в мире TypeScript.
  • Основные конструкции: test, describe, expect, toBe, toEqual, toThrow.
  • Лучшие практики: изолируй тесты, используй AAA, тестируй граничные случаи, пиши понятные названия.

Теперь, когда ты знаешь основы, вот что стоит изучить дальше, чтобы стать настоящим профи:

  1. TDD (Test-Driven Development) — методология, где ты сначала пишешь тест, а потом код. Звучит безумно, но работает отлично.
  2. Интеграционное тестирование — проверка, как разные части системы работают вместе (например, база данных + сервер).
  3. E2E тестирование (End-to-End) — тесты, которые эмулируют действия реального пользователя в браузере. Инструменты: Cypress, Playwright.
  4. Покрытие кода (Code Coverage) — метрика, показывающая, сколько процентов твоего кода покрыто тестами. Jest умеет это считать.
  5. Mock-библиотеки — более глубокое изучение моков: sinon.js, jest.mock для модулей.