Почему тестирование TypeScript — это не страшно, а выгодно
Вы написали классную функцию на TypeScript, запустили проект — всё работает. Через неделю добавили новую фичу, а старая логика сломалась. Знакомо? Если да, то вам срочно нужно подружиться с тестированием. И не надо бояться: тесты — это не скучная обязаловка, а ваш личный «детектор багов», который работает 24/7.
В этой статье мы разберём, с чего начать тестирование TypeScript новичку. Вы узнаете, как настроить окружение, написать первый unit-тест и не запутаться в типах. Никакой воды — только практика и реальные примеры кода.
Зачем вообще тестировать TypeScript, если он уже безопасный?
TypeScript действительно ловит много ошибок на этапе компиляции: неправильные типы, отсутствующие поля, неверные аргументы. Но статическая типизация не спасает от логических ошибок. Ваша функция может идеально проходить компиляцию, но выдавать неверный результат. Например:
function sum(a: number, b: number): number {
return a - b; // Ошибка! Вместо сложения — вычитание
}
TypeScript не ругнётся — типы соблюдены. А вот юнит-тест сразу покажет, что sum(2, 2) не равно 4. Поэтому тестирование TypeScript — это не прихоть, а необходимость для любого серьёзного проекта.
Шаг 1. Выбираем инструмент: почему Jest — лучший друг новичка
На рынке есть несколько популярных фреймворков для тестирования: Mocha, Jasmine, Vitest, Jest. Для начинающих я настоятельно рекомендую Jest. Почему?
- Работает «из коробки» — не нужно долго настраивать.
- Встроенный mock-механизм и покрытие кода.
- Отличная поддержка TypeScript через
ts-jest. - Понятные сообщения об ошибках.
Давайте установим Jest в ваш проект. Предполагаю, что TypeScript у вас уже есть. Если нет — создайте новый проект:
mkdir my-ts-project
cd my-ts-project
npm init -y
npm install typescript jest @types/jest ts-jest --save-dev
Теперь создадим файл jest.config.js в корне проекта:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
};
И добавим скрипт в package.json:
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}
Готово! Теперь можно писать тесты.
Шаг 2. Пишем первый тест на TypeScript
Создадим простую функцию в файле src/math.ts:
export function add(a: number, b: number): number {
return a + b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Деление на ноль запрещено');
}
return a / b;
}
Теперь напишем для неё тест. Создайте папку src/__tests__/ и файл math.test.ts:
import { add, divide } from '../math';
describe('Math functions', () => {
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('divides two numbers', () => {
expect(divide(10, 2)).toBe(5);
expect(divide(9, 3)).toBe(3);
});
test('throws error when dividing by zero', () => {
expect(() => divide(5, 0)).toThrow('Деление на ноль запрещено');
});
});
Запускаем npm test и видим зелёный отчёт. Поздравляю — вы только что написали свои первые unit-тесты на TypeScript!
Что здесь произошло?
describe— объединяет тесты в логическую группу.testилиit— отдельный тест-кейс.expect— утверждение, которое проверяет результат.toBe,toThrow— «матчеры» (сравниватели).
Шаг 3. Тестируем асинхронный код и промисы
Современный TypeScript-код часто работает с async/await. Jest это отлично поддерживает. Допустим, у нас есть функция, которая имитирует запрос к API:
// src/api.ts
export async function fetchUser(id: number): Promise<{ name: string; age: number }> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'Alice', age: 30 });
}, 100);
});
}
Тест для неё:
// src/__tests__/api.test.ts
import { fetchUser } from '../api';
describe('API calls', () => {
test('returns user by id', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ name: 'Alice', age: 30 });
expect(user.name).toBe('Alice');
});
test('user age is a number', async () => {
const user = await fetchUser(2);
expect(typeof user.age).toBe('number');
});
});
Обратите внимание: мы используем toEqual для глубокого сравнения объектов, а не toBe. Это важно — toBe сравнивает ссылки, а toEqual — содержимое.
Шаг 4. Мокаем зависимости (чтобы тесты были быстрыми)
Когда вы тестируете модуль, который обращается к базе данных или внешнему API, не нужно делать реальные запросы. Вместо этого мы «мокаем» (подменяем) зависимость. Jest предоставляет для этого мощный инструмент — jest.mock.
Создадим сервис, который использует наш «API»:
// src/userService.ts
import { fetchUser } from './api';
export async function getUserSummary(id: number): Promise {
const user = await fetchUser(id);
return `${user.name} is ${user.age} years old`;
}
Теперь замокаем fetchUser в тесте:
// src/__tests__/userService.test.ts
import { getUserSummary } from '../userService';
import { fetchUser } from '../api';
jest.mock('../api'); // Jest автоматически замокает весь модуль
const mockedFetchUser = fetchUser as jest.Mock;
describe('UserService', () => {
beforeEach(() => {
mockedFetchUser.mockClear();
});
test('returns formatted summary', async () => {
mockedFetchUser.mockResolvedValue({ name: 'Bob', age: 25 });
const summary = await getUserSummary(1);
expect(summary).toBe('Bob is 25 years old');
expect(mockedFetchUser).toHaveBeenCalledTimes(1);
expect