Тестирование в TypeScript: Unit, Integration, E2E (продвинутый уровень)

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

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

Начать курс

Продвинутое тестирование в TypeScript: от Unit до E2E

Тестирование — это не просто проверка работоспособности кода, а фундамент профессиональной разработки на TypeScript. На продвинутом уровне мы переходим от простых assert-ов к построению изолированных, быстрых и надежных тестовых пирамид. В этой статье мы разберем практические подходы к unit, интеграционному и e2e-тестированию с использованием лучших инструментов экосистемы TypeScript.

1. Продвинутое Unit-тестирование с Jest и TypeScript

Unit-тесты проверяют изолированные модули. В TypeScript ключевая сложность — работа с зависимостями, типами и асинхронностью.

1.1. Моки и типизация с jest.mocked

Используйте jest.mocked для полной типизации моков. Это гарантирует, что вы не вызовете несуществующий метод.

import { fetchUserData } from './api';
import { processUser } from './userProcessor';

jest.mock('./api');

const mockedFetchUserData = jest.mocked(fetchUserData);

describe('processUser', () => {
  it('должен обработать данные пользователя', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@test.com' };
    mockedFetchUserData.mockResolvedValue(mockUser);

    const result = await processUser(1);

    expect(result).toEqual({
      displayName: 'Alice',
      contact: 'alice@test.com'
    });
    expect(mockedFetchUserData).toHaveBeenCalledWith(1);
  });

  it('должен выбросить ошибку при отсутствии email', async () => {
    mockedFetchUserData.mockResolvedValue({ id: 2, name: 'Bob', email: null });
    await expect(processUser(2)).rejects.toThrow('Email is required');
  });
});

1.2. Тестирование сложных типов и generics

Для функций с дженериками создавайте тестовые типы-помощники, чтобы избежать дублирования.

// Код
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

// Тест
type TestArray = number[];
const array: TestArray = [10, 20, 30];

describe('getFirstElement', () => {
  it('должен вернуть первый элемент числа', () => {
    const result = getFirstElement(array);
    expect(result).toBe(10);
    // TypeScript автоматически выводит тип number
  });

  it('должен вернуть undefined для пустого массива', () => {
    const result = getFirstElement([]);
    expect(result).toBeUndefined();
  });
});

2. Интеграционное тестирование: база данных и API

Интеграционные тесты проверяют взаимодействие модулей: контроллеров, сервисов и базы данных. Для TypeScript-приложений на Node.js идеально подходит связка Jest + Supertest + Testcontainers.

2.1. Настройка тестовой базы данных с Testcontainers

Testcontainers поднимает PostgreSQL в Docker, гарантируя чистую среду для каждого теста.

import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { Client } from 'pg';

let container: StartedPostgreSqlContainer;
let client: Client;

beforeAll(async () => {
  container = await new PostgreSqlContainer().start();
  client = new Client({
    connectionString: container.getConnectionUri()
  });
  await client.connect();
  await client.query(`
    CREATE TABLE users (
      id SERIAL PRIMARY KEY,
      name VARCHAR(100) NOT NULL,
      email VARCHAR(100) UNIQUE NOT NULL
    );
  `);
}, 30000); // таймаут 30 секунд для скачивания образа

afterAll(async () => {
  await client.end();
  await container.stop();
});

describe('User Repository', () => {
  it('должен создать и вернуть пользователя', async () => {
    const res = await client.query(
      `INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *`,
      ['Test', 'test@example.com']
    );
    expect(res.rows[0].name).toBe('Test');
  });
});

2.2. Тестирование REST API с Supertest

Запускаем Express-приложение и тестируем эндпоинты, используя реальную БД.

import request from 'supertest';
import { app } from '../app';
import { db } from '../database';

beforeEach(async () => {
  await db.query('DELETE FROM users');
});

describe('POST /api/users', () => {
  it('должен создать пользователя и вернуть 201', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({ name: 'John', email: 'john@test.com' })
      .expect(201);

    expect(response.body).toMatchObject({
      name: 'John',
      email: 'john@test.com'
    });
    expect(response.body).toHaveProperty('id');
  });

  it('должен вернуть 409 при дубликате email', async () => {
    await request(app)
      .post('/api/users')
      .send({ name: 'First', email: 'dup@test.com' });

    await request(app)
      .post('/api/users')
      .send({ name: 'Second', email: 'dup@test.com' })
      .expect(409);
  });
});

3. E2E-тестирование с Playwright и TypeScript

End-to-end тесты симулируют действия реального пользователя в браузере. Playwright — лучший выбор для TypeScript благодаря нативной поддержке типов и автогенерации селекторов.

3.1. Структура Page Object Model (POM)

Используйте паттерн Page Object для переиспользования селекторов и логики.

// pages/LoginPage.ts
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}

  async navigate() {
    await this.page.goto('/login');
  }

  async login(username: string, password: string) {
    await this.page.fill('#username', username);
    await this.page.fill('#password', password);
    await this.page.click('button[type="submit"]');
  }

  async getErrorMessage() {
    return this.page.textContent('.error-message');
  }
}

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('успешный вход в систему', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('admin', 'password123');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.locator('h1')).toContainText('Dashboard');
});

test('отображение ошибки при неверном пароле', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('admin', 'wrong');

  const error = await loginPage.getErrorMessage();
  expect(error).toBe('Invalid credentials');
});

Блоги

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