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

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

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

Начать курс

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

Интеграционное тестирование — критически важный этап разработки на JavaScript (Node.js). Оно проверяет взаимодействие вашего приложения с внешними сервисами: базами данных, API, кэшами и очередями. Использование Docker для этих целей позволяет изолировать тестовую среду, гарантировать её воспроизводимость и избежать проблем с "это работает на моей машине". В этой статье мы разберём, как настроить интеграционное тестирование JavaScript-приложений с помощью Docker, Docker Compose и популярных тестовых фреймворков.

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

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

  • Изоляцию: каждый тестовый запуск происходит в чистом контейнере.
  • Воспроизводимость: одинаковое окружение для всех разработчиков и CI/CD.
  • Скорость: контейнеры запускаются за секунды, а не минуты.
  • Управление зависимостями: легко поднять PostgreSQL, Redis, RabbitMQ и другие сервисы.

Настройка тестовой среды с Docker Compose

Начнём с создания проекта. Допустим, у нас есть простое Node.js приложение, которое сохраняет данные в PostgreSQL. Создадим файл docker-compose.test.yml:

version: '3.8'

services:
  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      - db
      - redis
    environment:
      - NODE_ENV=test
      - DATABASE_URL=postgresql://testuser:testpass@db:5432/testdb
      - REDIS_URL=redis://redis:6379
    volumes:
      - ./coverage:/app/coverage

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb
    ports:
      - "5432"
    tmpfs:
      - /var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379"

Обратите внимание на tmpfs для PostgreSQL — это гарантирует, что после завершения тестов данные не сохранятся, и каждый запуск будет начинаться с чистой базы.

Dockerfile для тестов

Создадим Dockerfile.test, который оптимизирован для тестирования, а не для продакшена:

FROM node:18-alpine

WORKDIR /app

# Копируем только package.json для кэширования слоя зависимостей
COPY package*.json ./
RUN npm ci --only=production && npm ci --only=dev

# Копируем исходный код
COPY . .

# Устанавливаем wait-for-it для ожидания готовности сервисов
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

CMD ["sh", "-c", "/wait-for-it.sh db:5432 -- /wait-for-it.sh redis:6379 -- npm test"]

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

Теперь напишем сами тесты. Используем Jest и Supertest для тестирования API. Создадим файл tests/integration/user.test.js:

const request = require('supertest');
const { app } = require('../src/app');
const { Pool } = require('pg');

describe('User API Integration Tests', () => {
  let pool;

  beforeAll(async () => {
    // Создаём пул соединений к тестовой БД
    pool = new Pool({
      connectionString: process.env.DATABASE_URL
    });

    // Выполняем миграции перед тестами
    const migrationSQL = `
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL,
        email VARCHAR(255) UNIQUE NOT NULL,
        created_at TIMESTAMP DEFAULT NOW()
      );
    `;
    await pool.query(migrationSQL);
  });

  afterAll(async () => {
    // Очищаем и закрываем соединение
    await pool.query('DROP TABLE IF EXISTS users');
    await pool.end();
  });

  describe('POST /api/users', () => {
    it('should create a new user and return 201', async () => {
      const newUser = {
        name: 'Иван Петров',
        email: 'ivan@example.com'
      };

      const response = await request(app)
        .post('/api/users')
        .send(newUser)
        .expect(201);

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

    it('should return 409 for duplicate email', async () => {
      const user = {
        name: 'Петр Иванов',
        email: 'petr@example.com'
      };

      // Сначала создаём пользователя
      await request(app)
        .post('/api/users')
        .send(user)
        .expect(201);

      // Пытаемся создать с тем же email
      const response = await request(app)
        .post('/api/users')
        .send(user)
        .expect(409);

      expect(response.body.error).toBe('Email already exists');
    });
  });

  describe('GET /api/users/:id', () => {
    it('should return user by ID', async () => {
      // Создаём тестового пользователя
      const createRes = await request(app)
        .post('/api/users')
        .send({ name: 'Тест', email: 'test@example.com' })
        .expect(201);

      const userId = createRes.body.id;

      const response = await request(app)
        .get(`/api/users/${userId}`)
        .expect(200);

      expect(response.body.name).toBe('Тест');
      expect(response.body.email).toBe('test@example.com');
    });

    it('should return 404 for non-existent user', async () => {
      const response = await request(app)
        .get('/api/users/99999')
        .expect(404);

      expect(response.body.error).toBe('User not found');
    });
  });
});

Запуск тестов и работа с отчётами

Для запуска интеграционных тестов используем команду:

docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit

Флаг --abort-on-container-exit гарантирует, что если тесты упадут, все контейнеры остановятся. Для получения отчётов о покрытии кода добавим в package.json:

{
  "scripts": {
    "test": "jest --coverage --forceExit --detectOpenHandles",
    "test:integration": "docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit"
  }
}

Интеграция с CI/CD (GitHub Actions)

Блоги

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