Введение: Зачем тестировать TypeScript-код?
TypeScript — это мощный инструмент, который добавляет статическую типизацию в JavaScript, позволяя отлавливать ошибки на этапе компиляции. Однако даже самая строгая система типов не гарантирует, что ваш код будет работать правильно в реальных условиях. Логические ошибки, неверная обработка граничных случаев и проблемы интеграции модулей остаются за рамками компилятора. Именно здесь на помощь приходит тестирование.
Тестирование в TypeScript — это не просто проверка функций. Это комплексный процесс, который включает три ключевых уровня: Unit (модульное), Integration (интеграционное) и E2E (сквозное). Каждый из них решает свою задачу и вместе они формируют "пирамиду тестирования", обеспечивающую надежность приложения.
В этой статье мы разберем, как писать тесты для TypeScript на всех трех уровнях, используя современные инструменты: Jest, Supertest и Playwright. Вы узнаете, какие ошибки помогают предотвратить эти тесты и как внедрить их в ваш проект.
Unit-тестирование: Проверка изолированных модулей
Unit-тесты (модульные тесты) — это фундамент пирамиды. Они тестируют отдельные функции, классы или методы в изоляции от внешних зависимостей (базы данных, API, файловой системы). Главная цель — убедиться, что каждый "кирпичик" вашего приложения работает корректно.
Инструменты и настройка
Самый популярный фреймворк для unit-тестирования в TypeScript — Jest. Он предоставляет встроенную поддержку TypeScript через ts-jest или @swc/jest.
Установка Jest в проект:
npm install --save-dev jest @types/jest ts-jest
npx ts-jest config:init
Пример unit-теста
Рассмотрим простую функцию для расчета скидки:
// discount.ts
export function applyDiscount(price: number, discountPercent: number): number {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Процент скидки должен быть от 0 до 100');
}
return price - (price * discountPercent) / 100;
}
Напишем unit-тесты, покрывающие основные сценарии:
// discount.test.ts
import { applyDiscount } from './discount';
describe('applyDiscount', () => {
it('должен корректно применять скидку 20%', () => {
expect(applyDiscount(100, 20)).toBe(80);
});
it('должен возвращать исходную цену при скидке 0%', () => {
expect(applyDiscount(50, 0)).toBe(50);
});
it('должен возвращать 0 при скидке 100%', () => {
expect(applyDiscount(200, 100)).toBe(0);
});
it('должен выбрасывать ошибку при отрицательной скидке', () => {
expect(() => applyDiscount(100, -10)).toThrow('Процент скидки должен быть от 0 до 100');
});
it('должен выбрасывать ошибку при скидке больше 100%', () => {
expect(() => applyDiscount(100, 150)).toThrow('Процент скидки должен быть от 0 до 100');
});
});
Важно: Unit-тесты не должны обращаться к внешним ресурсам. Вместо этого используйте моки (mocks) для подмены зависимостей, например, с помощью jest.mock().
Интеграционное тестирование: Взаимодействие между модулями
Интеграционные тесты проверяют, как разные части системы работают вместе. Это может быть взаимодействие между сервисом и базой данных, контроллером и репозиторием, или между микросервисами. В отличие от unit-тестов, здесь мы не изолируем компоненты полностью.
Пример: тестирование REST API
Предположим, у нас есть Express-приложение с одним эндпоинтом. Для тестирования HTTP-запросов используем Supertest.
// userController.ts
import { Router, Request, Response } from 'express';
interface User {
id: number;
name: string;
}
const users: User[] = [{ id: 1, name: 'Alice' }];
const router = Router();
router.get('/users', (req: Request, res: Response) => {
res.json(users);
});
router.post('/users', (req: Request, res: Response) => {
const newUser: User = { id: users.length + 1, name: req.body.name };
users.push(newUser);
res.status(201).json(newUser);
});
export default router;
Напишем интеграционный тест, который проверяет полный сценарий "создание + получение":
// userController.test.ts
import request from 'supertest';
import express from 'express';
import userRouter from './userController';
const app = express();
app.use(express.json());
app.use(userRouter);
describe('User API', () => {
it('GET /users — должен возвращать список пользователей', async () => {
const res = await request(app).get('/users');
expect(res.status).toBe(200);
expect(res.body).toEqual([{ id: 1, name: 'Alice' }]);
});
it('POST /users — должен создавать нового пользователя', async () => {
const newUser = { name: 'Bob' };
const res = await request(app).post('/users').send(newUser);
expect(res.status).toBe(201);
expect(res.body).toMatchObject({ name: 'Bob' });
expect(res.body.id).toBeDefined();
});
});
Совет: Для тестов, работающих с базой данных, используйте in-memory базу (например, SQLite) или контейнеры Docker (через библиотеку testcontainers), чтобы не портить реальные данные.
E2E-тестирование: Сквозная проверка пользовательского сценария
End-to-End (E2E) тесты — это вершина пирамиды. Они симулируют действия реального пользователя: открытие браузера, клики, ввод текста, переходы между страницами. E2E-тесты проверяют, что вся система целиком (фронтенд, бэкенд, база данных) работает корректно.
Инструменты: Playwright
Один из лучших инструментов для E2E — Playwright от Microsoft. Он поддерживает TypeScript "из коробки" и умеет работать с Chrome, Firefox и Safari.
Установка:
npm install --save-dev @playwright/test
npx playwright install