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

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

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

Начать курс

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

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

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

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

  • Изоляцию: каждый тест запускается в чистом контейнере без влияния на другие тесты или локальную среду.
  • Воспроизводимость: одинаковые условия выполнения на машине разработчика и в CI/CD.
  • Масштабируемость: возможность параллельного запуска тестов с разными версиями зависимостей (PostgreSQL, Redis, Elasticsearch).
  • Быстрое восстановление: пересоздание контейнеров занимает секунды, а не часы настройки.

Настройка продвинутого тестового окружения

Рассмотрим типичное Node.js приложение, которое использует PostgreSQL и Redis. Вместо простого docker-compose.yml создадим гибкую конфигурацию с поддержкой параллельных тестов и health checks.

Шаг 1: Docker Compose с динамическими портами

Чтобы избежать конфликтов портов при параллельном запуске, используем динамическое выделение:

# docker-compose.test.yml
version: '3.8'
services:
  postgres_test:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: test_db
      POSTGRES_USER: test_user
      POSTGRES_PASSWORD: test_pass
    ports:
      - "0:5432"  # Динамический порт
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
      interval: 2s
      timeout: 5s
      retries: 10
    tmpfs: /var/lib/postgresql/data  # Данные в RAM для скорости

  redis_test:
    image: redis:7-alpine
    ports:
      - "0:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 5s
      retries: 10

Шаг 2: Утилиты для запуска тестов с Docker

Создадим класс-помощник, который автоматически находит динамические порты и ожидает готовности сервисов:

// test/setup/docker-manager.js
const { DockerComposeEnvironment, Wait } = require('testcontainers');
const { Client } = require('pg');
const Redis = require('ioredis');

class DockerTestManager {
    constructor() {
        this.environment = null;
        this.config = {
            pgPort: null,
            redisPort: null
        };
    }

    async start() {
        this.environment = await new DockerComposeEnvironment(
            '.', 
            'docker-compose.test.yml'
        ).withWaitStrategy('postgres_test', Wait.forHealthCheck())
         .withWaitStrategy('redis_test', Wait.forHealthCheck())
         .up();

        // Извлекаем маппинг портов
        const pgContainer = this.environment.getContainer('postgres_test');
        const redisContainer = this.environment.getContainer('redis_test');
        
        this.config.pgPort = pgContainer.getMappedPort(5432);
        this.config.redisPort = redisContainer.getMappedPort(6379);
        
        return this.config;
    }

    async stop() {
        if (this.environment) {
            await this.environment.down();
        }
    }
}

module.exports = DockerTestManager;

Продвинутые сценарии интеграционного тестирования

Теперь перейдем к конкретным тестам, которые проверяют реальное взаимодействие компонентов.

Тестирование записи и чтения из PostgreSQL

Проверим, что наше приложение корректно сохраняет и извлекает данные:

// test/integration/user.repository.test.js
const { Client } = require('pg');
const DockerTestManager = require('../setup/docker-manager');

describe('User Repository Integration', () => {
    let dockerManager;
    let pgClient;

    beforeAll(async () => {
        dockerManager = new DockerTestManager();
        const config = await dockerManager.start();
        
        pgClient = new Client({
            host: 'localhost',
            port: config.pgPort,
            database: 'test_db',
            user: 'test_user',
            password: 'test_pass'
        });
        await pgClient.connect();
        
        // Создаем таблицу для тестов
        await pgClient.query(`
            CREATE TABLE IF NOT EXISTS users (
                id SERIAL PRIMARY KEY,
                email VARCHAR(255) UNIQUE NOT NULL,
                name VARCHAR(100) NOT NULL,
                created_at TIMESTAMP DEFAULT NOW()
            )
        `);
    });

    afterAll(async () => {
        await pgClient.end();
        await dockerManager.stop();
    });

    beforeEach(async () => {
        await pgClient.query('TRUNCATE users RESTART IDENTITY CASCADE');
    });

    test('should create a new user', async () => {
        const result = await pgClient.query(
            'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
            ['test@example.com', 'Test User']
        );
        
        expect(result.rows[0]).toMatchObject({
            email: 'test@example.com',
            name: 'Test User'
        });
        expect(result.rows[0].id).toBeDefined();
    });

    test('should enforce unique email constraint', async () => {
        await pgClient.query(
            'INSERT INTO users (email, name) VALUES ($1, $2)',
            ['duplicate@example.com', 'First']
        );
        
        await expect(
            pgClient.query(
                'INSERT INTO users (email, name) VALUES ($1, $2)',
                ['duplicate@example.com', 'Second']
            )
        ).rejects.toThrow();
    });
});

Тестирование кэширования с Redis

Проверим, что кэш работает корректно и сбрасывается при необходимости:

// test/integration/cache.service.test.js
const Redis = require('ioredis');
const DockerTestManager = require('../setup/docker-manager');

describe('Cache Service Integration', () => {
    let dockerManager;
    let redisClient;

    beforeAll(async () => {
        dockerManager = new DockerTestManager();
        const config = await dockerManager.start();
        
        redisClient = new Redis({
            host: 'localhost',
            port: config.redisPort
        });
    });

    afterAll(async () => {
        await redisClient.quit();
        await dockerManager.stop();
    });

    beforeEach(async () => {
        await redisClient.flushall();
    });

    test('should set and get cached value', async () => {
        await redisClient.set('user:1', JSON.stringify({ id: 1, name: 'Alice' }));
        
        const cached = await redisClient.get('user:1');
        expect(JSON.parse(cached)).toEqual({ id: 1, name: 'Alice'                

Блоги

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