Введение: Почему тестирование в TypeScript — это must have
TypeScript — мощный инструмент, который добавляет статическую типизацию в JavaScript. Однако даже самая строгая система типов не гарантирует, что ваш код будет работать правильно в рантайме. Баги, логические ошибки, неверная обработка краевых случаев — всё это остаётся за кадром компиляции. Именно здесь на помощь приходит тестирование.
В этой статье мы разберём три основных уровня тестирования в TypeScript: unit (модульное), integration (интеграционное) и e2e (сквозное). Вы узнаете, чем они отличаются, когда какой применять, и увидите реальные примеры кода. После прочтения вы сможете настроить полноценный тестовый пайплайн для вашего TypeScript-проекта.
1. Unit-тестирование: проверяем атомарные единицы кода
Unit-тесты — это самый быстрый и дешёвый уровень проверки. Они изолируют одну функцию, метод или класс и тестируют его в отрыве от внешних зависимостей (баз данных, API, файловой системы).
1.1 Выбор инструмента и настройка
Самый популярный фреймворк для TypeScript — Jest. Он имеет встроенную поддержку TypeScript через ts-jest или @swc/jest.
// Установка
npm install --save-dev jest @types/jest ts-jest
// Инициализация конфигурации
npx ts-jest config:init
После этого в корне появится файл jest.config.js с настройками.
1.2 Пример unit-теста для функции
Допустим, у нас есть простая функция для расчёта скидки:
// discount.ts
export function applyDiscount(price: number, discountPercent: number): number {
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('Некорректный процент скидки');
}
return price - (price * discountPercent) / 100;
}
Напишем для неё unit-тест:
// discount.test.ts
import { applyDiscount } from './discount';
describe('applyDiscount', () => {
it('должна корректно считать скидку 10%', () => {
expect(applyDiscount(1000, 10)).toBe(900);
});
it('должна возвращать исходную цену при скидке 0%', () => {
expect(applyDiscount(500, 0)).toBe(500);
});
it('должна выбрасывать ошибку при скидке больше 100%', () => {
expect(() => applyDiscount(100, 150)).toThrow('Некорректный процент скидки');
});
});
Запускаем: npx jest. Все три теста должны пройти зелёными.
1.3 Моки и стабы
Если функция обращается к внешнему сервису (например, базе данных), мы заменяем его моком:
// userService.test.ts
import { getUser } from './userService';
import { db } from './database';
jest.mock('./database'); // Автоматический mock
test('getUser возвращает пользователя по ID', async () => {
(db.findUser as jest.Mock).mockResolvedValue({ id: 1, name: 'Иван' });
const user = await getUser(1);
expect(user.name).toBe('Иван');
});
2. Integration-тестирование: проверяем взаимодействие модулей
Integration-тесты проверяют, как разные части системы работают вместе: контроллеры, сервисы, репозитории, базы данных. Они медленнее unit-тестов, но дают больше уверенности в корректности связок.
2.1 Тестируем API-эндпоинт с Express
Для тестирования HTTP-запросов используем библиотеку Supertest вместе с Jest.
// Установка
npm install --save-dev supertest @types/supertest
Предположим, у нас есть Express-приложение:
// app.ts
import express from 'express';
import { createUser } from './controllers/userController';
export const app = express();
app.use(express.json());
app.post('/api/users', createUser);
Интеграционный тест:
// app.test.ts
import request from 'supertest';
import { app } from './app';
import { UserRepository } from './repositories/userRepository';
// Мокаем репозиторий
jest.mock('./repositories/userRepository');
describe('POST /api/users', () => {
it('должен создать пользователя и вернуть 201', async () => {
const mockUser = { id: '1', name: 'Анна', email: 'anna@test.com' };
(UserRepository.prototype.create as jest.Mock).mockResolvedValue(mockUser);
const response = await request(app)
.post('/api/users')
.send({ name: 'Анна', email: 'anna@test.com' });
expect(response.status).toBe(201);
expect(response.body).toEqual(mockUser);
});
it('должен вернуть 400 при пустом имени', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@test.com' }); // без name
expect(response.status).toBe(400);
});
});
2.2 Тестирование с реальной базой данных
Иногда полезно протестировать интеграцию с реальной БД (например, SQLite в памяти):
// db.test.ts
import { createConnection, getConnection } from 'typeorm';
beforeAll(async () => {
await createConnection({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
});
});
afterAll(async () => {
await getConnection().close();
});
test('сохранение и чтение пользователя', async () => {
const repo = getConnection().getRepository(User);
const user = repo.create({ name: 'Петр', email: 'petr@test.com' });
await repo.save(user);
const found = await repo.findOneBy({ email: 'petr@test.com' });
expect(found?.name).toBe('Петр');
});
3. E2E-тестирование: проверяем сценарий целиком
E2E (End-to-End) тесты симулируют действия реального пользователя: клики, ввод текста, переходы по страницам. Они запускаются в браузере и проверяют, что всё приложение работает как единое целое.
3.1 Инструмент: Playwright
Playwright от Microsoft — современный фреймворк для e2e-тестирования, поддерживающий TypeScript из коробки.
// Установка
npm install --save-dev @