Тестирование в TypeScript: от unit до e2e — полное руководство
Современная веб-разработка немыслима без качественного тестирования. TypeScript, как надстройка над JavaScript, предоставляет мощные инструменты для типизации, но не решает проблему логических ошибок. В этой статье мы разберём три уровня тестирования: unit (модульное), integration (интеграционное) и e2e (сквозное). Вы узнаете, как настроить окружение, писать эффективные тесты на TypeScript и интегрировать их в CI/CD пайплайн.
1. Unit-тестирование: основа пирамиды тестирования
Unit-тесты проверяют изолированные модули (функции, классы, методы) без внешних зависимостей. Для TypeScript лучшим выбором является Jest — он поддерживает TypeScript из коробки и предоставляет богатый API для моков и ассертов.
Настройка Jest для TypeScript
npm install --save-dev jest @types/jest ts-jest
npx ts-jest config:init
Файл jest.config.ts должен содержать:
export default {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts']
};
Пример unit-теста функции
Допустим, у нас есть функция вычисления скидки:
// src/utils/discount.ts
export function calculateDiscount(price: number, percent: number): number {
if (percent < 0 || percent > 100) throw new Error('Invalid percent');
return price - (price * percent) / 100;
}
Тест для неё:
// src/__tests__/discount.test.ts
import { calculateDiscount } from '../utils/discount';
describe('calculateDiscount', () => {
it('должен вернуть 80 для цены 100 и скидки 20%', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
it('должен выбросить ошибку при некорректном проценте', () => {
expect(() => calculateDiscount(100, 150)).toThrow('Invalid percent');
});
it('должен вернуть 0 при скидке 100%', () => {
expect(calculateDiscount(50, 100)).toBe(0);
});
});
Работа с моками
Для изоляции тестов используйте моки. Пример с сервисом, зависящим от БД:
// src/services/user.service.ts
import { db } from '../database';
export class UserService {
async getUser(id: number) {
const user = await db.findUser(id);
if (!user) throw new Error('User not found');
return user;
}
}
// src/__tests__/user.service.test.ts
import { UserService } from '../services/user.service';
import { db } from '../database';
jest.mock('../database');
describe('UserService', () => {
it('должен вернуть пользователя', async () => {
const mockUser = { id: 1, name: 'Alice' };
(db.findUser as jest.Mock).mockResolvedValue(mockUser);
const service = new UserService();
const result = await service.getUser(1);
expect(result).toEqual(mockUser);
});
});
2. Интеграционное тестирование: проверка взаимодействия компонентов
Интеграционные тесты проверяют, как модули работают вместе: API-роуты, базы данных, внешние сервисы. Для TypeScript-приложений на Node.js часто используют Supertest в паре с Jest.
Настройка интеграционного теста для Express/NestJS
npm install --save-dev supertest @types/supertest
Пример теста для REST API (NestJS):
// src/__tests__/app.integration.test.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('GET /users должен вернуть массив', async () => {
const response = await request(app.getHttpServer())
.get('/users')
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
});
it('POST /users должен создать пользователя', async () => {
const newUser = { name: 'Bob', email: 'bob@test.com' };
const response = await request(app.getHttpServer())
.post('/users')
.send(newUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Bob');
});
afterAll(async () => {
await app.close();
});
});
Работа с тестовой базой данных
Для изоляции используйте in-memory базу (например, SQLite) или Docker-контейнеры. Пример с TypeORM:
// В тестовом модуле
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from '../users/entities/user.entity';
// Подмена репозитория
const mockUserRepo = {
find: jest.fn().mockResolvedValue([{ id: 1, name: 'Test' }]),
save: jest.fn().mockResolvedValue({ id: 2, name: 'New' }),
};
3. Сквозное (e2e) тестирование: проверка полного пользовательского сценария
E2E-тесты эмулируют действия реального пользователя в браузере. Для TypeScript-проектов отлично подходит Playwright — он поддерживает TypeScript, работает быстро и умеет ждать элементы.
Установка Playwright
npm install --save-dev @playwright/test
npx playwright install
Пример e2e-теста для веб-приложения
// tests/login.e2e.ts
import { test, expect } from '@playwright/test';
test('пользователь может войти в систему', async ({ page }) => {
// Открыть страницу логина
await page.goto('https://example.com/login');
// Заполнить форму
await page.fill('[data-testid="email"]', 'user@example.com');
await page.fill('[data-testid="password"]', 'securePass123');
await page.click('[data-testid="submit"]');
// Проверить, что перенаправило на дашборд
await expect(page).toHaveURL(/\/dashboard/);
await expect(page.locator('h1')).toHaveText('Welcome back!');
});