Интеграционное тестирование TypeScript с Docker: продвинутый уровень

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

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

Начать курс

Интеграционное тестирование TypeScript с Docker: продвинутый уровень

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

Почему Docker — идеальный инструмент для интеграционных тестов TypeScript

Традиционные подходы к интеграционному тестированию часто страдают от проблем с окружением: "на моей машине работает". Docker решает эту проблему, предоставляя:

  • Изоляцию — каждый тест запускается в чистом контейнере, без влияния на другие процессы
  • Воспроизводимость — одинаковое окружение на всех этапах: локально, в CI, на staging
  • Скорость — легковесные контейнеры запускаются за секунды
  • Гибкость — легко комбинировать разные версии БД, кэшей, очередей

Для TypeScript-проектов Docker особенно полезен, так как позволяет тестировать не только Node.js рантайм, но и взаимодействие с TypeScript-компилятором, транспиляцией и сборкой.

Настройка Docker-окружения для TypeScript-тестов

Начнем с базовой конфигурации. Создадим Dockerfile для продакшн-сборки и отдельный для тестов:

# Dockerfile.test
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/

# Сборка TypeScript
RUN npm run build

# Этап для тестов
FROM node:18-alpine AS test-runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY tests/ ./tests/
COPY jest.config.ts ./

# Запуск интеграционных тестов
CMD ["npx", "jest", "--config", "jest.config.ts", "--testMatch", "**/*.integration.test.ts"]

Для управления несколькими сервисами используем docker-compose.yml:

version: '3.8'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
      - REDIS_HOST=redis
      - REDIS_PORT=6379
    volumes:
      - ./tests:/app/tests:ro
    networks:
      - test-network

  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - test-network

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - test-network

networks:
  test-network:
    driver: bridge

Написание продвинутых интеграционных тестов на TypeScript

Теперь создадим тест, который проверяет полный сценарий: запись в БД, кэширование в Redis, и отправку события в очередь. Используем библиотеку testcontainers для динамического управления контейнерами из TypeScript-кода:

// tests/integration/user-registration.integration.test.ts
import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
import axios from 'axios';

describe('User Registration Integration', () => {
  let postgresContainer: StartedTestContainer;
  let redisContainer: StartedTestContainer;
  let appContainer: StartedTestContainer;

  beforeAll(async () => {
    // Запускаем PostgreSQL
    postgresContainer = await new PostgreSqlContainer()
      .withDatabase('testdb')
      .withUsername('testuser')
      .withPassword('testpass')
      .start();

    // Запускаем Redis
    redisContainer = await new RedisContainer()
      .start();

    // Собираем и запускаем наше приложение
    appContainer = await GenericContainer
      .fromDockerfile('./')
      .withBuildArgs({
        BUILD_MODE: 'test'
      })
      .build();

    // Запускаем с переменными окружения
    appContainer = await appContainer
      .withEnvironment({
        DB_HOST: postgresContainer.getHost(),
        DB_PORT: String(postgresContainer.getMappedPort(5432)),
        REDIS_HOST: redisContainer.getHost(),
        REDIS_PORT: String(redisContainer.getMappedPort(6379)),
        NODE_ENV: 'test'
      })
      .withExposedPorts(3000)
      .withWaitStrategy(Wait.forHttp('/', 3000))
      .start();
  });

  afterAll(async () => {
    await appContainer?.stop();
    await redisContainer?.stop();
    await postgresContainer?.stop();
  });

  it('должен успешно зарегистрировать пользователя и сохранить в БД', async () => {
    const appUrl = `http://localhost:${appContainer.getMappedPort(3000)}`;
    
    const response = await axios.post(`${appUrl}/api/users/register`, {
      email: 'test@example.com',
      password: 'StrongP@ss1',
      name: 'Test User'
    });

    expect(response.status).toBe(201);
    expect(response.data).toHaveProperty('id');
    expect(response.data.email).toBe('test@example.com');

    // Проверяем, что пользователь сохранен в БД
    const dbResponse = await axios.get(
      `${appUrl}/api/users/${response.data.id}`
    );
    expect(dbResponse.data.name).toBe('Test User');
  });

  it('должен кэшировать данные в Redis после регистрации', async () => {
    const appUrl = `http://localhost:${appContainer.getMappedPort(3000)}`;
    
    // Регистрируем пользователя
    const registerRes = await axios.post(`${appUrl}/api/users/register`, {
      email: 'cache_test@example.com',
      password: 'AnotherP@ss1'
    });

    // Немедленный запрос должен вернуть данные из кэша
    const cachedRes = await axios.get(
      `${appUrl}/api/users/${registerRes.data.id}`
    );
    expect(cachedRes.headers['x-cache']).toBe('HIT');                

Блоги

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