Интеграционное тестирование TypeScript с Docker: полное руководство

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

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

Начать курс

Интеграционное тестирование TypeScript с Docker: от А до Я

Современная разработка на TypeScript требует не только качественного юнит-тестирования, но и проверки взаимодействия компонентов в реальных условиях. Интеграционное тестирование с использованием Docker позволяет поднимать изолированные окружения с базами данных, кэшами и другими сервисами, что делает тесты максимально приближенными к production. В этой статье мы разберем, как настроить интеграционное тестирование TypeScript-приложения с помощью Docker, напишем тесты для REST API и рассмотрим лучшие практики.

Почему Docker для интеграционного тестирования?

Docker решает одну из главных проблем интеграционных тестов — зависимость от внешних сервисов. Вместо того чтобы использовать общую базу данных или Redis, которые могут быть заняты другими разработчиками, мы поднимаем изолированные контейнеры для каждого тестового запуска. Это гарантирует:

  • Чистоту тестов: каждый запуск начинается с пустого состояния
  • Повторяемость: тесты работают одинаково на любой машине
  • Изоляцию: тесты не влияют друг на друга и на production-данные
  • Скорость: контейнеры поднимаются за секунды

Настройка проекта и Docker Compose

Создадим простой проект на TypeScript с Express и PostgreSQL. Наша задача — протестировать эндпоинт создания пользователя. Начнем с файла docker-compose.test.yml:

version: '3.8'
services:
  db_test:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    ports:
      - "5433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5

  app_test:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      DATABASE_URL: postgres://testuser:testpass@db_test:5432/testdb
      NODE_ENV: test
    ports:
      - "3001:3000"
    depends_on:
      db_test:
        condition: service_healthy

Теперь создадим Dockerfile.test для сборки TypeScript-приложения:

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]

Написание интеграционных тестов с Jest и Supertest

Установим необходимые пакеты:

npm install --save-dev jest @types/jest ts-jest supertest @types/supertest

Создадим конфигурацию Jest (jest.config.ts):

export default {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.test.ts'],
  setupFilesAfterSetup: ['./jest.setup.ts'],
  testTimeout: 30000,
};

Файл jest.setup.ts будет отвечать за запуск и остановку Docker-контейнеров:

import { execSync, spawn } from 'child_process';
import path from 'path';

const composeFile = path.resolve(__dirname, '../docker-compose.test.yml');

beforeAll(async () => {
  // Поднимаем контейнеры
  execSync(`docker-compose -f ${composeFile} up -d --build`, { stdio: 'inherit' });
  
  // Ждем, пока приложение станет доступным
  await new Promise((resolve) => setTimeout(resolve, 5000));
});

afterAll(async () => {
  // Останавливаем и удаляем контейнеры
  execSync(`docker-compose -f ${composeFile} down -v`, { stdio: 'inherit' });
});

Теперь напишем сам тест. Создадим файл __tests__/users.test.ts:

import request from 'supertest';

const BASE_URL = 'http://localhost:3001/api';

describe('Users API Integration Tests', () => {
  const testUser = {
    name: 'Иван Иванов',
    email: 'ivan@example.com',
    password: 'securePass123'
  };

  it('должен создать нового пользователя', async () => {
    const response = await request(BASE_URL)
      .post('/users')
      .send(testUser)
      .expect(201);

    expect(response.body).toHaveProperty('id');
    expect(response.body.name).toBe(testUser.name);
    expect(response.body.email).toBe(testUser.email);
  });

  it('должен получить пользователя по ID', async () => {
    // Сначала создаем пользователя
    const createRes = await request(BASE_URL)
      .post('/users')
      .send(testUser);

    const userId = createRes.body.id;

    // Получаем его по ID
    const response = await request(BASE_URL)
      .get(`/users/${userId}`)
      .expect(200);

    expect(response.body.id).toBe(userId);
    expect(response.body.email).toBe(testUser.email);
  });

  it('должен вернуть 404 для несуществующего пользователя', async () => {
    await request(BASE_URL)
      .get('/users/9999')
      .expect(404);
  });

  it('должен вернуть ошибку при дублировании email', async () => {
    await request(BASE_URL)
      .post('/users')
      .send(testUser)
      .expect(201);

    await request(BASE_URL)
      .post('/users')
      .send(testUser)
      .expect(409);
  });
});

Оптимизация и best practices

Чтобы интеграционные тесты были быстрыми и надежными, следуйте этим рекомендациям:

  • Используйте healthcheck'и в Docker Compose: это гарантирует, что сервисы готовы к работе до запуска тестов
  • Параллелизируйте тесты: если у вас много тестов, используйте jest --maxWorkers=4 для ускорения
  • Очищайте данные между тестами: используйте beforeEach для удаления записей из базы
  • Не тестируйте то, что уже покрыто юнит-тестами: интеграционные тесты должны проверять взаимодействие компонентов, а не бизнес-логику

Пример очистки данных между тестами:

import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

beforeEach(async () => {
  await pool.query('TRUNCATE TABLE users CASCADE');
});

Запуск тестов в CI/CD

Для запуска в GitHub Actions используйте следующий workflow:

name: Integration Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses                

Блоги

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