Тестирование в TypeScript: unit, integration, e2e от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Тестирование в 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!');
});

Блоги

Рекомендаций по книгам